RN拆包解决方案(一) bundle拆分

前言

本文是基于react-native 0.55, react 16.3.1版本展开

目的

  • 减少业务包体积(app瘦身)
  • 节省热更新流量
  • 提升模块加载速度
拆包结构

实现方式

  • 打通用包,包括react-native框架、code-push框架、常用第三方框架、rnlib代码等
  • 打全量bundle包
  • 全量包基于通用包打出差异包(业务包)
  • 通用包和业务包同时引入工程
  • 原生端预先加载通用包,当进入rn页面只加载业务包
  • code-push分别更新通用包和业务包

jsbundle解析(全量包)

var 
__DEV__ = false,
__BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now(),
process = this.process || {};
process.env=process.env || {};
process.env.NODE_ENV = "production";
!(function(r) {
"use strict";
    r.__r = o, 
   ....
})();
__d(function(r,o,t,i,n){t.exports=r.ErrorUtils},18,[]);
...
...
__d(function(c,e,t,a,n){var r=e(n[0]);r(r.S,'Object',{create:e(n[1])})},506,[431,460]);
require(46);
require(11);

大致分为四个部分

  • var 声明的变量,对当前运行环境的定义,bundle 的启动时间、Process进程环境相关信息;
  • (function() { })() 闭包中定义的代码块,其中定义了对 define(__d)、 require(__r)、clear(__c) 的支持,以及 module(react-native及第三方dependences依赖的module) 的加载逻辑;
  • __d 定义的代码块,包括RN框架源码 js 部分、自定义js代码部分、图片资源信息,供 require 引入使用;
  • require定义的代码块,找到 __d 定义的代码块并执行,其中require中的数字即为 __d定义行中最后出现的那个数字。

如果每个业务都单独打全量包,那第一、二部分和大量第三部分代码将会重复,因此我们需要提取这部分代码做为通用部分,common.jsbundle,业务包将会复用这些代码

使用metro拆包

在上文的jsbundle解析中,__d中定义的各个module后都有一个数字表示,并在最后的require方法中进行调用(如require(41)),这其中的数字就是metro项目中createModuleIdFactory方法生成的(node_modules/metro/src/lib/createModuleIdFactory.js);如果添加了module,那么ID会重新生成,如果要做一个基础包,那么公共module的ID必须是固定的,因此0.56+版本的RN可以通过此方法的接口来将module的ID固定,0.52~0.55的RN依赖的metro也用到了这个方法,只是没暴露出来,可以通过修改源码的方式来实现0.56+版本相同的效果

源码改动如下

function createModuleIdFactory() {

  if (process.env.NODE_ENV != 'production') { // debug模式
    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;
    };
  } else { // 生产打包使用具体包路径代替id,方便拆包处理
    // 定义项目根目录路径
    const projectRootPath = `${process.cwd()}`;
    // path 为模块路径名称
    return path => {
      let moduleName = '';
      if (path.indexOf('node_modules\\react-native\\Libraries\\') > 0) { 
        moduleName = path.substr(path.lastIndexOf('\\') + 1); 
      } else if (path.indexOf(projectRootPath) == 0) { 
        moduleName = path.substr(projectRootPath.length + 1); 
      } 
      moduleName = moduleName.replace('.js', ''); 
      moduleName = moduleName.replace('.png', ''); 
      moduleName = moduleName.replace('.jpg', ''); 
      moduleName = moduleName.replace(/\\/g, '_'); // 适配Windows平台路径问题 
      moduleName = moduleName.replace(/\//g, '_'); // 适配macos平台路径问题 
      return moduleName; 
    }; 
  } 
} module.exports = createModuleIdFactory;

从上述源码也可以看出,系统使用整数型的方式,从0开始遍历所有模块,并依次使 Id 增加 1。所以我们可以修改此处逻辑,以模块路径名称的方式作为Id即可。
注意:这里我加了一个process.env.NODE_ENV的判断,即在开发模式下依旧使用原模式(因为我发现全局改的话开发模式会运行失败)

基础包和业务包打包

打包之前,需要我们分别定义好基础模块与业务模块文件,核心代码如下:

// base.js
require('react-native');
require('react');
require('@bwt/bwt-navigation');
require('./rnlib/RNKit');
require('./rnlib/UIKit');

import CodePush from "@bwt/bwt-code-push";
...//这里可以引入更多的第三方模块及自己的公共模块
// index_{{BundleName}}.js
import App_{{ModuleName}} from './App_{{ModuleName}}';

AppRegistry.registerComponent("{{ModuleName}}", () => App_{{ModuleName}});

注:base.js为基础模块入口,index_{{BundleName}}.js为业务业务模块入口

接下来就是通过react-native bundle命令来进行打包了,需要两个不同的命令,区别在于打包入口文件参数(–entry-file)不一样:

base.js入口打包

react-native bundle --platform ios --dev false --entry-file base.js --bundle-output $projectPath/ios/bundle/common/common.jsbundle --assets-dest $projectPath/ios/bundle/common/ --dev false

输出common.jsbundle

index_{{BundleName}}.js入口打包

react-native bundle --platform ios --dev false --entry-file index_{{BundleName}}.js --bundle-output $projectPath/ios/bundle/business.jsbundle --assets-dest $projectPath/ios/bundle/$jsbundleName/ --dev false

输出business.jsbundle

差异包打包

business.jsbundle基于common.jsbundle打差异包,实现思路:

  • business.jsbundle逐行扫描
  • 扫描内容如在common.jsbundle中没找到,用数组存放
  • 将数组转换为数据保存到差异包patch.jsbundle

结论:求出两个文件的差集,且只包含business.jsbundle的代码

//$1:common.jsbundle $2:business.jsbundle $3:patch.jsbundle
sort $2 $1 $1 | uniq -u > $3 #回写

输出patch.jsbundle作为最终业务包,引入到工程中

总结

我们利用bundle的结构拆分出common.jsbundle和patch.jsbundle,common.jsbundle只需要引入到工程一次,就可以被复用;当原生运行时加载模块的时候有几种方案:

  • 进入页面时动态合并成全量包后显示
  • 预加载common.jsbundle,进入页面时只加载patch.jsbundle后显示,RN加载代码容器使用单例
  • 预加载common.jsbundle,进入页面时加载patch.jsbundle,同时用另一个代码容器预加载common.jsbundle,当下次进入页面时使用;此方法使代码容器生命周期各自独立

本文讲述的是bundle文件拆包方案,那么原生端应该如何管理呢?且看下篇,原生多bundle加载方案,RN拆包解决方案(二) bundle加载

你可能感兴趣的:(RN拆包解决方案(一) bundle拆分)