美文网首页
metro源码分析之RN拆包

metro源码分析之RN拆包

作者: FingerStyle | 来源:发表于2022-11-29 21:27 被阅读0次

    拆包原理

    由于metro向开发者提供了Serialization的相关函数,我们便可以改写其默认实现,从而实现过滤我们不需要的模块的目的。

    拆包重点1--模块ID

    拆包第1个问题是确定基础包有哪些模块。我们是把React-native框架本身和一些必要的第三方库打到基础包里面,在此之外的模块都属于业务包,因此打业务包时需要参考基础包包含了哪些模块。
    我们在序列化生成bundle的过程的过程中会调用到baseJSBundle这个函数,他主要是遍历依赖图的所有模块,根据每一个模块的路径生成ID,然后根据ID的大小排序,再通过processModules过滤掉不用的模块并包装module,最终生成bundle.我们来看下baseJSBundle的代码:

    function baseJSBundle(entryPoint, preModules, graph, options) {
      for (const module of graph.dependencies.values()) {
        options.createModuleId(module.path);
      }
    
      const processModulesOptions = {
        filter: options.processModuleFilter,
        createModuleId: options.createModuleId,
        dev: options.dev,
        projectRoot: options.projectRoot,
      }; // Do not prepend polyfills or the require runtime when only modules are requested
    
      if (options.modulesOnly) {
        preModules = [];
      }
      //这里传入的processModuleOptions.filter就是processModuleFilter
      const preCode = processModules(preModules, processModulesOptions)
        .map(([_, code]) => code)
        .join("\n");
        //这里创建moduleId并根据ID大小来对module排序
      const modules = [...graph.dependencies.values()].sort(
        (a, b) => options.createModuleId(a.path) - options.createModuleId(b.path)
      );
      const postCode = processModules(
        getAppendScripts(
          entryPoint,
          [...preModules, ...modules],
          graph.importBundleNames,
          {
            asyncRequireModulePath: options.asyncRequireModulePath,
            createModuleId: options.createModuleId,
            getRunModuleStatement: options.getRunModuleStatement,
            inlineSourceMap: options.inlineSourceMap,
            projectRoot: options.projectRoot,
            runBeforeMainModule: options.runBeforeMainModule,
            runModule: options.runModule,
            serverRoot: options.serverRoot,
            sourceMapUrl: options.sourceMapUrl,
            sourceUrl: options.sourceUrl,
          }
        ),
        processModulesOptions
      )
        .map(([_, code]) => code)
        .join("\n");
      return {
        pre: preCode,
        post: postCode,
        modules: processModules(
          [...graph.dependencies.values()],
          processModulesOptions
        ).map(([module, code]) => [options.createModuleId(module.path), code]),
      };
    }
    

    这里面用到了createModuleId来确定moduleId,这个createModuleId是options的一个属性,对应的是Server.js的_createModuleId属性,而_createModuleId又是通过createModuleIdFactory方法执行后返回的函数。metroServer在初始化时,会读入开发者传入的配置,其中就包括了我们前面提到的createModuleIdFactory.

    class Server {
      constructor(config, options) {
        this._config = config;
        this._serverOptions = options;
    
        if (this._config.resetCache) {
          this._config.cacheStores.forEach((store) => store.clear());
    
          this._config.reporter.update({
            type: "transform_cache_reset",
          });
        }
    
        this._reporter = config.reporter;
        this._logger = Logger;
        this._platforms = new Set(this._config.resolver.platforms);
        this._isEnded = false; // TODO(T34760917): These two properties should eventually be instantiated
        // elsewhere and passed as parameters, since they are also needed by
        // the HmrServer.
        // The whole bundling/serializing logic should follow as well.
    
        this._createModuleId = config.serializer.createModuleIdFactory();//这里使用我们提供的createModuleIdFactory得到_createmoduleId函数
        this._bundler = new IncrementalBundler(config, {
          hasReducedPerformance: options && options.hasReducedPerformance,
          watch: options ? options.watch : undefined,
        });
        this._nextBundleBuildID = 1;
      }
      //...省略无关代码
    }
    

    createModuleIdFactory的默认实现在defaultCreateModuleIdFactory.js中

    function createModuleIdFactory() {
      const fileToIdMap = new Map();
      let nextId = 0;
      return (path) => {
        let id = fileToIdMap.get(path);
    
        if (typeof id !== "number") {
          id = nextId++;
          fileToIdMap.set(path, id);
        }
    
        return id;
      };
    }
    

    从这个函数的实现我们可以看出,moduleId是每次调用createModuleId时去fileToIdMap获取,如果拿到的id不是number类型(比如说undefined类型),则会在上一次生成的ID基础上+1,这样可以保证每个模块的id是唯一的。但是对于拆包来说,基础包和业务包是分开打的,每次moduleId都是从0开始自增,这样基础包和业务包里面就有很多模块的ID是一样的,导致无法根据moduleId过滤,所以我们需要把moduleId固定下来。
    这是我们重写的代码:

    function createModuleIdFactory() {
      console.log("-----------node dir", projectRootPath);
      return path => {
        const name = getModuleId(projectRootPath, path, entry, false, moduleMapDir);
        platformNameArray.push(name);
        const platformMapDir = projectRootPath + pathSep + moduleMapDir;
        if (!fs.existsSync(platformMapDir)) {
          fs.mkdirSync(platformMapDir);
        }
        const platformMapPath = platformMapDir + pathSep + platformMapName;
        fs.writeFileSync(platformMapPath, JSON.stringify(platformNameArray));
    
        return name;
      };
    }
    

    这里的getModuleId会根据包的类型,分别在不同的路径下区查找map文件(map文件是用来记录moduleId和路径、版本的映射关系的)来得到moduleId,这样保证每次打包moduleId都是固定的,以基础包loader为例,获取moduleId的方法如下:

    // 获取loader的ModuleId
    function getLoaderModuleIdByIndex(projectRootPath, path) {
      const pathRelative = getRelativePath(projectRootPath, path, false);
      const findPlatformItem = baseModuleIdMap.find(value => {
        return value.path === pathRelative;
      });
      if (findPlatformItem) {
        return findPlatformItem.id;
      } else {
        //基础包
        curModuleId = ++curModuleId;
        const version = getLibVersion(projectRootPath, pathRelative);
        baseModuleIdMap.push({ id: curModuleId, path: pathRelative, v: version });
        fs.writeFileSync(baseMappingPath, JSON.stringify(baseModuleIdMap));
        return curModuleId;
      }
    }
    

    这里提一下为什么要把版本号考虑进来,看过网上很多方案,都是只考虑了文件路径,但是没有考虑版本,其实同一个模块不同版本是很有可能不兼容的,像RN从0.59升级到0.60以上版本,就是一个很大的变化,当然这种情况我们肯定是要根据APP版本下发不同bundle的,但对于其他的第三方库我们升级时就有可能没考虑到这一点了。

    拆包重点2--过滤基础包的模块

    打业务包的时候,需要过滤掉基础包所包含的模块,关键就在于processModulesFilter这个函数,根据传入的module返回true来保留模块,或者返回false来过滤模块。前面提到processModules过滤了一些模块,我们来看下代码

    function processModules(
      modules,
      { filter = () => true, createModuleId, dev, projectRoot }
    ) {
      return [...modules]
        .filter(isJsModule)
        .filter(filter)//过滤模块
        .map((module) => [
          module,
          wrapModule(module, {
            createModuleId,
            dev,
            projectRoot,
          }),
        ]);
    }
    

    这里的第二个filter函数的参数就是processModulesFilter,我们实际项目拆包定义的processModulesFilter函数如下

    function postProcessModulesFilter(module) {
      if (platformModules === null || platformModules.length === 0) {
        console.log("请先打基础包");
        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;
        }
        const name = getModuleId(projectRootPath, path, entry, true, moduleMapDir);
        if (platformModules.indexOf(name) >= 0) {
          //这个模块在基础包已打好,过滤
          return false;
        }
      }
      return true;
    }
    

    到这里,RN的拆包基本上就完成了,剩下的主要是划分各个业务包的起始moduleId,避免业务包的moduleId冲突,逻辑比较简单,就不详细介绍了。

    相关文章

      网友评论

          本文标题:metro源码分析之RN拆包

          本文链接:https://www.haomeiwen.com/subject/wferfdtx.html