metro源码分析之RN拆包

拆包原理

由于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拆包)