美文网首页Hybrid开发ReactNative
RN拆包解决方案(一) bundle拆分

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

作者: gy先生 | 来源:发表于2019-12-29 13:50 被阅读0次

    前言

    本文是基于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拆分

        本文链接:https://www.haomeiwen.com/subject/hjuyoctx.html