React Native 分包實踐
阿新 • • 發佈:2019-01-04
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()
}
}