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<string>,
): Promise<$ReadOnlyArray<AssetData>> {
  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 的修改。所以这里基于官方 metro@0.30.2 版本 fork 了一份,只需要 Metro 内的代码,其他的依赖依然和官方同步。local-cli 的部分完全不动,重新实现了一个 cli 的部分,调用 metro 构建,并最后生成 zip 文件。

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

预加载

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

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 在自己上手实现拆包工具之前,我们也调研了其他家关于这方面的解决方案。 因为这种操作并没有官方的解决方案,又和各家的...
    砌墙的民工阅读 3,328评论 0 0
  • 一. 拆包动机 RN作为非常优秀的移动端跨平台开发框架,在近几年得到众多开发者的认可。国内各大厂采用在当前原生应用...
    LaxusJ阅读 8,783评论 0 55
  • 本文主要介绍 bundle 命令的执行过程,以及 Facebook 专门为 react-native 开发的打包工...
    砌墙的民工阅读 5,663评论 0 1
  • 引言 React Native以其独到的特性,吸引着互联网公司纷纷为之投入或多或少的人力。在实际的开发过程中,开发...
    Jason景阅读 13,748评论 4 25
  • 昨天下午,妈妈教我拌凉菜。我把妈妈切好的黄瓜丝放在盘里,再按妈妈的方法,放上盐,味精、醋和香菜,然后用筷...
    宋彤彤A阅读 1,765评论 0 0

友情链接更多精彩内容