React Native Metro 学习笔记 1 -- local-cli

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
    • react-native:react native 框架
      • local-cli:本地命令行工具,与开发者交互的入口
    • metro*:Metro 打包器,内部依赖

react-native-cli

上一节中提到的 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

通过第一节的分析我们发现,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

需要补充的文件包括:

  • ../setupBabel
  • ./../rn-get-polyfills
  • ../../jest/hasteImpl

至此,再运行 ./local-cli/cli.js 已经能够跑起来了。

local-cli 命令行选项

在本节中梳理 lcoal-cli 所接收的命令行选项。命令的格式如下:

Usage: cli [options] [command]

其中,有两个通用选项:

  • -V(--version):版本
  • -h(--help):帮助

下面来梳理 command:

start

启动 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

run-ios

构建 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]端口号

run-android

构建 app 并在 Android 模拟器、设备中运行。对应于 runAndroid/runAndroid.js。

参数:

名称说明
--install-debugdebug 包
--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 路径

new-library

创建一个 native library bridge。对应于 library/library.js。

参数:

名称说明
--name [string]库名称

bundle

构建离线包。对应于 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读取全局缓存

unbundle

已废弃,改名为 ram-bundle。

ram-bundle

builds javascript as a "Random Access Module" bundle for offline use。

参数:

名称说明
--indexed-ram-bundleForce the "Indexed RAM" bundle file format, even when building for android

eject

主要是针对使用 create-react-native-app 创建的 expo 项目,用来删除 expo,重新创建 android、ios 目录。

参数:

名称说明
eject重新生成 Android 和 iOS 的原生工程

link/unlink

链接原生依赖。link/link.js,格式:

link [options] [packageName]

取消原生依赖链接。link/unlink.js,格式:

unlink [options] <packageName>

install/uninstall

安装、删除原生依赖。install/install.js,install/uninstall.js,格式:

uninstall [options] <packageName>

upgrade

升级项目中 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

打印环境信息,格式:

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 是如何解析依赖的,它有几个路径,各有什么作用。

这个问题留给下一篇博客中继续解决。

总结

最后,我们总结下这一篇的内容:

  1. 梳理了 React Native、local-cli、react-native-cli、metro 之间的关系
  2. 梳理了 local-cli 的命令行参数,这是一份很好的参考资料
  3. 进行一项尝试,将 local-cli 从 React Native 剥离出来

虽然剥离的工作遇到了问题,但这是一个很好的切入点,在后面的系列文章中,我们将就着这个项目继续探索下去。并在探索过程中将 Metro 的打包机制摸清楚。