RN拆包解析

作者: LaxusJ | 来源:发表于2019-01-25 18:57 被阅读2次

    一. 拆包动机

    RN作为非常优秀的移动端跨平台开发框架,在近几年得到众多开发者的认可。国内各大厂采用在当前原生应用内集成RN的方式,使得App应用的灵活性得到了很大的提升。在原生应用内嵌入RN,就是需要在原生应用内加载RN模块(1个或多个JSBundle),并得以显示。JSBundle中包含了当前RN模块的js代码。如果存在多个RN模块需要被加载时,就需要分别打出多个JSBundle,并且多个JSBundle包含了很多重复的代码(例如:第三方依赖)。拆包的方式,就是将其中重复不变的代码打成基础包,动态变化的打成业务包。那么就做到了JSBundle的拆分。JSBundle的拆分,对降低内存的占用,减少加载时间,减少热更新时流量带宽等,在优化方面起到了非常大的作用。

    二.bundle简要分析

    1.bundle命令

    • entry-file:即入口文件,打包时以该文件作为入口,一步步进行模块分析处理。
    • platform:用于区分打包什么平台的 bundle
    • dev:用于区分 bundle 使用环境,非 dev 时,会对代码进行 minified
    • bundle-output:打包产物输出地址,即打包好的 bundle 存放地址
    • sourcemap-output:打包时生成对应的 sourcemap 文件存放地址,在跟踪查找错误或崩溃时,能帮助开发快速定位到代码
    • assets-dest:bundle 中使用的静态资源文件存放地址

    1.结构分析

    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, 
     
        r.__d = function(r,i,n) {
            if(null != e[i]) 
                return;
            e[i] = {
                dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}
            }
        },
     
        r.__c = n;
     
       .... 代码省略
       
    })();
     
     
    __d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1]),o=n(r(d[2])),u=r(d[3]);t.AppRegistry.registerComponent(u.name,function(){return o.default})},0,[1,2,328,330]);
     
    ....省略其他 __d 代码
     
    __d(function(g,r,i,a,m,e,d){m.exports=function(t){if(t&&t.__esModule)return t;var o={};if(null!=t)for(var n in t)if(Object.prototype.hasOwnProperty.call(t,n)){var c=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(t,n):{};c.get||c.set?Object.defineProperty(o,n,c):o[n]=t[n]}return o.default=t,o}},329,[]);
     
    __d(function(e,s,t,a,n,N,d){n.exports={name:"RNTest",displayName:"RNTest"}},330,[]);
     
     
     
    __r(79);
    __r(0);
    

    以最基础的RN项目的 bundle 为例,可以看到 bundle 文件中大致定义了四个模块:

    (1)var 声明的变量,对当前运行环境的定义,bundle 的启动时间、Process进程环境相关信息

    (2)(function() { })() 闭包中定义的代码块,其中定义了对 define(__d)、 require(__r)、clear(__c) 的支持,以及 module(react-native及第三方dependences依赖的module) 的加载逻辑

    (3)__d 定义的代码块,包括RN框架源码 js 部分、自定义js代码部分、图片资源信息,供 require 引入使用

    (4)__r 定义的代码块,找到 __d 定义的代码块 并执行

    最终归纳出以下结构


    image

    polyfills : 预加载,最早执行的一些function,声明es语法新增的接口,定义模块声明方法等
    module difinitations : 模块声明,以__d开头,一般为每一个js文件或资源文件,将其封装成一个module对象,并进行标号
    require calls : bundle文件尾部指定入口文件,如如require(79),最后一行require(0);

    ps:79可以找到是InitializeCore,这个加载了js-c++-java三层的通信注册类,通信临听类等

    三.拆包方案

    其他方案对比

    1. moles-packer

    简介:携程大厂推出,稳定可靠,针对react native0.44时代的版本

    优点:重写了react native自带的打包工具,重写就是为了分包,为分包而生的项目,肯定可靠

    缺点:不持续维护更新,只适合rn老版本用户了,0.5以上的rn版本全部扑街

    1. 自己修改打包代码

    简介:现在很多教程都是让你去修改打包的源码,在里面判断分包,58的0.44版本就是这个方案

    优点:如果很懂打包源码,这个做法灵活,定制化强,100%没问题

    缺点:上手难,需要完全理解打包源码,网上的教程比较古老

    1. diff patch

    简介:大致的做法就是先打个正常的完整的jsbundle,然后再打个只包含了基础引用(react和第三方module)的基础包,比对一下patch,得出业务包,这样基础包和业务包都有了

    优点:简单暴力,如果只是想简单做下分包的可以尝试下

    缺点:1、不利于维护,由于module后面都是rn生成数字,依赖变了数字也变,导致基础包变了所有包都需要变2、图片没法分包,有的第三方库是有图片的,这个方法只处理jsbundle不处理图片

    Metro

    在执行 react-native bundle | unbundle 命令时,RN框架背后其实是依赖了 Metro-Bundler 来完成打包、加载任务。Metro 作为一个独立的打包工具,官方文档 对于它的定义如下:

    The JavaScript bundler for React Native.
    Fast:Metro aims for sub-second reload cycles, fast startup and quick bundling speeds.
    快:Metro旨在实现亚秒级重载循环,快速启动和快速捆绑速度。
    Scalable:Works with thousands of modules in a single application.
    可扩展:在单个应用程序中使用数千个模块。
    Integrated:Supports every React Native project out of the box.
    集成:支持开箱即用的每个React Native项目。

    Metro 的高度可扩展性,为我们提供了自由配置的打包方式。我们可以根据实际的需要来控制打包过程中的一些需求。官方为我们提供了很多种可配置的方式,可以使用以下三种方式创建Metro配置(按优先级排序):

    metro.config.js
    metro.config.json
    package.json中的 metro 字段
    还可以通过在调用 CLI 时指定 --config <path / to / config> 来为配置提供自定义文件。

    Metro中的常见配置结构如下所示:

    module.exports = { 
        resolver: { 
            /* resolver options */
        }, 
        transformer: { 
            /* transformer options */ 
        }, 
        serializer: { 
            /* serializer options */ 
        }, 
        server: {
            /* server options */
        }
        /* general options */ 
    };
    

    在打包过程中,Metro-Bundler 帮助我们完成了全部工作,解析加载的过程如下:


    image

    项目中,入口点文件(如 index.js)利用 import 依赖了其他组件。即组件间都是相互依赖的。

    Resolution 代表 解析 的过程,负责梳理关联js文件间的相互依赖关系。

    Transformation 代表 转换 的过程,负责将模块文件转换成平台可理解的格式。

    Serialization 代表 序列化 的过程,负责在完成转换过程并将模块转换为可访问的格式后,将其序列化。序列化程序将模块组合在一起以生成一个或多个包。捆绑包实际上是一组模块,组合成一个JavaScript文件。

    更多关于配置的详细信息可以查看(和谐翻墙):

    (1) Configuring Metro

    (2)Role of Metro Bundler in React native

    核心修改项

    拆包的核心思想就是将基础包和业务包拆分。那么我们只需要使用如下两个配置项即可:

    createModuleIdFactory
    用于生成 require 语句的模块ID,配置 createModuleIdFactory 让其每次打包的 module 使用固定的id(路径相关)。
    参数是要打包的 module 文件的绝对路径,返回的是打包后的 module 的 id

    processModuleFilter
    起到过滤功能,用于从输出中丢弃特定模块。配置 processModuleFilter 过滤基础包,打出对应业务包。
    参数是 Module 信息,返回值是 boolean 类型 ,如果是 false 就过滤掉不进行打包

    Metro Config 配置文件

    在打包过程中,我们需要依赖 createModuleIdFactory 、processModuleFilter 来帮助我们将JSBundle拆分为基础包和业务模块包。拆分的过程就需要我们通过配置 config 文件来完成。接下来我们来看看如何编写 config 配置文件。

    在编写 config 配置文件之前,先来想个问题,为什么要固定基础包中的模块ID( __r(id) )呢?

    在上面我们贴出的bundle文件中,可以看到最底部有两段代码:

    __r(79);
    __r(0);
    

    不同文件打出的 bundle,最底部都为__r(0); 而上面的会随着顺序依次增加,例如以 index.js 文件打出的 bundle id 为 79,以 CustomComponent.js 打出的为 80。

    基础包(common.bundle)
    在打基础包的时候,我们会把RN的基础文件以及第三方的依赖打进去。当我们在打业务包的时候,可能会做修改,例如导入组件的顺序发生变化,或者依赖版本做了更新等等。都有可能导致ID发生变化,造成基础包中不能找到对应的模块ID,导致基础包失效。所以需要将ID固定。一种简单的方式就是以模块名称作为 require 即可。所以配置 createModuleIdFactory 让其每次打包的 module 使用固定的模块名称即可。

    业务包 (bussiness.bundle)
    在打业务包时,需要结合 createModuleIdFactory、processModuleFilter 同时进行。createModuleIdFactory负责固定 module 的ID。processModuleFilter 负责过滤掉基础包的内容模块。

    createModuleIdFactory 源代码

    //node_modules/metro/src/lib/createModuleIdFactory.js 
    "use strict";
    
    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;
      };
    }
    
    module.exports = createModuleIdFactory;
    

    我们知道,createModuleIdFactory 用于生成 require 语句的模块ID,从上述源码也可以看出,系统使用整数型的方式,从0开始遍历所有模块,并依次使 Id 增加 1。所以我们可以修改此处逻辑,以模块路径名称的方式作为Id即可。

    参考文档

    https://blog.csdn.net/u013718120/article/details/84571326

    相关文章

      网友评论

        本文标题:RN拆包解析

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