RN JSBundle 拆分解决方案(2): JSBundle 、Metro 结构分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013718120/article/details/84571326

在第一篇(RN JSBundle 拆分解决方案(1): 应用启动、视图加载原理解析)中我们对RN的启动、视图加载流程从源码角度做了简单的梳理。本篇内容我们继续对JSBundle 文件内容结构,以及当前RN框架默认的打包工具 Metro 进行分析。

JSBundle 文件结构

当我们执行 react-native bundle | unbundle 命令时,RN框架会按照当前给定的参数,打包出特定平台下的 bundle 文件。在React Native 实现热部署、差异化增量热更新文章中,对于打包做了比较详细的介绍,例如执行如下打包命令:

react-native bundle --entry-file index.js --bundle-output ./bundle/index.android.bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false

上述命令是以 index.js 文件为入口,打出 Android 平台下的bundle文件,将bundle文件和资源数据写入到 bundle 目录下,并关闭开发模式。在源码分析一节中,我们知道开发模式关系到了bundle的加载方式。0.5x 版本下,RN框架借助 Metro-Bundler 完成对bundle文件的打包流程。其实当我们执行 npm start 开启本地服务,并运行程序时,就可以发现 Metro-Bundler 已经执行并开始从本地 Server 加载 bundle文件了:

在打包完成后,可以看到系统为我们生成了两个文件:

index.android.bundle

index.android.bundle.meta

.bundle后缀的文件即是 Android 平台下可以加载的 JSBundle 文件。.meta文件存储了关于 bundle 的一些元数据信息。关于meta 此处不再过多赘述,我们主要来看 .bundle 文件:

⚠️ 由于 js 代码在开发模式会被混淆,我们可以打 release 包(--dev指定为false)来查看被混淆之前的代码

扫描二维码关注公众号,回复: 4656038 查看本文章
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 定义的代码块 并执行

总结以上,bundle 文件大致包含三部分:

1. polyfills:最先执行的一些 function,ES6特性支持,定义模块声明方法等
2. modules:模块声明,以 __d开头,定义各式各样的module,其中包括了 react-native 的module(StatusBar、View、Text ...),引入的第三方 module 等
3. require:执行 InitializeCore 和 Entry File,最后一行执行 require(0)

⚠️ __d 的执行对应于 nodule_modules / metro / lib / polyfills / require.js 文件中的 define 方法

⚠️ require 的执行对应于 nodule_modules / metro / lib / polyfills / require.js 文件中的 metroRequire 方法

JSBundle 文件我们了解以上的内容就已经足够了,接下来看看 Metro 是什么。

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 帮助我们完成了全部工作,解析加载的过程如下:

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

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

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

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

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

(1)Configuring Metro  

(2)Role of Metro Bundler in React native

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

createModuleIdFactory () => (path: string) => number Used to generate the module id for require statements.
processModuleFilter (module: Array<Module>) => boolean A filter function to discard specific modules from the output.

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 负责过滤掉基础包的内容模块。

总结

以上我们介绍了如何使用 Metro 的 config 配置来帮助我们实现基础包的require固定,以及过滤出业务包的实现流程。由于该方案在Windows平台上某些情况下会导致不能打包成功,出现不明确的错误。所以我们可以结合使用第二种方案:修改 Metro-Bundler 打包源码逻辑,固定基础包模块ID,使用 diff-patch 的方式打出差异包,分模块进行加载。下一章节《RN JSBundle 拆分解决方案(3): 实现 JSBundle 文件按需加载》我们会从代码角度分别来描述如何实现。

猜你喜欢

转载自blog.csdn.net/u013718120/article/details/84571326
RN