一、Metro 是什么
Metro 是一个针对 React Native的JavaScript模块打包器,他接收一个entry file (入口文件) 和一些配置作为参数,返回给你一个单独的JavaScript文件,这个文件包含了你写的所有的JavaScript 代码和所有的依赖。
也就是说Metro把你写的几十上百个js文件和几百个node_modules的依赖,打包成了一个文件。
React Native 提供的 metro 自带分包功能。metro我们本来就一直在用,只要在metro打包的时候,提供相应的打包规则。就可以实现rn的分包了。
二、Metro的工作原理
Metro 的打包过程有3个独立的阶段
-
Resolution 阶段
Metro 需要建立一个你的入口文件所需要的所有的模块的表,为了找到一个文件依赖了哪些文件,Metro 使用了一个resolver。在实际中,Resolution阶段是和transformation阶段并行进行的。
-
Transformation阶段
所有的模块都要经历一个 transformer, transformer 负责把一个模块转换成RN能理解的格式;
-
Serialization阶段
一旦模块被转换完成,就会马上被serialized,通过serializer,把上一个阶段转换好的模块组合成一个或多个bundle,bundle 就是字面意思:把一堆模块组合成一个单独的JavaScript文件
三、实践(均以Android为例,IOS流程一致)
metro 关键****api****介绍
重点关注Serialization阶段
我们分包需要用的选项主要是两个:
createModuleIdFactory:这个函数传入要打包的 module 文件的绝对路径,返回这个 module 在打包的时候生成的 id。
processModuleFilter:这个函数传入 module 信息,返回一个 boolean 值,false 则表示这个文件不打入当前的包。
入口文件示例:
基础包basics.js
import 'react';
import 'react-native';
import React, { Component } from "react";
import { AppRegistry, Text } from "react-native";
class App extends React.Component {
render() {
return <Text> loading </Text>;
}
}
AppRegistry.registerComponent("sdk", () => App);
业务包business.js
import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { AppRegistry } from 'react-native';
export default class App extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>欢迎来到业务包</Text>
</View>
);
}
}
AppRegistry.registerComponent('App1', () => App);
统一moduleId生成规则:
const pathSep = require('path').sep;
const md5 = require('js-md5');
//是否加密
const isEncrypt = false;
function getModuleId(projectRootPath, path) {
let name = '';
if (path.indexOf(projectRootPath) == 0) {
/*
这里是react native 自带库以外的其他库,因是绝对路径,带有设备信息,
为了避免重复名称,可以保留node_modules直至结尾
如/{User}/{username}/{userdir}/node_modules/xxx.js 需要将设备信息截掉
*/
name = path.substr(projectRootPath.length + 1);
console.log('root libraries:' + name);
}
//最后在将斜杠替换为空串或下划线 (可选)
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
name = name.replace(regExp, '_');
console.log('regExp name:' + name);
//名称加密
if (isEncrypt) {
name = md5(name);
console.log('encryptName:' + name);
}
return name;
}
主工程(基础包)metro 配置文件介绍 basics.config.js:
function createModuleIdFactory() {
const projectRootPath = __dirname;
return path => {
// 在这里我们拿到依赖的文件路径,
// 我们需要在这个函数块中,将路径以收集并且将这些数据生成文件
// 部署到我们内网的服务器中
// 当业务模块需要打包的时候,是否要将代码打进包中,将以这个文件为依据
let name = getModuleId(projectRootPath, path);
return name;
};
}
module.exports = {
serializer: {
createModuleIdFactory:createModuleIdFactory
}
};
业务工程(业务包)metro 配置文件介绍business.config.js:
const pathSep = require('path').sep;
const platformMap = require('基础包的打包数据');
let entry;
function postProcessModulesFilter(module) {
const projectRootPath = __dirname;
// 如果业务包没有数据,进程直接退出,
// 避免打入不必要的代码
if (platformMap == null || platformMap.length == 0) {
console.log('请先打基础包');
process.exit(1);
return false;
}
const path = module['path']
let name = getModuleId(projectRootPath, 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)){
// 如果之前主工程已经打过包的模块,进行排除 返回 false
return false;
}
}
// 没有特殊情况,则可以正常打包
return true;
}
function createModuleIdFactory() {
const projectRootPath = __dirname;
return path => {
let name = getModuleId(projectRootPath,path);
return name;
};
}
function getModulesRunBeforeMainModule(entryFilePath) {
entry = entryFilePath;
return [];
}
module.exports = {
serializer: {
createModuleIdFactory: createModuleIdFactory,
processModuleFilter: postProcessModulesFilter,
getModulesRunBeforeMainModule:getModulesRunBeforeMainModule
/* serializer options */
}
};
打基础包:
npx react-native bundle --platform android --dev false --entry-file 基础包入口文件basics.js --bundle-output 输出路径/basics.android.bundle --assets-dest 输出路径/ --config basics.config.js
完成示例:
[图片上传失败...(image-6482f5-1702608944452)]
打业务包:
npx react-native bundle --platform android --dev false --entry-file 业务包入口文件business.js --bundle-output 输出路径/android/business1.android.bundle --assets-dest 输出路径/ --config business.config.js
完成示例:
[图片上传失败...(image-954117-1702608944451)]
分包部署与下发客户端
所有类型的包打完之后,我们会压缩成zip包,可以部署到cdn上。 当客户端检测到有模块已经更新,则会从cdn上拉取相对应的代码包。
客户端加载顺序(****Android****)
客户端必须加载主工程的代码完成之后,才可以加载业务包,这是必要条件,不然会直接闪退。
以Android为例,核心代码如下:
加载主工程基础包:
...
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(getApplication())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
...
builder.setJSBundleLoader(new JSBundleLoader() {
@Override
public String loadScript(JSBundleLoaderDelegate jsBundleLoaderDelegate) {
JSBundleLoader.createFileLoader(basicDir + "basics.android.bundle",
basicDir, false).loadScript(jsBundleLoaderDelegate);
return "basics.android.bundle";
}
});
...
final ReactInstanceManager reactInstanceManager = builder.build();
reactRootView.startReactApplication(reactInstanceManager, "sdk"); //"sdk" 是基础包入口文件AppRegistry注册的组件名称
加载业务包:
...//
CatalystInstanceImpl catalystInstance = (CatalystInstanceImpl) reactInstanceManager.getCurrentReactContext().getCatalystInstance();
JSBundleLoader jsBundleLoader = JSBundleLoader.createFileLoader(
businessDir + "business1.android.bundle", businessDir, false
);
jsBundleLoader.loadScript(catalystInstance);
reactRootView.startReactApplication(reactInstanceManager, "App1"); //"App1" 是业务包入口文件AppRegistry注册的组件名称
以上。