什么是JSBundle
JSBundle 是 JS代码打包后的产物,在React-native里面主要是通过react-native-cli提供的命令进行打包。跟网页类似,一个RN项目除了代码还会有资源文件,比如本地图片、JSON等,这些都会放到跟JSbundle同级的assets目录下。这些文件可以内置在原生的工程里,在原生工程启动时通过RN官方提供的方法加载JSbundle,并放到JS引擎中执行。也可以打成压缩包之后通过网络下载(也就是热更新)后再在本地执行。
react-native-cli
React-native官方提供的命令行工具,里面包含了拉取react-native模版工程(init命令)、诊断运行环境(doctor命令)、打包(bundle命令),以及远程调试(本地node服务)所用到的代码。默认通过npm依赖的方式集成到react-native源码中,也可以单独下载(https://github.com/react-native-community/cli#documentation),具体命令可通过官方文档或npx react-native --help 查阅
JSBundle格式
原始格式
原始格式是纯文本的JS代码,默认情况下会进行压缩和混淆
当打包时关闭掉-minify选项后,可以看到原始的代码
JSbundle里每一行都是一个module,也就是一个文件的内容,在每一行的末尾会有该module的ID和所依赖的其他module的ID
moduleId是根据编译时的顺序生成的,默认是从0开始生成,按文件依次递增。前面的内容都是框架自带的module,我们自己写的module通常在后面。
本文是根据官方提供的awesome project模版拉取的代码并生成的jsbunble,可以跟源码对比看看有什么差异。
官方为了缩减JSbundle的大小,对很多函数做了简化处理,例如declare变成了__d, require变成了__r。
Hermes二进制格式
二进制格式的本质是字节码,字节码是JS转成可执行代码的中间形式。由于JS代码在 JS引擎里面需要编译为字节码或者机器码才能执行,这一阶段比较耗时,而且每次启动都是执行,明显是属于重复工作。为了减少这个时间,官方推出了hermes二进制格式,也就是我们说的字节码,支持预编译并且可以缓存在本地,减少二次编译,甚至可以在生成JSbundle的时候就编译为二进制格式。关于字节码的详细解释,可以参考我这篇博客(https://www.jianshu.com/p/af772cc66428),这里就不详细解释了。
RAM格式
RAM也是一种二进制格式,推出的目的是为了压缩包大小,主要是将jsbundle按模块拆分为单个的文件以支持按需加载,但是由于只支持iOS,并没有真正推广起来。想了解可以看官方介绍(https://facebook.github.io/metro/docs/bundling),不推荐深入研究。
打包脚本
我们使用react-native bundle命令来打包,假设打出来的包都放在 build 这个目录下,我们需要执行以下指令:
这生成index.android.bundle和index.android.bundle.packager.map,分别是JSbundle和sourceMap文件
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./build/index.android.bundle --sourcemap-output ./build/index.android.bundle.packager.map
生成hermes二进制文件index.android.bundle.hbc及其与源码的映射文件index.android.bundle.hbc.map(主要是记录模块的VLQ编码与二进制文件中对应函数的偏移量的映射关系)。注意这里-output-source-map的值是上一步生成的JSBundle,并非我们通常所说的sourcemap文件。不同的版本hermesc的位置略有不同,可执行
./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out ./build/index.android.bundle.hbc -output-source-map ./build/index.android.bundle
或
./node_modules/react-native/sdks/hermesc/osx-bin/hermesc -emit-binary -out ./build/index.android.bundle.hbc -output-source-map ./build/index.android.bundle
根据第1步生成的sourcemap以及第2步生成的映射文件生成二进制文件的sourcemap。这一步不是必须的,但是如果你想通过sentry之类的错误收集平台来找到出错的代码,并且JSbundle是使用了hermes二进制格式的,就一定要上传这个sourceMap。
./node_modules/react-native/scripts/compose-source-maps.js ./build/index.android.bundle.packager.map ./build/index.android.bundle.hbc.map -o ./build/index.android.bundle.map
metro
metro是react-native专用的打包工具,有点类似web开发里面的webpack。前面说的react-native bundle命令背后就是用的metro(可参考代码 https://github.com/react-native-community/cli/tree/main/packages/cli-plugin-metro),关于metro的使用可以参考官方文档(https://facebook.github.io/metro/docs/concepts)
metro 大致可以分为resolver、transformer和Serialization三个阶段,分别是解析源码生成module的依赖图、转换module的格式已被目标平台所理解以及序列化生成最终产物,三个阶段官方有提供默认的实现(比如transformer是使用了babel),也提供了配置来替换一些关键函数。metro内部会根据依赖图的变化计算新增、修改和删除的模块,并且通过缓存transformer的结果来提升debug时的热更新效率。关于metro有很多博客介绍,可以参考https://www.jianshu.com/p/5730da61132f。
我们做拆包,主要是针对Serialization这个阶段做修改,修改的函数包括createModuleIdFactory(自定义模块ID的生成规则,确保唯一即可)、processModuleFilter(根据module信息判断是否已经处理过,打业务包需要)
hermesc
这是hermes的一个命令行工具,封装了hemres用到的常用函数,其中-emit-binary 功能是根据传入的路径找到jsbundle,加载内容,然后一行一行的解析,将JS编译为字节码,同时生成映射文件。这个工具也支持dump字节码、AST、IR以及解析JSX、ts等功能,可以说是非常全面了。可以输入./node_modules/react-native/sdks/hermesc/osx-bin/hermesc --help
查看所有的命令