2018-11-06
Metro 是 React Native 的打包器,在公司中我们通常要在它的基础上进行二次开发,最常见的技术项目就是 React Native 分包拆包技术。对于 React Native 工程师来说,对 Metro 的深入理解是在这一领域技术成长的进阶。在这个博客系列中,我将分享我在 Metro 学习过程中的心得。本文是系列的第一篇,我们从 local-cli 这个入口展开探索之旅。
对于一个 React Native APP 工程,其内部 metro 打包器的工程结构是如何呢?
在 node_modules
下有一个 react-native Package,这个就是 React Native 框架。
在 react-native 包中,有一个 local-cli 目录,里面包含一个命令行工具,其入口为 cli.js。这个工具我们在日常开发中经常用到:它可以启动本地编译服务器,帮助我们在真机上动态调试,也可以执行构建命令,打 Bundle 包。
local-cli 本身不具备编译功能,它是对 Metro 打包器的一层封装。local-cli 主要负责解析命令行参数,生成配置文件,传递给 Metro 打包器,由后者执行实际操作。
metro 打包器也位于 node_modules
下,它由一系列包构成,以 metro* 开头的包都是。
总结一下,工程结构如下:
node_modules
上一节中提到的 local-cli,可能有的同学感觉比较陌生,在实际中如果我们要运行工程,通常会执行下面命令:
react-native run-android
其中,react-native 是全局安装的一个命令,其包名为 react-native-cli。这个链接指向它的 GitHub 工程,其代码很简单,只有一个 index.js。
其核心功能是寻找 React Native App 项目中的 local-cli,将命令行参数透传给后者。
因此,react-native 命令实际上就是在跟 local-cli 打交道了。
通过第一节的分析我们发现,local-cli 虽然在 react-native 框架内部,但是它本身是一个独立的命令行工具。
在本节中我们进行一个尝试,将 local-cli 剥离出来,放到一个单独的工程里来。
首先创建一个新的空的前端工程,将 local-cli 目录整体复制到新工程的根目录。
运行 ./local-cli/cli.js,缺什么依赖就补什么。
需要补充的第三方依赖有:
metro metro-core metro-babel-register
xmldoc node-notifier morgan
compression errorhandler serve-static
shell-quote opn inquirer
envinfo
需要补充的文件包括:
至此,再运行 ./local-cli/cli.js 已经能够跑起来了。
在本节中梳理 lcoal-cli 所接收的命令行选项。命令的格式如下:
Usage: cli [options] [command]
其中,有两个通用选项:
下面来梳理 command:
启动 Metro WebServer。对应文件为 server/server.js。
参数:
名称 | 说明 |
---|---|
--port [number] | 端口 |
--host [string] | 主机地址 |
--projectRoot [string] | 项目根目录 |
--watchFolders [list] | 额外的观察目录 |
--assetExts [list] | 额外的资源文件扩展名 |
--sourceExts [list] | 额外的源码目录 |
--platforms [list] | 额外的平台 |
--providesModuleNodeModules [list] | 指定使用 providesModule 导入依赖关系的任何 npm 包 |
--max-workers [number] | 并发线程数 |
--skipflow | 关闭 flow 检查 |
--nonPersistent | 关闭 watcher |
--transformer [string] | 指定自定义的 transformer |
--reset-cache, --resetCache | 删除缓存文件 |
--custom-log-reporter-path, --customLogReporterPath [string] | 替换 TerminalReporter 的日志记录器 |
--verbose | 唠叨模式 |
--https | 启用 https |
--key [path] | SSL key |
--cert [path] | SSL cert |
构建 app 并在 iOS 模拟器、设备中运行。对应于 runIOS/runIOS.js。
参数:
名称 | 说明 |
---|---|
--simulator "iPhone 5" | 指定模拟器版本 |
--simulator "Apple TV" | 运行 Apple TV 模拟器 |
--project-path "./app/ios" | 指定 ios 目录 |
--device "Max\'s iPhone" | 指定真机设备 |
--udid [string] | 根据 uuid 指定设备 |
--configuration [string] | 指定设置 |
--scheme [string] | 指定 Xcode scheme |
--no-packager | 不启动打包器进行构建 |
--verbose | 唠叨模式 |
--port [number] | 端口号 |
构建 app 并在 Android 模拟器、设备中运行。对应于 runAndroid/runAndroid.js。
参数:
名称 | 说明 |
---|---|
--install-debug | debug 包 |
--root [string] | 更换 android 目录 |
--flavor [string] | 指定 flavor |
--variant [string] | 指定 variant |
--appFolder [string] | 指定 android 应用代码的 app 目录 |
--appId [string] | 指定构建完成后启动的 applicationId |
--appIdSuffix [string] | 指定构建完成后启动的 applicationIdSuffix |
--main-activity [string] | 指定要启动的 MainActivity |
--deviceId [string] | 指定设备 id |
--no-packager | 不启动打包器进行构建 |
--port [number] | 端口号 |
--terminal [string] | 指定启用 metro 打包器的 Terminal 路径 |
创建一个 native library bridge。对应于 library/library.js。
参数:
名称 | 说明 |
---|---|
--name [string] | 库名称 |
构建离线包。对应于 bundle/bundle。具体命令行参数位于 bundleCommandLineArgs.js。
参数:
名称 | 说明 |
---|---|
--entry-file [path] | 入口代码 |
--platform [string] | 平台 "ios" or "android" |
--transformer [string] | 自定义 transformer |
--dev [boolean] | 是否 dev |
--minify [boolean] | 是否混淆 |
--bundle-output [string] | Bundle 输出路径 |
--bundle-encoding [string] | Bundle 编码规范 |
--max-workers [number] | 并发线程 |
--sourcemap-output [string] | sourcemap 输出路径 |
--sourcemap-sources-root [string] | sourcemap 根目录 |
--assets-dest [string] | 资源文件储存路径 |
--verbose | 唠叨模式 |
--reset-cache | 删除缓存文件 |
--read-global-cache | 读取全局缓存 |
已废弃,改名为 ram-bundle。
builds javascript as a "Random Access Module" bundle for offline use。
参数:
名称 | 说明 |
---|---|
--indexed-ram-bundle | Force the "Indexed RAM" bundle file format, even when building for android |
主要是针对使用 create-react-native-app 创建的 expo 项目,用来删除 expo,重新创建 android、ios 目录。
参数:
名称 | 说明 |
---|---|
eject | 重新生成 Android 和 iOS 的原生工程 |
链接原生依赖。link/link.js,格式:
link [options] [packageName]
取消原生依赖链接。link/unlink.js,格式:
unlink [options] <packageName>
安装、删除原生依赖。install/install.js,install/uninstall.js,格式:
uninstall [options] <packageName>
升级项目中 React Native 版本。upgrade/upgrade.js。
官方已经不建议使用这个来升级了,新的建议是使用 react-native-git-upgrade 项目:
- Run "npm install -g react-native-git-upgrade"
- Run "react-native-git-upgrade"
See https://facebook.github.io/react-native/docs/upgrading.html'
通过一下两个命令打印日志:
log-android [options]
log-ios [options]
根据入口文件打印出它的依赖。dependencies/dependencies.js。格式:
dependencies [options]
参数:
名称 | 说明 |
---|---|
--entry-file [path] | 入口文件 |
--output [path] | 依赖输出文件 |
--platform [extension] | 平台 |
--transformer [path] | 指定 transformer |
--max-workers [number] | 并发线程数 |
--dev [boolean] | 是否 dev |
--verbose | 唠叨模式 |
打印环境信息,格式:
info [options]
比如,在我的电脑上执行:
React Native Environment Info:
System:
OS: macOS High Sierra 10.13.6
CPU: x64 Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
Memory: 40.14 MB / 16.00 GB
Shell: 5.3 - /bin/zsh
Binaries:
Node: 10.5.0 - /usr/local/bin/node
Yarn: 1.7.0 - /usr/local/bin/yarn
npm: 6.1.0 - /usr/local/bin/npm
Watchman: 4.7.0 - /usr/local/bin/watchman
SDKs:
iOS SDK:
Platforms: iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0
IDEs:
Android Studio: 2.3 AI-162.4069837
Xcode: 10.0/10A255 - /usr/bin/xcodebuild
npmGlobalPackages:
react-native-cli: 2.0.1
至此,我们对 local-cli 的命令行选项全部梳理完了!可以看出内容还是很多的,也比较枯燥。
其中,有些选项我之前都不知道,学习后感到很惊喜,很有用,还有一些实验性特性值得深入发掘。
这些梳理更多的作用是作为一份参考指南,供未来用到时快速查阅。
说了这么多,下面用我们剥离出来的 local-cli 打个包看看,参考上面梳理的 bundle 命令:
node local-cli/cli.js bundle --entry-file /Users/Code/ReactNative/testmetro/index.js --platform android --bundle-output /Users/Code/ReactNative/testmetro/out/my.bundle
使用上面命令进行打包,会失败:
SHA-1 for file /Users/Code/ReactNative/testmetro/index.js is not computed
是路径除了问题。原来的 local-cli 寄生在 react-native 里面,现在我们把它搬出来了,导致打包逻辑对路径的计算逻辑(按照相对路径)乱掉了。
为了验证这个设想,我的做法是回到 React Native App 工程,到它的 react-native 里,将 local-cli 改名为 local-cli-bak,然后将我们的 local-cli 拷贝过来,在 React Native App 工程内调用打包指令。
移回去后再打包就能打包成功了(毕竟是从里面拷贝出来,又拷贝回去,有问题才怪 2333)。
总结一下:我们剥离出来的这个独立 local-cli 版本的 bundle 功能是无法正常工作的。我猜测原因是因为 local-cli 寄生在 react-native 里面,很多逻辑使用相对路径进行查找,路径不正确导致。
要解决这个问题,就需要搞明白 React Native 是如何解析依赖的,它有几个路径,各有什么作用。
这个问题留给下一篇博客中继续解决。
最后,我们总结下这一篇的内容:
虽然剥离的工作遇到了问题,但这是一个很好的切入点,在后面的系列文章中,我们将就着这个项目继续探索下去。并在探索过程中将 Metro 的打包机制摸清楚。