开发 React Native 需要知道的前端工具链
本文介绍作者开发 React Native 应用和组件库时,使用到的前端工具链,以及需要注意的地方。
Node
Node (Node.js) 是一个基于 Chrome V8 引擎的 JavaScript 运行时。
Node 可用于开发服务端应用,也可用于开发命令行应用,几乎所有的前端工具链都依赖 Node。
开发 React Native 应用,也必须安装 Node。如果你使用的是 macOS,那么可以使用 Homebrewopen in new window 安装 Node。
brew install node
React Native 工程实践open in new window也简单使用了 fs, path, process 等 Node 模块。
Npm
npmopen in new window 是 Node Package Manager,是个命令行工具。
使用 npm init
命令来初始化一个简单的 package.json 文件。执行该命令后,终端会依次询问 name, version, description 等字段。
package.json 文件中一些重要的字段,你需要理解。
{ "name": "myapp", // 理解什么是语义化版本号 "version": "1.0.0", // 如果开发的是 React Native App,设置 private 为 true。 // 避免一不小心通过 `npm publish` 把项目发布到 https://www.npmjs.com/ "private": true, // 如果开发 React Native 命令行应用,需要通过 bin 指定可执行文件的位置 // 如果全局安装,npm 会使用符号链接把可执行文件链接到 /usr/local/bin // 如果局部安装,会链接到 ./node_modules/.bin/ "bin": "bin/cli.js", // 开发 React Native 组件库时,通过 main 指定编译后的程序主入口文件 "main": "./lib/index.js", // 开发 React Native 组件库时,通过 typings 指定编译后的 Typescript 类型文件入口 "typings": "./lib/index.d.ts", // 开发 React Native 组件库时,指定组件库的入口文件,方便本项目示例程序引用 // 可以使用 npx react-native-create-lib <libname> 命令来创建一个组件库 "react-native": "src/index", // 开发 React Native 组件库时,如果含有原生代码 "nativePackage": true, // scripts 指定可运行脚本, // scripts 中的命令执行时,会将当前目录下 node_modules/.bin 的子目录临时加入到 PATH 变量中 "scripts": { // start 命令实际上是 ./node_modules/.bin/react-native start --reset-cache "start": "react-native start --reset-cache", // 运行这个命令来检查项目的 TypeScript 类型是否有问题 "tsc": "tsc --noEmit", // 每次更新 eslint 规则后,运行这个命令来检查和修复项目,使之符合新的规则 "lint": "eslint . --fix --ext .js,.jsx,.ts,.tsx", }, // 主要依赖 "dependencies": {}, // 开发依赖 "devDependencies": {} }
可以通过以下命令来全局设置 npm 镜像,解决 Great Wall 的问题。
npm config set registry https://registry.npm.taobao.org
不过我们推荐使用 .npmrc 文件来局部配置。在和 package.json 同级的目录中创建 .npmrc 文件,在其中指定 npm 镜像。
# .npmrc registry=https://registry.npm.taobao.org/
我司部署了私有的 npm 服务器,那么 npm 包如何发布到私有服务器呢?
修改 package.json,配置 publishConfig 字段,然后使用 npm publish
发布。
{ "name": "@sdcx/libname", "publishConfig": { "registry": "https://nexus.xxxxxx.com/repository/npm-packages/" } }
如何在项目中引用私有的 npm 包呢?
我司的 npm 包名,都使用 @sdcx
作为 scope,在需要引入私有 npm 包的项目的 .npmrc 文件中添加指定 scope 的 registry 即可。
# .npmrc registry=https://registry.npm.taobao.org/ @sdcx:registry=https://nexus.xxxxxx.com/repository/npm-packages
有时,我们开发 npm 包,需要在本地调试,此时可以使用 npm link
npm link
主要做了两件事:
- 为目标 npm 模块创建软链接,将其链接到全局 node 模块安装路径 /usr/local/lib/node_modules/
- 为目标 npm 模块的可执行 bin 文件创建软链接,将其链接到全局 node 命令安装路径 /usr/local/bin/
譬如,我们开发了一个普通的 npm 包 npm-lib-demo
,在 npm-lib-demo
根目录下运行 npm link
, 然后在依赖该 lib 的项目根目录下运行 npm link npm-lib-demo
,即可在该项目 node_module 中看到链接过来的模块包。
又譬如,我们开发了一个命令行工具 npm-bin-demo
,在 npm-bin-demo
根目录下运行 npm link
,即可愉快地使用 npm-bin-demo
中的命令,就像使用 npm i -g npm-bin-demo
命令全局安装了一样。
Yarn
Yarnopen in new window 也是 Node 的包管理器,在一定历史时期内,yarn 比 npm 更快更稳定,所以我们一般使用 npm 来运行 scripts, 使用 yarn 来管理依赖。
.npmrc
文件同样对 yarn 生效。
使用 yarn install
来根据 yarn.lock 文件更新依赖。
yarn install # yarn 在本地 node_modules 目录安装 package.json 里列出的所有依赖 yarn install --force # 重新拉取所有包,即使之前已经安装的
使用 yarn add
来添加依赖。
yarn add <package...> # 安装包到 dependencies 中 yarn add <package...> # [--dev/-D] 用 --dev 或 -D 安装包到 devDependencies 中
使用 yarn remove
来移除无用的依赖。
yarn remove <package...> # 移除包
使用 yarn list
来查看依赖树。
yarn list --depth 0
使用 yarn outdated
来查看可升级依赖。
使用 yarn upgrade
可以更新依赖,但最好不要使用这个命令,因为它会把所有依赖都升级,而不是只升级需要升级的依赖。如果需要升级某个依赖,可以使用 yarn upgrade <package>
。
如果涉及主版本更新,也就是上图红色部分依赖,需要使用 yarn add
来重新安装,以达到升级版本的目的。
如果希望知道为什么依赖了某个包,可以使用 yarn why
来查看。
无论何时,都可以使用 --help
参数来获得帮助
yarn --help yarn list --help
Yarn 支持选择性版本解析,它允许你通过 package.json 文件中的 resolutionsopen in new window 字段指定依赖包的版本或范围。
譬如,你可能会遇到下面这个问题
这是因为 react-native 本身就依赖了某个版本的 react,和 devDependencies
字段指定的依赖冲突了,为了解决这个冲突,在 resolutions
中指定使用的 react 版本。
"devDependencies": { "@types/react": "^17.0.15", "@types/react-native": "^0.67.0", } "resolutions": { "@types/react": "^17.0.15" },
Babel
Babelopen in new window 就是 巴别塔 ,是 JavaScript 的编译器,通过 Babel 我们可以把按最新标准编写的 JS 代码向下编译成兼容各种浏览器或 Node 的通用版本。
Babel 的配置文件长下面这个样子
// babel.config.js module.exports = { presets: ['module:metro-react-native-babel-preset'], plugins: [ 'react-native-classname-to-style', [ 'react-native-platform-specific-extensions', { extensions: ['scss', 'sass'], }, ], ], }
Babel 在转译的时候,会将源代码分成 syntax 和 built-in 两部分来处理:
- syntax:类似于
...
、?.
、let
、const
等语法 - built-in:类似于
Promise
、includes()
等新的内置类型和函数
Babel 在转译的过程中,对 syntax 的处理可能会使用到 helper 函数,对 built-in 的处理会引入 polyfill。
Babel 在每个需要使用 helper 的地方都会定义一个 helper,导致最终的构建物里有大量重复的 helper;引入 polyfill 时会直接修改全局变量及其原型,造成全局变量和原型污染。
@babel/plugin-transform-runtime
的作用是将 helper 和 polyfill 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的,这样就解决了上面的两个问题。
@babel/runtime 其实就是 helper 函数的集合
在 React Native 中,我们不需要自己导入和配置 @babel/plugin-transform-runtime
,因为 metro-react-native-babel-preset 已经帮我们做了这件事情。
preset 是 Babel 插件和配置的集合,本身也是一个 Babel 插件。
学习参考资料:
Babel —— 把 ES6 送上天的通天塔open in new window
ESLint
ESLintopen in new window 是一个插件化的 JavaScript 代码检测工具,也可以检测 JSX 和 TypeScript。
ESLint 用来帮助我们检测代码中可能存在的问题,确保代码遵循最佳实践,统一代码风格,提高代码的可读性和可维护性。
一个 ESLint 配置文件大概长下面这个样子:
// .eslintrc.js module.exports = { root: true, env: { es6: true, }, parserOptions: { sourceType: 'module', }, extends: ['plugin:prettier/recommended', 'prettier/react'], plugins: ['eslint-comments', 'react', 'react-hooks', 'react-native', 'jest'], overrides: [ { files: ['*.ts', '*.tsx'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint/eslint-plugin'], rules: {}, }, { files: ['jest/*'], env: { jest: true, }, }, ], globals: { __DEV__: true, window: false, }, rules: { 'react-native/no-inline-styles': 0, }, }
globals
声明了有哪些全局变量可用,true 表示该变量为 writeable,而 false 表示 readonly
env
是一个环境中一组全局变量的预设,因为使用 globals
来一个个声明比较麻烦
extends
就是直接使用别人已经配置好的 ESLint 规则
plugins
就是使用第三方 ESLint 插件,需要预先安装
overrides
可以针对不同类型的文件设置不同的 parser, plugins, env, rules
如果我们想把自己配置好的 ESLint 规则分享给别人,可以发布一个以 eslint-config-
为前缀的 npm 包,引用时可以省略 eslint-config-
前缀。
譬如作者发布了一个名为 @gfez/eslint-config-react-native 的 npm 包,那么首先安装这个包:
yarn add @gfez/eslint-config-react-native -D
然后在 .eslintrc.js
中可以这样引用:
module.exports = { root: true, extends: ['@gfez/react-native'], }
如果需要使用第三方 ESLint 规则,则需要安装 ESLint 插件。ESLint 的插件与扩展一样有固定的命名格式,以 eslint-plugin-
为前缀,使用的时候也可以省略这个前缀。
譬如想要使用 eslint-plugin-workspacesopen in new window 插件,来确保在 monorepo 中强制执行一致的导入。那么首先安装插件:
yarn add eslint-plugin-workspaces -D
然后在 .eslintrc.js
中引入插件,配置规则:
module.exports = { root: true, plugins: ['workspaces'], rules: { 'workspaces/no-relative-imports': 'error', 'workspaces/require-dependency': 'warn', }, }
如果希望免除检查某些文件,可以将这些文件添加到 .eslintignore
中,譬如:
# .eslintignore ios/ android/ builds/ */build/
通常会在 package.json 中配置一条 lint 命令,在日常开发或者 CI/CD 中,可以使用这个命令来检查并自动修复代码。
// package.json "scripts": { "lint": "eslint . --fix --ext .js,.jsx,.ts,.tsx" }
学习参考资料:
Prettier
Prettieropen in new window 是个代码格式化工具,我们用它来保证代码风格一致性。
Prettier 不会单独使用,通常会将它作为 ESLint 规则的一部分。首先安装一下依赖:
yarn add prettier eslint-plugin-prettier eslint-config-prettier -D
尽管 Prettier 固守自己的代码风格,但也允许我们在 .prettierrc.js
文件中进行一些配置。
// .prettierrc.js module.exports = { semi: false, trailingComma: 'all', jsxBracketSameLine: true, singleQuote: true, tabWidth: 2, arrowParens: 'avoid', }
最后,在 eslintrc.js
中使用推荐的配置:
module.exports = { root: true, extends: ['plugin:prettier/recommended'], }
作者在 ESLint 章节中曾经指出,extends
就是直接使用别人已经配置好的 ESLint 规则。
plugin:prettier/recommended
的意思是,到 eslint-plugin-prettier 插件中去找名为 recommended
的配置。查看一下 eslint-plugin-prettier 的源码,就明白了。
// eslint-plugin-prettier.js module.exports = { configs: { recommended: { extends: ["prettier"], // eslint-config-prettier plugins: ["prettier"], // eslint-plugin-prettier rules: { "prettier/prettier": "error", // 开启 eslint-plugin-prettier/prettier 规则 "arrow-body-style": "off", "prefer-arrow-callback": "off", }, }, }, rules: { prettier: { // 自定义 ESLint 规则,名为 prettier meta: {...}, create(context) { // 在实现中,动态导入了 prettier npm 包 } }, }, }
eslint-config-prettier 关闭了 ESLint 和 Prettier 冲突的规则,因为 ESLint 有部分规则也是关于代码风格的。
eslint-plugin-prettier 则定义了一条名为 prettier
的规则来启用 Prettier 确保代码风格一致。
在开发 React Native 时,我们会使用 eslint-plugin-react 来指定 React 专用的 ESLint 规则。其中有一些规则也和 Prettier 冲突,因此 eslint-config-prettier 提供了一个 React 专用的配置来移除这些冲突。
// .eslintrc.js module.exports = { root: true, extends: [ "plugin:prettier/recommended", "prettier/@typescript-eslint", "prettier/react", ], }
上面的代码,关闭了 TypeScript ("prettier/@typescript-eslint"
)、React (pretter/react
) 和 Prettier 的冲突。
通常,也会为 VS Code 安装 ESLint 和 Prettier 扩展,并添加如下设置
// settings.json { "[javascript]": { "editor.formatOnSave": false, "editor.tabSize": 2 }, "[javascriptreact]": { "editor.formatOnSave": false, "editor.tabSize": 2 }, "[typescript]": { "editor.formatOnSave": false, "editor.tabSize": 2 }, "[typescriptreact]": { "editor.formatOnSave": false, "editor.tabSize": 2 }, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ], "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } }
TypeScript
TypeScriptopen in new window 是具有类型语法的 JavaScript。
有一点需要注意的是,在 React Native App 中, TypeScript 只提供类型检查,而 Babel 负责将 TypeScript 转译成 JavaScript 。
以下是 TypeScript 在 React Native 的配置文件。可以在 TSConfig 参考open in new window 找到关于这些配置项的描述。
// tsconfig.json { "compilerOptions": { // Type Checking // 严格的类型检查 "strict": true, // Modules // 设置解析非绝对路径模块名时的基准目录 // 但由于使用 Babel 进行转译,实际上这个配置不起作用 "baseUrl": "./", // 设置程序的模块系统,通常是 es6 或者 commonjs // 但由于使用 Babel 进行转译,总是编译成 commonjs,实际上这个配置不起作用 "module": "esnext", // 指定模块解析策略 // 但由于使用 Babel 进行转译,实际上这个配置不起作用 "moduleResolution": "node", // 将模块导入映射到相对于 baseUrl 路径 // 但由于使用 Babel 进行转译,需要配合 babel-plugin-module-resolver 一起使用 "paths": {} , // 支持解析 json 文件,提供类型支持 "resolveJsonModule": true, // Emit // 禁止 tsc 编译器生成文件,因为使用 Babel 来编译 "noEmit": true, // JavaScript Support // 允许在 ts 文件中引入 js 文件。 "allowJs": true , // Interop Constraints // 不会改变代码行为,只是确保每个 ts 文件都可以单独编译,因为 Babel 一次只能在一个文件上操作 "isolatedModules": true, // 对导入 commonjs 的方式进行兼容,仅对类型检查起作用,让 TypeScript 的行为与 Babel 一致 "allowSyntheticDefaultImports": true, // 对编译后的代码做兼容性处理,支持 import React from 'react' 这种方式导入 commonjs 模块 // 但因为使用 Babel 来编译, Babel 本身对 commonjs 有类似处理,所以这个配置实际上不起作用 "esModuleInterop": true, // Language and Environment // 控制 JSX 在 JavaScript 文件中的输出方式。这只影响 .tsx 文件的 JS 文件输出 // 但由于使用 Babel 进行转译,总是编译成 react 的模式,实际上这个配置不起作用 "jsx": "react-native", // 使用最新的语法和 API,因为使用 Babel 来编译,仅影响类型检查 "target": "esnext", // 为最新的语法和 API 提供类型支持 "lib": ["esnext"], // Completeness // 不检查第三方库的类型 "skipLibCheck": true, } }
如果使用 VS Code,需要为其添加如下设置:
// settings.json { "typescript.tsdk": "./node_modules/typescript/lib", "typescript.preferences.includePackageJsonAutoImports": "off" }
由于 VS Code 自身内置了 TypeScript,为了避免版本冲突,使用 typescript.tsdk
指定了 TypeScript 的安装路径。
typescript.preferences.includePackageJsonAutoImports
设置为 off
,表示只从 node_modules 中寻找依赖,不在 package.json 中找。当你发现导入模块时,VS Code 出现了重复的提示,设置为 off
后可以解决这个问题。
react-native-cli
React Native 项目自带一个 CLI 工具open in new window,我们对这个 CLI 并不陌生,package.json 中的 scripts
字段通常会有以下配置,来执行 react-native
命令:
"scripts": { "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start --reset-cache", },
React Native CLI 有一个配置机制,允许改变它的行为并提供额外的特性。可以通过在项目的根目录创建 react-native.config.js 来配置。
运行如下命令,可以打印 CLI 使用的最终配置,以理解可配置项:
react-native config
譬如,你开发的是一个组件库,示例项目不是根目录下的 android 和 ios,那么可以通过 project 字段更改项目默认的位置:
// react-native.config.js module.exports = { project: { ios: { sourceDir: './example/ios/', }, android: { sourceDir: './example/android/', }, }, }
这样,运行 react-native run-android
命令,会在 example/android 目录下运行 Android 平台的项目。
譬如,使用自定义字体open in new window,可以通过 assets
字段设置字体资源的位置:
// react-native.config.js module.exports = { project: { ios: {}, android: {}, }, assets: ['./assets/fonts/'], }
然后运行 react-native link
命令,就会将自定义字体复制到原生项目中,并修改原生项目相应的配置。
譬如,集成 CodePushopen in new window,需要像如下配置:
// react-native.config.js module.exports = { dependencies: { 'react-native-code-push': { platforms: { android: { packageInstance: 'new CodePush(BuildConfig.CODEPUSH_KEY, getApplicationContext(), BuildConfig.DEBUG)', }, }, }, }, }
这样,生成的代码就知道如何正确地创建 CodePush 实例。
更多配置,请查看 Configurationopen in new window。