2019-03-22
react-native link 是一个我们在开发中经常用到的命令,它的作用是将第三方库中的 Native Modules 引入 Native 工程。在 Android 中,引入 Native 需要修改 Gradle、Java 代码,这是如何实现的呢?
另一点值得一提的是,React Native cli 已经从官方源中迁移出来了,成为一个社区维护的项目 react-native-cli。拥抱社区是 React Native 今年的一项重点工作,未来 React Native 生态会变得更好玩。
React Native 的第三方库虽然使用 npm 分发并使用 npm/yarn 进行安装,但是对于带有 Native Modules 的模块细心的人可能会注意到,需要使用 react-native link 进行连接。
其实,React Native 是由自己的包管理器的,名为 React Native Package Manager(rnpm),link 操作是 rnpm 的一部分。
rnpm 曾经是一个独立项目,现在被整合进入了 React Native Core。
因此,想要弄清 link 的操作,我们首先要了解 React Native Package Manager。
React Native 包与普通的前端包不同之处在于,React Native 包通常还包括 Android、iOS 原生代码。如何才能将原生代码成功连接到原生工程内呢?
rnpm 就是来解决这个问题的
rnpm 对 package.json 进行了扩充。开发者在开发 rnpm 包时,在 package.json 通过添加 rnpm 字段来控制 rnpm 的行为。其中包含几个控制项。
用于添加在连接前后执行的命令:
"rnpm": { "commands": { "prelink": "./bin/requestGAToken", "postlink": "./bin/linkingSucceeded" } }
用于指定需要连接进入工程的资源目录:
... "rnpm": { "assets": ["Fonts"] }, ...
package.json 的 rnpm 字段是如何被解析的呢?
让我们回到 react-native-cli 的代码,它位于 packages/cli/src/tools/getPackageConfiguration.js:
/** * Returns configuration of the CLI from `package.json`. */ export default function getPackageConfiguration( folder: string, ): PackageConfigurationT { return require(path.join(folder, './package.json')).rnpm || {}; }
其中:
我们直接看它的返回类型 PackageConfigurationT:
/** * Configuration of the CLI as set by a package in the package.json */ export type PackageConfigurationT = { assets?: string[], commands?: {[name: string]: string}, params?: InquirerPromptT[], android: AndroidConfigParamsT, ios: IOSConfigParamsT, }; export type InquirerPromptT = any; export type AndroidConfigParamsT = {};
这与文档上的说明是一致的。
在我们深入 link.js 代码之前,我们先来看下一个包含 Native Modules 的第三方库的结构。
第一个包含 Native Modules 的第三方库包含以下几个部分:
需要注意的是,这种 React Native 第三方库使用 npm 进行分发,因此位于工程的 node_modules
目录下。
这对于习惯原生开发的同学来说是一个不同的地方。
下面我们以 react-native-intent-launcher 这个第三方库为例,它位于 node_modules/react-native-intent-launcher 下。目录结构如下:
node_modules/react-native-intent-launcher
├── IntentConstant.js
├── LICENSE
├── README.md
├── android
│ ├── android.iml
│ ├── build
│ ├── build.gradle
│ ├── gradle
│ ├── gradlew
│ ├── gradlew.bat
│ ├── local.properties
│ └── src
├── index.js
└── package.json
从中可以看出:
package.json
文件浏览这个项目后发现:
通过对这个工程的分析,我们发现它并没有对 link 进行特殊的配置。
因此,解析工程、找出 Native Modules 的任务都落在 link 命令里面了。
有了这些背景知识,下面我们进入 link 内部一探究竟。
在本文中,我们的核心问题是在 Android 中,引入 Native 需要修改 Gradle、Java 代码,这是如何实现的呢?
这个问题的答案就在 packages/cli/src/commands/link/android 这个目录下。
它提供了一系列函数,用于注册原生模块、解注册原生模块、拷贝资源等待。
下面我们逐个来看:
这个操作位于 packages/cli/src/commands/link/android/registerNativeModule.js 中:它的代码如下:
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import applyPatch from './patches/applyPatch'; import makeStringsPatch from './patches/makeStringsPatch'; import makeSettingsPatch from './patches/makeSettingsPatch'; import makeBuildPatch from './patches/makeBuildPatch'; import makeImportPatch from './patches/makeImportPatch'; import makePackagePatch from './patches/makePackagePatch'; export default function registerNativeAndroidModule( name, androidConfig, params, projectConfig, ) { // 生成导入 Java 依赖的 patch const buildPatch = makeBuildPatch(name); // applyPatch 进行正则替换 // 在 settings.gradle 中通过相对路径导入 node_modules 下的库的 android 目录 applyPatch( projectConfig.settingsGradlePath, makeSettingsPatch(name, androidConfig, projectConfig), ); // 在 app/build.gradle 下加入 Java Module 依赖 applyPatch(projectConfig.buildGradlePath, buildPatch); // 向 strings.xml 中追加 string 资源 applyPatch(projectConfig.stringsPath, makeStringsPatch(params, name)); // 向注册 MainReactPackage 的地方插入第三方库的 Module applyPatch( projectConfig.mainFilePath, makePackagePatch(androidConfig.packageInstance, params, name), ); // 向注册 MainReactPackage 的地方加上 Java 类的 import applyPatch( projectConfig.mainFilePath, makeImportPatch(androidConfig.packageImportPath), ); }
至此我们可以看出:
link 中最神秘的部分被揭开了,感觉没有想象中那么美丽……没想到是靠暴力正则替换打 patch 实现的,有些失望。但是它确实能工作,而且也工作地很稳定。
可能是 link 这个命令过于神奇(自动插入 gradle 依赖、自动插入 Java 代码),因此期望过大。也确实,这么一个神奇的功能总不能是通过人工智能来实现的吧 2333
这件事情给我的感悟是:
这不就是 KISS 思想吗?!通过这次代码分析加深了我的认识。
资源拷贝就要简单多了 packages/cli/src/commands/link/android/copyAssets.js:
/** * Copies each file from an array of assets provided to targetPath directory * * For now, the only types of files that are handled are: * - Fonts (otf, ttf) - copied to targetPath/fonts under original name */ export default function copyAssetsAndroid( files: Array<string>, project: {assetsPath: string}, ) { const assets = groupFilesByType(files); logger.debug(`Assets path: ${project.assetsPath}`); (assets.font || []).forEach(asset => { const fontsDir = path.join(project.assetsPath, 'fonts'); logger.debug(`Copying asset ${asset}`); // @todo: replace with fs.mkdirSync(path, {recursive}) + fs.copyFileSync // and get rid of fs-extra once we move to Node 10 fs.copySync(asset, path.join(fontsDir, path.basename(asset))); }); }
从中可以看出,它现在只支持对字体资源的拷贝。
link 命令的入口在 packages/cli/src/commands/link/link.js。
我们先来看这个入口对外导出了什么:
export default { func: link, description: 'scope link command to certain platforms (comma-separated)', name: 'link [packageName]', options: [ { command: '--platforms [list]', description: 'If you want to link dependencies only for specific platforms', parse: (val: string) => val.toLowerCase().split(','), }, ], };
其中:
下面我们就来详细看 link 函数。
link 函数的签名如下:
/** * Updates project and links all dependencies to it. * * @param args If optional argument [packageName] is provided, * only that package is processed. */ function link([rawPackageName]: Array<string>, ctx: ContextT, opts: FlagsType) {
其中:
rawPackageName 需要连接的模块名称,可以不填
opts 配置选项
它将 link 操作拆分为以下几个步骤:
原来还有资源文件也会一同链接进入主工程。
我们在前面已经分析过了模块解析,以及 Android Platform 工具集中的核心操作。
link 命令在这里只是进行了目录解析,和将这些过程串联起来,这些都是非核心的工作,因此不再赘述。
在本文中,我们以 react-native link 命令为起点,通过深入挖掘,了解到原来 React Native 中还有 React Native Package Manager 项目,同时也分析了 react-native link 的实现原理,打开了它的神秘面纱。