1. 实现思路
1. RN从本地中读取bundle文件进行显示
2. 将JS文件进行分包打包
3. Native实现页面跳转,每个包跳转都为一个新的Activity
4. 进行bundle文件基础包与功能包的拆分,使用Google的diff_match_patch算法生成差异文件
5. 网络下载差异文件进行合并
6. 展示新页面
2. 操作步骤
Android
1. 修改MainApplication
2. 修改MainActivity
3. 创建LocalReactActivityDelegate
4. 打第一bundle
react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundle/index.android.bundle --assets-dest bundle/assets/
5. 打第二个bundle
react-native bundle --platform android --dev false --entry-file src/indexNet.js --bundle-output bundle/index1.android.bundle --assets-dest bundle/assets/
其中indexNet.js是新页面的入口文件
6. 使用google-diff-match-patch进行差异比对
3. 本地加载实现
1). 修改MainApplication
public class MainApplication extends Application {
private static MainApplication sInstance;
/**
* 获取当前对象
*/
public static MainApplication getInstance() {
return sInstance;
}
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
SoLoader.init(this, /* native exopackage */ false);
}
}
这里主要是将Application中实现的接口取消,转由MainActivity中提供.
2). 修改MainActivity
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "Unpacking";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new LocalReactActivityDelegate(this, getMainComponentName());
}
}
这里实现ReactActivity中的createReactActivityDelegate方法,自定义代理设置
3). LocalReactActivityDelegate代理代码
/**
* 本地ReactActivity代理
*/
class LocalReactActivityDelegate(activity: Activity, @Nullable mainComponentName: String) :
ReactActivityDelegate(activity, mainComponentName) {
private val mReactNativeHost: ReactNativeHost = object : ReactNativeHost(MainApplication.getInstance()) {
/**
* 返回ReactPackage对象
*/
override fun getPackages(): MutableList<ReactPackage> = Arrays.asList(
MainReactPackage(),
CustomPackage()
)
/**
* 是否为开发
*/
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
/**
* 返回JSBundle文件路径
*/
override fun getJSBundleFile(): String? = "${Environment.getExternalStorageDirectory()}/bundle/index.android.bundle"
}
/**
* 返回ReactNativeHost对象
*/
override fun getReactNativeHost(): ReactNativeHost = mReactNativeHost
}
4). Native实现页面跳转
I. 创建CustomPackage
/**
* 自定义ReactPackage
*/
class CustomPackage : ReactPackage {
/**
* 创建本地模块
*/
override fun createNativeModules(reactContext: ReactApplicationContext?): MutableList<NativeModule>
= Arrays.asList(PageModule(reactContext))
/**
* 创建视图管理者
*/
override fun createViewManagers(reactContext: ReactApplicationContext?): MutableList<ViewManager<View, ReactShadowNode<*>>>
= Collections.emptyList()
}
5). 创建PageMoudle
/**
* 页面管理模块
*/
@ReactModule(name = "PageModule")
class PageModule(reactContext: ReactApplicationContext?) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "PageModule"
/**
* 开启网络上的模块页面
*/
@ReactMethod
fun startNetActivity() {
val intent = Intent(currentActivity, NetActivity::class.java)
currentActivity?.startActivity(intent)
}
}
6). NetActivity页面
/**
* 新页面
*/
class NetActivity : ReactActivity() {
override fun getMainComponentName(): String? {
return "NetActivity"
}
override fun createReactActivityDelegate(): ReactActivityDelegate =
object: ReactActivityDelegate(this, mainComponentName) {
override fun getReactNativeHost(): ReactNativeHost = object : ReactNativeHost(MainApplication.getInstance()) {
override fun getPackages(): MutableList<ReactPackage> = Arrays.asList(MainReactPackage())
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override fun getJSBundleFile(): String? = "${Environment.getExternalStorageDirectory()}/bundle/index1.android.bundle"
}
}
}
7). RN中使用
修改App.js文件
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, NativeModules, TouchableOpacity} from 'react-native';
const PageModule = NativeModules.PageModule;
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
android:
'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
type Props = {};
export default class App extends Component<Props> {
startPage() {
PageModule.startNetActivity()
}
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
<TouchableOpacity
onPress={() => this.startPage()}
>
<Text style={styles.instructions}>开启新页面</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
8). 执行打包
react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundle/index.android.bundle --assets-dest bundle/assets/
--platform: 平台
--dev: 是否为开发模式
--entry-file: 入口文件
--bundle-output: bundle输出路径,这里注意bundle文件夹必须存在
--assets-dest: 资源文件存放路径
9). 测试
I. 生成之后将bundle文件夹拷贝至sdcard下
II. 如果想要在调试时可用,务必修改android/app/build.gradle文件,将bundleInDebug设置为false, 设置为false之后即不将bundle文件打包进app
project.ext.react = [
entryFile : "index.js",
bundleInDebug: false
]
III. 运行
注:此时无法开启新的页面,因为新页面的js文件不存在
4. 开启新页面
1). RN中创建入口文件indexNet.js
/** @format */
import {AppRegistry} from 'react-native';
import NetJs from './NetJs';
import {name as appName} from './NetActivity';
AppRegistry.registerComponent(appName, () => NetJs);
2). RN中创建NetActivity.json文件
{
"name": "NetActivity",
"displayName": "NetActivity"
}
注:此处的name和displayName应当与NetActivity.kt中的getMainComponentName方法返回的一致。
3). NetJs.js文件内容
import React, {PureComponent} from 'react';
import {
StyleSheet, Text,
View
} from 'react-native';
/**
* @FileName: NetJs
* @Author: mazaiting
* @Date: 2018/10/9
* @Description:
*/
class NetJs extends PureComponent {
render() {
return (
<View style={styles.container}>
<Text>Welcome mazaiting!</Text>
</View>
)
}
}
/**
* 样式属性
*/
const styles = StyleSheet.create({
container: {
backgroundColor: '#DDD'
}
});
/**
* 导出当前Module
*/
module.exports = NetJs;
4). 打包
react-native bundle --platform android --dev false --entry-file src/indexNet.js --bundle-output bundle/index1.android.bundle --assets-dest bundle/assets/
5). 拷贝文件
将bundle文件夹直接拷贝到sdcard目录下,此时再重新运行APP, 即可成功显示页面。
6). 带来的问题
初始包太大,每个包都讲RN基础内容打包,此时应该比较差异,进行差异化分解。
5. 差异化
1). diff-match-patch主页
2). 测试页面
3). 依赖
implementation 'google-diff-match-patch:google-diff-match-patch:0.1'
4). 代码使用
const val FILE1 = "index.android"
const val FILE2 = "index1.android"
const val FILE3 = "$FILE2-$FILE1.patch"
const val FILE_DIR = "E:\\android\\React-Native-Study\\Unpacking\\bundle\\"
const val SUFFIX = ".bundle"
object Patch {
@JvmStatic
fun main(args: Array<String>) {
val file1 = "$FILE_DIR$FILE1$SUFFIX"
val file2 = "$FILE_DIR$FILE2$SUFFIX"
val file3 = "$FILE_DIR$FILE3"
val patch = productPatch(file1, file2)
productFile(file1, file3, patch)
}
/**
* 生成文件
* @param fileOri 源文件
* @param fileDest 目标文件
* @param patchString 差异字符串
*/
private fun productFile(fileOri: String, fileDest: String, patchString: String?) {
// 创建对象
val patch = diff_match_patch()
// 读取文件
val file1 = readFile(fileOri)
// 获取补丁内容
val patchText = patch.patch_fromText(patchString)
// 应用补丁
val patchApply = patch.patch_apply(LinkedList(patchText), file1)
println("===============================================")
println("结果:" + patchApply[0])
// 写入文件内容
writeResult(fileDest, patchApply[0].toString())
println("===============================================")
// 获取执行结果数组,true为成功,false为失败
val patchResult: BooleanArray = patchApply[1] as BooleanArray
val result = StringBuilder()
patchResult.forEach { result.append("$it ") }
println("result: $result")
}
/**
* 写入文件
* @param fileDest 目标文件
* @param string 文件内容
*/
private fun writeResult(fileDest: String, string: String) {
// Read a file from disk and return the text contents.
val output = FileWriter(fileDest)
val writer = BufferedWriter(output)
try {
writer.write(string)
} finally {
writer.close()
output.close()
}
}
/**
* 生成差异patch
* @param fileOri 源文件
* @param fileDest 目标文件
* @return 差异字符串
*/
private fun productPatch(fileOri: String, fileDest: String): String? {
// 创建对象
val diff = diff_match_patch()
// 读取基础文件内容
val file1 = readFile(fileOri)
// 读取目标文件内容
val file2 = readFile(fileDest)
// 进行差异化
val diffString = diff.diff_main(file1, file2, true)
// 数组长度大于2执行
if (diffString.size > 2) {
diff.diff_cleanupSemantic(diffString)
}
println(diffString)
println("===============================================")
// 生成patch内容
val patchString = diff.patch_make(file1, diffString)
println(patchString)
println("===============================================")
// 将patch内容转为字符串
val patchText = diff.patch_toText(patchString)
println(patchText)
return patchText
}
/**
* 读取文件
* @param filename 文件名
* @return 文件内容
*/
@Throws(IOException::class)
private fun readFile(filename: String): String {
// Read a file from disk and return the text contents.
val sb = StringBuilder()
val input = FileReader(filename)
val bufRead = BufferedReader(input)
try {
var line = bufRead.readLine()
while (line != null) {
sb.append(line).append('\n')
line = bufRead.readLine()
}
} finally {
bufRead.close()
input.close()
}
return sb.toString()
}
}