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。具体的原理后面启动原理篇再详细介绍。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容