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

以上。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容