06-性能优化-拆包3-自研方案

基于前面分析, 我们要解决拆包问题的话,需要解决以下几个问题:

  1. 支持 TypeScript。
  2. common 部分的 moduleId 需要固化下来。不论怎么构建同一个文件对应的 id 是不变的。
  3. module 分流,将 module 写到不同的 jsbundle 里。
  4. 资源文件的拆分。
  5. Android & iOS 如何预加载。

备注:笔者在开发时 React-Native 版本为 0.54.0。目前最新的版本 0.57+, 一些策略需要再修改。

1. 支持 TypeScript

根据TypeScript-React-Native-Starter配置, 在工程内添加一个 rn-cli.config.js 的文件, 内容如下:

module.exports = {
   getTransformModulePath() {
     return require.resolve("react-native-typescript-transformer");
   },
   getSourceExts() {
     return ["ts", "tsx"];
   }
 };

能够编译 ts 文件是因为在 config 文件里指定了对应的 transformer. 在进入编译之前, local-cli/core/index.js 里面,会读取这个 config.js 生成 RNConfig 对象,传入到对应的指令流程内。
所以我们只需要在调用 Metro 之前拿到这个 RNConfig 对象就可以支持 ts 的编译了。

2. 固定 ModuleID

只需要固定 common 模块的 id 就可以了,业务模块之间只需要做好 id 的隔离。解决方案是给每个模块指定一个 packageId:

moduleId = packageId * 1000000 + offset 

offset 在模块内自增。

common 模块的固定: 判断如果是在生成 common 模块,生成完 id 之后将 id 写入到文件存储。下次再从文件里直接读取对应的 id。

这样的另一个好处是不改变 moduleId 的类型,所以没有兼容性问题。

function createCommonStableModuleIdFactory(projectDir, packageId, manifestFile) {
  const stableIdMap = {
    nextId: packageId * 1000000 + 1,
    modules: {},
    assets:[]
  };
  if (fs.existsSync(manifestFile)) {
    Object.assign(stableIdMap, JSON.parse(fs.readFileSync(manifestFile)));
  } else {
    fs.mkdirSync(path.dirname(manifestFile));
  }
  return () => {
    return path => {
      let id = stableIdMap.modules[relativePath(projectDir, path)];
      if (typeof id !== 'number') {
        id = stableIdMap.nextId++;
        stableIdMap.modules[relativePath(projectDir, path)] = id;
        fs.writeFileSync(manifestFile, JSON.stringify(stableIdMap));
      }
      return id;
    };
  };
}
 
function createBizModuleIdFactory(projectDir, packageId, manifestFile) {
  if (!fs.existsSync(manifestFile)) {
    throw new Error('Not found manifestFile: ' + manifestFile);
  }
  const commonModule = JSON.parse(fs.readFileSync(manifestFile));
  return () => {
    const fileToIdMap = new Map();
    let nextId = packageId * 1000000 + 1;
    return path => {
      let id = commonModule["modules"][relativePath(projectDir, path)];
      if (typeof id !== 'number') {
        id = fileToIdMap.get(path);
        if (typeof id !== 'number') {
          id = nextId++;
          fileToIdMap.set(path, id);
        }
      }
      return id;
    };
  };
}

在 Metro/Server 的构造函数中, 有一个 createModuleIdFactory 的可选参数。 在 react-native 的官方 bundle 里面,并没有传递这个参数值,使用的是 Metro 的默认实现。

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;
  };
}

所以在不改变官方 bundle 的情况下,我们可以自己构造一个 MetroServer 对象,传入自己的 createModuleIdFactory 即可。

3. Module 分流

分流只需要在业务模块构建的时候做就可以。

在构建业务模块式,我们判断如果是 common 内的 module, 则不打入 jsbundle 内。

Metro 在构建的最后一步生成 code 时,会遍历整个 module,然后使用 define 封装。所以我们可以在遍历时,加入一个 function,判断是否可以需要将 module 进行转换, function 的具体实现可以由 cli 传过来.

async function fullBundle(
  deltaBundler: DeltaBundler,
  options: BundleOptions & {
    excludeModule?: (moduelPath: string) => boolean,
  },
): Promise<{ bundle: string, numModifiedFiles: number, lastModified: Date }> {
  const { modules, numModifiedFiles, lastModified } = await _getAllModules(
    deltaBundler,
    options,
  );
  const code = modules
    .filter((m) => {
      if (options.excludeModule) {
        return !options.excludeModule(m.path);
      }
      return true;
    })
    .map(m => m.code);
  return {
    bundle: code.join('\n'),
    lastModified,
    numModifiedFiles,
  };

function 的实现:判断 module 是否属于在 manifest 内定义过。

function excludeModule(projectDir, manifest) {
    const commonModule = JSON.parse(fs.readFileSync(manifest));
    return (path: string) => {
        if (commonModule['modules'][relativePath(projectDir, path)] !== undefined) {
            return true;
        }
        return false;
    };
}

4. 资源的拆分

官方 client 在构建完 bundle 之后,会再调用 MetroServer 的 getAssets 方法,获取对应模块的资源文件。在这个方法里也加个判断:

async function getAssets(
  deltaBundler: DeltaBundler,
  options: BundleOptions,
  projectRoots: $ReadOnlyArray,
): Promise<$ReadOnlyArray> {
  const { modules } = await _getAllModules(deltaBundler, options);
  const assets = await Promise.all(
    modules.map(async module => {
      if (module.type === 'asset') {
        if (options.excludeAsset && options.excludeAsset(module.path)) {
          return null;
        }
        const localPath = toLocalPath(projectRoots, module.path);
        return getAssetData(
          module.path,
          localPath,
          options.assetPlugins,
          options.platform,
        );
      }
      return null;
    }),
  );
  return assets.filter(Boolean);
}

过滤规则:

function saveCommonAssets(projectDir, manifest) {
    if (!fs.existsSync(manifest)) {
        throw new Error('Not Found manifest.json');
    }
    const config = JSON.parse(fs.readFileSync(manifest));
    config.assets = config.assets || [];
    return (path: string) => {
        if (config.assets.indexOf(relativePath(projectDir, path)) === -1) {
            config.assets.push(relativePath(projectDir, path));
            fs.writeFileSync(manifest, JSON.stringify(config));
        }
        return false;
    };
}
function excludeBizAssets(projectDir, manifest) {
    if (!fs.existsSync(manifest)) {
        throw new Error('Not Found manifest.json');
    }
    const config = JSON.parse(fs.readFileSync(manifest));
    return (path: string) => {
        if (config.assets && config.assets.indexOf(relativePath(projectDir, path)) >= 0) {
            return true;
        }
        return false;
    };
}

工程实现

基于以上几点, 涉及到 react-native/local-cli/bundle 的修改, 也涉及到 Metro 的修改。所以这里基于官方 [email protected] 版本 fork 了一份,只需要 Metro 内的代码,其他的依赖依然和官方同步。local-cli 的部分完全不动,重新实现了一个 cli 的部分,调用 metro 构建,并最后生成 zip 文件。

备注:目前拆包工具的源码实现由于和公司内部服务有一点耦合,暂未开源。待剥离不相关部分之后,会再推到 github。

预加载

拆分之后生成一个 common.bundle 和 biz.bundle。使用时可以将 common 的部分提前 load 起来,并这这部分可以被不断复用。仅需要实际加载 biz 部分。
实现步骤参照自: ReactNativeSplit。具体的原理后面启动原理篇再详细介绍。

你可能感兴趣的:(06-性能优化-拆包3-自研方案)