RN Metro拆包实践

一、Metro 是什么

Metro 是一个针对 React Native的JavaScript模块打包器,他接收一个entry file (入口文件) 和一些配置作为参数,返回给你一个单独的JavaScript文件,这个文件包含了你写的所有的JavaScript 代码和所有的依赖。

也就是说Metro把你写的几十上百个js文件和几百个node_modules的依赖,打包成了一个文件。

React Native 提供的 metro 自带分包功能。metro我们本来就一直在用,只要在metro打包的时候,提供相应的打包规则。就可以实现rn的分包了。

二、Metro的工作原理

Metro 的打包过程有3个独立的阶段

  1. Resolution 阶段

Metro 需要建立一个你的入口文件所需要的所有的模块的表,为了找到一个文件依赖了哪些文件,Metro 使用了一个resolver。在实际中,Resolution阶段是和transformation阶段并行进行的。

  1. Transformation阶段

所有的模块都要经历一个 transformer, transformer 负责把一个模块转换成RN能理解的格式;

  1. 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注册的组件名称

以上。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容