Metro安装

​ 在运行 npm install 时 React Native 已经安装 Metro 了,其版本跟 React Native 版本有关,如果需要单独安装最新版 Metro,可以执行以下命令:

npm install --save-dev metro metro-core

yarn add --dev metro metro-core

Metro配置

​ 配置 Metro 有三种方法,分别为 metro.config.jsmetro.config.jsonpackage.json 中添加 metro 字段,常用的方式为 metro.config.js

Metro 配置内部结构请参考Metro官网

module.exports = {
  resolver: {
    /* resolver options */
  },
  transformer: {
    /* transformer options */
  },
  serializer: {
    /* serializer options */
  },
  server: {
    /* server options */
  }

  /* general options */
};

​ 每个 options 内都有很多配置选项,而对于拆包打包来说,最重要的是 serializer 选项内的 createModuleIdFactoryprocessModuleFilter
请添加图片描述

  • createModuleIdFactory :Metro 支持了通过此方法配置自定义模块 ID,同样支持字符串类型 ID,用于生成 require 语句的模块 ID,其类型为 () => (path: string) => number,其中 path 为各个 module 的完整路径。此方法的另一个用途就是多次打包时,对于同一个模块生成相同的 ID,下次更新发版时,不会因 ID 不同找不到 Module。
  • processModuleFilter:根据给出的条件,对 Module 进行过滤,将不需要的模块过滤掉。其类型为 (module: Array<Module>) => boolean,其中 module 为输出的模块,里面带着相应的参数,根据返回的布尔值判断是否过滤当前模块。返回 false 为过滤,不打入 bundle。

​ 下面参考配套的 Demo 工程 SampleProject 来具体说明一下该如何配置和使用 createModuleIdFactoryprocessModuleFilter

  1. 首先创建一个 SampleProject/MainProject/build/multibundle/moduleId.js 文件,其中自定义了 createModuleIdFactoryWrappostProcessModulesFilterWrap 两个方法,用于对应 createModuleIdFactoryprocessModuleFilter 两个配置选项。

    • createModuleIdFactoryWrap 主要作用是判断模块是基础包还是业务包,然后将获取到的 ModuleId 分别存放到 basicNameMap.jsonpageNameMap.json 文件中,留给 postProcessModulesFilterWrap 方法备用。

      basicNameMap.json 代表基础包所涉及到的 ModuleId 集合,pageNameMap.json 代表业务包所涉及到的 ModuleId 集合,这两个文件位于 moduleId.js 同目录的 map 文件夹下。

    • postProcessModulesFilterWrap 方法只需要在业务包的 metro.config.js 中配置,主要作用是通过一系列判断条件来判断需要打包的模块是否已经存在于 basicNameMap.json 文件中,如果存在,则返回 false,不进行打包;反之,则该模块需要进行打包。

      // SampleProject/MainProject/build/multibundle/moduleId.js
      
      const pathSep = require('path').sep;
      const fs = require('fs');
      const SHA256 = require('crypto-js/sha256');
      const basicNameArray = require('./map/basicNameMap.json');
      const homepageArray = require('./map/pageNameMap.json');
      
      function getModuleId(projectRootPath, modulePath, ...bundles) {
        let startIndex = modulePath.indexOf(projectRootPath);
        let pathRelative = modulePath.substr(startIndex + projectRootPath.length + 1);
        return String(SHA256(pathRelative));
      }
      
      function createModuleIdFactoryWrap(projectRootPath, ...bundles) {
        return () => {
          return (path) => {
            let moduleId = getModuleId(projectRootPath, path);
            let jsItem = path + ' ---> ' + moduleId;
            if ('basic' == bundles[0]) {
              if (!basicNameArray.includes(jsItem)) {
                basicNameArray.push(jsItem);
                fs.writeFileSync(
                  __dirname + pathSep + 'map' + pathSep + 'basicNameMap.json',
                  JSON.stringify(basicNameArray),
                );
              }
            } else {
              if (!homepageArray.includes(jsItem)) {
                homepageArray.push(jsItem);
                fs.writeFileSync(
                  __dirname + pathSep + 'map' + pathSep + 'pageNameMap.json',
                  JSON.stringify(homepageArray),
                );
              }
            }
      
            return moduleId;
          };
        };
      }
      
      function postProcessModulesFilterWrap(projectRootPath) {
        // 返回false则不打入bundle中
        console.log('----------postProcessModulesFilterWrap');
        return (module) => {
          const path = module.path;
          if (
            path.indexOf('__prelude__') >= 0 ||
            path.indexOf(
              pathSep +
                'node_modules' +
                pathSep +
                '@react-native' +
                pathSep +
                // 'js-polyfills',
                'polyfills',
            ) >= 0 ||
            path.indexOf(
              pathSep +
                'node_modules' +
                pathSep +
                'metro-runtime' +
                pathSep +
                'src' +
                pathSep +
                'polyfills',
            ) >= 0
          ) {
            return false;
          }
      
          const moduleId = getModuleId(projectRootPath, path);
          let jsItem = path + ' ---> ' + moduleId;
          if (path.indexOf(pathSep + 'node_modules' + pathSep) > 0) {
            if (
              'js' + pathSep + 'script' + pathSep + 'virtual' == module.output[0].type
            ) {
              return true;
            }
          }
      
          // 正在打业务包
          if (
            basicNameArray.includes(jsItem)
          ) {
            return false;
          }
      
          return true;
        };
      }
      
      module.exports = {createModuleIdFactoryWrap,postProcessModulesFilterWrap};
      

通过配置 ‌Metro 打包器‌ 来实现 React Native 应用的‌代码拆分(拆包)‌。其根本目的是将一个庞大的应用 bundle 文件,拆分成一个‌基础包(common.bundle)‌和多个**业务包/页面包(page1.bundle, page2.bundle…)。

核心目标:为什么要拆包?

减小首次加载体积‌:将 React、React Native 等基础库打包进基础包,用户只需在首次启动时加载一次。
按需加载‌:当用户访问某个特定页面时,才去动态加载该页面对应的业务包。这能极大提升应用的启动速度和运行时的用户体验。

实现拆包的两个关键配置,实现拆包最核心的是配置 serializer 选项下的两个方法:

  • createModuleIdFactory‌:

作用‌:为每一个模块(js文件)生成一个唯一的、固定的ID。
为何重要‌:在多次打包(比如先打基础包,再打业务包)时,确保同一个模块在不同次打包中得到的ID是相同的。这是为了避免业务包中重复包含基础包中已有的模块。如果ID不同或变动,运行时可能因找不到模块而报错。

  • processModuleFilter‌:

作用‌:像一个“过滤器”,决定哪些模块需要被打入当前的bundle中。


示例代码展示了一个完整的、可操作的拆包方案,其工作流程可以概括为两步:

  • 打基础包‌:

在 metro.config.js 中配置 createModuleIdFactory 为 createModuleIdFactoryWrap,并传入参数 ‘basic’。
打包过程中,它会为所有模块生成ID,并将这些“模块路径 -> ID”的映射关系记录到 basicNameMap.json 文件中。

  • 打业务包‌:

同样配置 createModuleIdFactory,但不传 ‘basic’ 参数,从而将映射关系记录到 pageNameMap.json。
同时,‌必须配置‌ processModuleFilter 为 postProcessModulesFilterWrap。
在打业务包时,这个过滤器会检查当前模块是否已经存在于 basicNameMap.json 中。
关键逻辑‌:如果发现某个模块已经在基础包里了,就返回 false 将其‌过滤掉‌,不打入当前的业务包中。

总而言之‌,这段说明提供了一套利用 Metro 内置配置,实现 React Native 代码拆包以优化性能的具体方法和代码范例。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐