React Native 熱更新方案
rn的業(yè)務(wù)越來(lái)越龐大,同時(shí)協(xié)同的團(tuán)隊(duì)越來(lái)越多. rn的動(dòng)態(tài)化就必須提上日程了.
對(duì)于rn熱更新,首當(dāng)其沖的問(wèn)題就是分包.
rn的基礎(chǔ)庫(kù)很大,再加上我們依賴了很多的三方庫(kù).這些代碼就必須在分包的時(shí)候單獨(dú)剝離出來(lái).
業(yè)務(wù)包讓他比較純粹的只有業(yè)務(wù)代碼. 這樣就可以保證業(yè)務(wù)包的體積比較小,保證熱更新時(shí)候的速度.
使用metro分包
React Native 提供的 metro 自帶分包功能。metro我們本來(lái)就一直在用,只要在metro打包的時(shí)候,提供相應(yīng)的打包規(guī)則.
就可以實(shí)現(xiàn)rn的分包了.
示例: ios打包
node ./node_modules/react-native/local-cli/cli.js bundle --platform ios --dev false --entry-file rn入口文件.js --bundle-output ./xxx/ --assets-dest ./xxx/ --config /{你的絕對(duì)路徑}/你的metro配置文件.js
metro 關(guān)鍵api介紹
我們分包需要用的選項(xiàng)主要是兩個(gè):
createModuleIdFactory:這個(gè)函數(shù)傳入要打包的 module 文件的絕對(duì)路徑鳍征,返回這個(gè) module 在打包的時(shí)候生成的 id。
processModuleFilter:這個(gè)函數(shù)傳入 module 信息,返回一個(gè) boolean 值沙咏,false 則表示這個(gè)文件不打入當(dāng)前的包食茎。
主工程分包
之前我們有提到過(guò)我們有一個(gè)項(xiàng)目是主工程,里面沒(méi)有任何的業(yè)務(wù)代碼.只有一些代碼運(yùn)行需要的所有依賴.
我們需要將所有的依賴全部收集起來(lái),當(dāng)業(yè)務(wù)模塊打包的時(shí)候,發(fā)現(xiàn)本地有這個(gè)依賴就可以使用 processModuleFilter
方法排除掉.
因?yàn)槲覀兊闹鞴こ膛c業(yè)務(wù)項(xiàng)目的依賴版本都是高度統(tǒng)一的.
所以我們node_modules下面的依賴包路徑都是完全一致的.
主工程的metro配置文件示例:
function createModuleIdFactory() {
return path => {
// 在這里我們拿到依賴的文件路徑,
// 我們需要在這個(gè)函數(shù)塊中,將路徑以收集并且將這些數(shù)據(jù)生成文件
// 部署到我們內(nèi)網(wǎng)的服務(wù)器中
// 當(dāng)業(yè)務(wù)模塊需要打包的時(shí)候,是否要將代碼打進(jìn)包中,將以這個(gè)文件為依據(jù)
return path;
};
}
module.exports = {
serializer: {
createModuleIdFactory:createModuleIdFactory
}
};
主工程入口文件示例:
// 這個(gè)文件我們會(huì)引入所有我們要用到的rn依賴,因?yàn)檫@些不常更新.
// metro打包的時(shí)候,會(huì)收集這些依賴的路徑
// 保證業(yè)務(wù)包打包的時(shí)候,不會(huì)重復(fù)打入
import React, {Component} from'react';
import { DeviceEventEmitter,Platform, NativeEventEmitter, NativeModules } from 'react-native';
import { SmartAssets } from "react-native-smartassets";
import 'moment/locale/zh-cn';
import 'react-navigation';
import '@tarojs/components-rn';
import '@tarojs/taro-router-rn';
import '@tarojs/taro-rn';
import '@tarojs/async-await';
import "@tarojs/mobx-rn";
import "dayjs";
import "querystring";
import "react-native-check-box";
import "classnames";
import "lodash.orderby";
import "react-native-swipe-list-view";
SmartAssets.initSmartAssets();
DeviceEventEmitter.addListener('sm-bundle-changed',
(bundlePath)=>{
SmartAssets.setBundlePath(bundlePath);
});
if(Platform.OS != 'android') {//ios
const {BundleloadEventEmiter} = NativeModules;
const bundleLoadEmitter = new NativeEventEmitter(BundleloadEventEmiter);
// eslint-disable-next-line no-unused-vars
const subscription = bundleLoadEmitter.addListener(
'BundleLoad',
(bundleInfo) => {
console.log('BundleLoad==' + bundleInfo.path);
SmartAssets.setBundlePath(bundleInfo.path);
}
);
}
require('react-native/Libraries/Core/checkNativeVersion');
業(yè)務(wù)模塊的分包
業(yè)務(wù)模塊打包主要是排除主模塊的依賴.
業(yè)務(wù)模塊的metro配置文件示例:
const pathSep = require('path').sep;
const platformMap = require('業(yè)務(wù)包的打包數(shù)據(jù)');
let entry;
function postProcessModulesFilter(module) {
const projectRootPath = __dirname;
// 如果業(yè)務(wù)包沒(méi)有數(shù)據(jù),進(jìn)程直接退出,
// 避免打入不必要的代碼
if (platformMap == null || platformMap.length == 0) {
console.log('請(qǐng)先打基礎(chǔ)包');
process.exit(1);
return false;
}
const path = module['path']
// 特殊的模塊也需要排除
if (path.indexOf("__prelude__") >= 0 ||
path.indexOf("/node_modules/react-native/Libraries/polyfills") >= 0 ||
path.indexOf("source-map") >= 0 ||
path.indexOf("/node_modules/metro/src/lib/polyfills/") >= 0) {
return false;
}
if (module['path'].indexOf(pathSep + 'node_modules' + pathSep) > 0) {
if ('js' + pathSep + 'script' + pathSep + 'virtual' == module['output'][0]['type']) {
return true;
}else if(platformMap.includes(name)){
// 如果之前主工程已經(jīng)打過(guò)包的模塊,進(jìn)行排除 返回 false
return false;
}
}
// 沒(méi)有特殊情況,則可以正常打包
return true;
}
function createModuleIdFactory() {
const projectRootPath = __dirname;
return path => {
let name = getModuleId(projectRootPath,path,entry,true);
return name;
};
}
function getModulesRunBeforeMainModule(entryFilePath) {
entry = entryFilePath;
return [];
}
module.exports = {
serializer: {
createModuleIdFactory: createModuleIdFactory,
processModuleFilter: postProcessModulesFilter,
getModulesRunBeforeMainModule:getModulesRunBeforeMainModule
/* serializer options */
}
};
分包部署與下發(fā)客戶端
所有類型的包打完之后,我們會(huì)壓縮成zip包,部署到cdn上.
當(dāng)客戶端檢測(cè)到有模塊已經(jīng)更新,則會(huì)從cdn上拉取相對(duì)應(yīng)的代碼包.
客戶端相關(guān)的加載方案我們借鑒的是以下項(xiàng)目:
https://github.com/smallnew/react-native-multibundler
客戶端加載順序
客戶端必須加載主工程的代碼完成之后,才可以加載業(yè)務(wù)包.
這是必要條件,不然會(huì)直接閃退.
客戶端熱更新容錯(cuò)
為了保證客戶端在任何時(shí)候都可以正常運(yùn)行,我們會(huì)在客戶端發(fā)版的時(shí)候?qū)⑺蟹职蜻M(jìn)客戶端中.當(dāng)客戶端熱更新失敗的時(shí)候,將會(huì)回滾到最初打進(jìn)客戶端的代碼.
以下是可能會(huì)導(dǎo)致更新失敗的場(chǎng)景,我們都會(huì)回滾渲染:
獲取代碼包超時(shí)
當(dāng)md5不匹配時(shí)
當(dāng)代碼解壓失敗時(shí)
代碼包版本號(hào)不合法
首次進(jìn)入就閃退
還有很多場(chǎng)景,這里就不一一列出了.
但是這一塊要絕對(duì)重視,一旦出錯(cuò),就會(huì)導(dǎo)致客戶端直接閃退.
尾巴
為了保證獨(dú)立開發(fā),獨(dú)立部署,我們的CI/CD根據(jù)這一套打包方案也做了很多的定制與開發(fā).這里就不展開篇幅來(lái)講了,分包其實(shí)很簡(jiǎn)單,最重要的還是客戶端的穩(wěn)定性.
在用戶手機(jī)上會(huì)出現(xiàn)非常多意想不到的極端環(huán)境.如何保證運(yùn)行時(shí)的穩(wěn)定性.
這里需要大家好好思考.