前端工程化实践 - 代码规范 & 提交规范 & 构建流程 & Monorepo
前言
本篇文章将从 仓库策略
、 依赖管理
、代码规范
、提交规范
、任务流程
五个角度向读者介绍前端项目的一些工程化技术以及如何使你的 React Native
项目兼容 Monorepo
策略,同时你将了解到 lerna
、yarn workspace
、husky
、lint-staged
、eslint
、prettier
、commitlint
、commitizen
、webpack
等技术在项目中是如何工作的。
仓库策略
Multirepo
什么是 Multirepo?
在传统的单仓库管理模式中,所有的代码都被存储在一个大型代码仓库中。然而,在多仓库管理模式中,不同的代码库可以独立地进行版本控制和更改管理。这意味着每个代码库都可以拥有自己的开发流程和版本控制策略。
Multirepo 的优点
-
更好的可扩展性
多仓库管理使得团队可以更容易地扩展和修改项目的不同部分,而无需影响整个代码库。 -
更好的可维护性
在多仓库管理模式下,每个代码库都可以拥有自己的开发流程和版本控制策略,这使得团队可以更容易地维护项目的不同部分。 -
更好的可测试性
多仓库管理使得团队可以更容易地对项目的不同部分进行测试,而无需运行整个代码库。
Multirepo 的缺点
-
管理复杂度增加
多仓库管理模式需要更多的管理和协调工作,因为不同的代码库需要独立地进行版本控制和更改管理。 -
集成问题
在多仓库管理模式下,不同的代码库之间可能存在集成问题,这可能需要额外的努力来解决。
Monorepo
什么是 Monorepo?
Monorepo
是一种软件开发的方法,它将所有相关代码存储在一个单一的代码库中。这个代码库可以包含多个项目
、库
和服务
。使用 Monorepo
可以更轻松地管理代码和依赖项,减少重复代码,提高开发效率。
Monorepo 的优点
与单独的代码库相比,Monorepo
有许多潜在的优势:
-
代码共享
在Monorepo
中,多个项目可以共享代码。这可以减少代码重复,提高代码质量,并使开发更加高效。开发人员可以更轻松地重用现有的代码,而无需复制和粘贴。 -
更容易维护
由于所有代码都在一个代码库中,因此更容易维护。开发人员可以更轻松地找到他们需要的代码,并且不需要在多个代码库之间切换。 -
更容易协作
Monorepo
使团队成员之间的协作更容易。由于所有代码都在一个代码库中,因此团队成员可以更轻松地共享代码并进行代码评审。这可以提高代码质量并促进知识共享。 -
更容易管理依赖项
在Monorepo
中,多个项目可以共享同一组依赖项。这可以减少版本冲突,并使依赖项管理更加容易。开发人员可以更轻松地更新依赖项并确保所有项目都使用相同的版本。
Monorepo 的缺点和限制
-
复杂性增加
Monorepo
增加了代码库的复杂性,因为它需要管理多个项目和依赖项之间的交互。这会导致开发人员需要花费更多的时间来理解整个代码库以及如何在不同项目之间共享代码。 -
构建时间增加
Monorepo
中有多个项目,因此构建时间会变长,尤其是在代码库变得越来越大的情况下。这可能会导致开发流程变慢,因为开发人员需要等待更长时间来构建和测试他们的代码。 -
依赖管理困难
在Monorepo
中,多个项目共享同一组依赖项。这可能会导致版本冲突和依赖项管理困难的问题。如果依赖项出现问题,可能需要花费更多的时间来解决它们。 -
部署困难
在Monorepo
中,多个项目共享同一组代码。这可能会导致部署困难的问题,因为必须确保所有项目都能正确地部署并与其他项目一起工作。 -
版本控制问题
Monorepo
可能会导致版本控制问题,因为多个项目共享同一组代码。如果没有正确地管理版本控制,可能会导致代码库中的代码出现问题,从而影响开发流程和产品质量。
依赖管理
Yarn workspace - 高效管理工作区依赖
简介
yarn workspace
是 yarn
提供的一种管理 Monorepo
的方式。它允许将多个相关项目组织在一个代码库中,并且可以通过一个单独的 yarn.lock
文件来管理它们的依赖项。workspace
可以帮助简化 Monorepo
的管理,提高代码重用性和开发效率。
在一个 workspace
中,每个项目都是一个独立的子目录,并且可以有自己的 package.json
文件。这些子目录可以通过 yarn workspace
命令来管理,例如安装依赖、运行脚本等。
使用 yarn workspace
可以帮助开发人员更轻松地管理 Monorepo
,减少代码重复,提高代码质量,并促进团队协作。
如何使用
- 在主仓库的
package.json
文件中添加"private": true
属性。
这可以防止在发布代码时意外发布
workspace
中的项目。
-
在
package.json
文件中添加一个workspaces
属性,并将其设置为一个数组,其中包含workspace
中的所有项目子目录的路径。例如:{ "private": true, "workspaces": [ "packages/*" ] }
-
在每个项目子目录下,创建一个独立的
package.json
文件,并在其中定义项目的依赖项和脚本。 -
在代码库的根目录下运行
yarn install
命令,以安装所有workspace
中的依赖项。yarn
会在根目录下生成一个单独的yarn.lock
文件,用于管理workspace
中所有项目的依赖项。
所有依赖都需要提升到根仓库吗?
当使用 yarn workspace
管理 Monorepo
时,如果多个项目之间共享依赖项,可能会出现版本冲突的问题。默认情况下,yarn
会将依赖项安装在 workspace
的根目录下,并将它们共享给所有项目,这可能会导致版本冲突和其他问题。
为了解决这个问题,可以使用 nohoist
配置选项,将指定的依赖项安装在每个项目的本地 node_modules
目录中,这样每个项目都有自己的依赖项,并且不会与其他项目发生冲突。
在 package.json
文件中,可以通过添加一个 nohoist
属性来配置 nohoist
。例如:
{
"private": true,
"workspaces": {
"packages": ["packages/*"],
"nohoist": [
"**/react",
"**/react-dom"
],
},
}
在上面的例子中,nohoist
配置指定了 react
和 react-dom
两个依赖项不应该被提升到 workspace
的根目录下。这些依赖项将在每个项目的本地 node_modules
目录中安装和管理。
基础命令
使用 yarn workspace
命令来管理 workspace
中的项目,例如:
yarn workspace module-a add axios
:向module-a
项目添加axios
依赖项。yarn workspace module-a run build
:在module-a
项目中运行build
脚本。yarn add lint-staged -D -W
: 在根仓库中添加lint-staged
开发依赖。
-W: --ignore-workspace-root-check ,允许依赖被安装在workspace的根目录
Lerna - 简化多包管理过程
Lerna 是什么?
Lerna
是一个快速、现代的构建系统,用于管理和发布来自同一存储库的多个 JavaScript/TypeScript
包。它允许你将多个软件包存储在一个存储库中,并在这些软件包之间共享代码和依赖项。
Lerna 主要做什么?
Lerna
主要用于简化管理多个包的过程。它可以自动化许多常见的任务,例如:安装依赖项、构建、测试和发布。此外,Lerna
还提供了一些方便的命令,用于管理多个包之间的依赖关系。
Lerna 能解决了什么问题?
当你需要同时维护多个软件包时,使用 Lerna
可以显著提高开发效率。通过将所有软件包存储在一个存储库中,你可以更轻松地共享代码和依赖项,并更好地管理版本控制。此外,Lerna
还可以自动化许多常见的任务,从而减少了手动操作的时间和错误率。
Lerna 的版本号管理策略
Lerna
具有两种版本号管理策略:固定模式、独立模式。
① 固定模式(默认)
在固定模式下,所有 package
共享一个版本号。Lerna
会自动将所有 package
的版本号更新为相同的版本号。这种模式适用于想要将所有 package
一起发布的情况。
② 独立模式
在独立模式下,每个 package
都有自己的版本号。Lerna
会提示您为每个 package
输入新的版本号。这种模式适用于希望单独发布每个 package
的情况。每次发布时,你都会收到针对每个已更改包的提示,以指定它是补丁、次要、主要还是自定义更改。
设置方法:
- 在
Lerna
的配置文件中设置默认的版本号管理模式。{ "version": "independent" }
- 此外,可以在运行初始化 Lerna 时使用
--independent
标志来强制使用独立模式。lerna init --independent
Lerna 的常用操作
-
lerna init
:初始化一个新的 Lerna 项目。查看文档--independent 使用独立模式管理版本号
--exact 默认情况下,lerna init
在添加或更新本地版本的lerna
时将在devDependencies
中添加Lerna
,使后续使用时保持相似的行为。 -
lerna create <name>
:创建一个新的package
。查看文档# lerna create <name> [location] # 执行 lerna init 后,默认的 lerna workspace 是 packages/*。在 packages 文件夹中创建 package1 lerna create package1 # 在 packages/pwd1 目录下,生成 package2 依赖包 lerna create package2 packages/pwd1
-
lerna add <package> [--scope <glob>] [--dev]
:将一个包添加到 package 的依赖项中。查看文档# 如果添加本地包,lerna 会自动 link 到本地包 lerna add package lerna add package --dev # 作为 devDependencies lerna add package --peer # 作为 peerDependencies lerna add package[@version] --exact # 安装准确版本的依赖 lerna add module-1 --scope=module-2 # 将 module-1 添加为 module-2 的依赖 lerna add package packages/abcd-* # 给前缀为 abcd 的包,安装依赖
--scope <glob> 在与给定的
Glob
匹配的目录中安装依赖 -
lerna bootstrap
:安装所有 package 的依赖项,并将它们链接在一起。查看文档默认情况下,
Lerna
将尝试重用你选择的包管理器的workspace
配置。你可以使用packages
属性指定 Packages 的位置,它会告诉Lerna
在哪里寻找package.json
文件。// lerna.json { "packages": ["packages/*"] }
同时,你可以通过
lerna.json
的useWorkspaces
属性决定是否使用yarn workspace
功能。如果useWorkspaces: true
,lerna
将会继承package.json -> workspaces
的配置。// lerna.json { "useWorkspaces": true, "npmClient": "yarn" }
-
lerna run <script> [--scope <glob>]
:在 package 中运行一个 npm script。查看文档# 这个操作会执行所有包的 build 脚本 (packages.json -> scripts -> build) # 没有 build 脚本的包将不会被执行 lerna run build # 你也可以选择仅执行 module-1 中的 build 脚本 lerna run build --scope=module-1
-
lerna exec <command> [--scope <glob>]
:在 package 中运行一个 shell 命令。查看文档lerna exec -- <command> [..args] # 在所有包中运行命令 lerna exec -- rm -rf ./node_modules lerna exec -- protractor conf.js
-
lerna changed
:列出由上次发布以来发生更改的 package。查看文档lerna changed # package-1 # package-2
-
lerna version
:更新 package 的版本号,并将它们提交到 Git。查看文档运行时,此命令执行以下操作:
① 检查git
工作区是否干净,如果有未提交的修改,会提示用户先提交修改或者stash
修改。
② 检查当前分支是否为git
主分支(通常是master
或main
分支),如果不是,会提示用户切换到主分支。
③ 提示用户输入新版本号,或者根据用户指定的版本升级方式(如major
、minor
、patch
等)自动生成新版本号。
④ 对每个包进行版本升级和打标签,并将修改提交到git
仓库。
⑤ 根据用户指定的git remote
和tag
格式,将修改推送到远程git
仓库。 -
lerna publish
:发布所有更新的 package。查看文档运行时,此命令执行以下操作:
① 检查git
工作区是否干净,如果有未提交的修改,会提示用户先提交修改或者stash
修改。
② 检查当前分支是否为git
主分支(通常是master
或main
分支),如果不是,会提示用户切换到主分支。
③ 提示用户输入新版本号,或者根据用户指定的版本升级方式(如major
、minor
、patch
等)自动生成新版本号。(参考lerna version
)
④ 对每个包进行版本升级和打标签,并将修改提交到git
仓库。
⑤ 为每个包生成一个新的版本号,并将新版本号写入到各个包的package.json
文件中。
⑥ 将每个包发布到指定的npm registry
上,并将发布日志写入到各个包的CHANGELOG.md
文件中。该命令默认只会发布被修改过的包,如果需要发布所有包,可以使用
--force-publish
选项。
此外,lerna
不会发布那些被标记为私有的包("private":true
)。这与npm publish
的行为一致。
什么是 Glob?
glob
是一种通配符模式,用于匹配文件路径名。它通常用于文件处理操作,例如列出一个目录中所有符合特定模式的文件名。在 Unix
和 类 Unix
操作系统中,glob
模式通常使用 shell
命令来执行,例如 ls *.txt
可以列出所有扩展名为 .txt
的文件。在编程语言中,也通常有相应的 glob
函数或类库来实现这种功能。
测试你的 Glob:https://kthompson.github.io/glob/
Glob 的常用规则
?
:匹配单个字符*
:匹配任何字符/
:路径分隔符**
:匹配所有文件和任意层级的目录。如:/src/**.js
将匹配src
目录内的所有js
文件(包括子目录中的文件){a, b}
:花括号内的表达式被扩展为多种模式,例如**/*.{b,c}
将扩展为**/*.b
和**/*.c
。最终结果是检查两种模式的集合。?(a | b)
:匹配零次或一次出现的给定模式。*(a | b)
:匹配零次或多次出现的给定模式。+(a | b)
:匹配一次或多次出现的给定模式。@(a | b)
:匹配给定模式之一。!(a | b)
:匹配除给定模式之一以外的任何内容。
Lerna + yarn workspace
当 Lerna
和 yarn workspace
配合使用时,可以更好地管理具有多个包的项目。Lerna
可以轻松地管理和发布包,而 yarn workspace
可以帮助你在 Monorepo
中管理依赖项。
代码规范
ESLint - 确保代码格式规范且正确
ESLint 是什么?
ESLint
是一个用于检查 JavaScript
代码错误和风格的工具。它可以帮助开发人员在编写代码时遵循一致的编码规范,并提高代码质量。ESLint
可以通过配置文件来指定要检查的文件和规则,并支持第三方插件和解析器。
ESLint 可以为项目带来哪些好处?
-
提高代码质量:
ESLint
可以检查代码中的错误和风格问题,帮助开发人员遵循一致的编码规范,从而提高代码质量。 -
减少错误:
ESLint
可以检查代码中的错误,例如未定义的变量、未使用的变量等,帮助开发人员及时发现和修复错误。 -
提高代码的可维护性:
ESLint
可以检查代码中的风格问题,例如缩进、空格等,帮助开发人员编写易于阅读和维护的代码。 -
支持扩展:
ESLint
支持第三方插件和解析器,可以根据项目的需要进行扩展。
如何使用 ESLint?
安装
局部安装:npm i eslint --save-dev
(建议)
全局安装:npm i eslint -g
初始化配置
通过 eslint --init
,cli 会根据你的选项生成对应的配置文件。你可以选择配置文件的格式,同时可以选择一些你需要的规则。
你也可以选择在根目录下手动创建 .eslintrc.js
文件,eslint
支持多种格式的配置文件,你可以创建其中一种。
按优先级排序:
.eslintrc.js
>.eslintrc.yaml
>.eslintrc.yml
>.eslintrc.json
>.eslintrc
>package.json 中的 eslint 配置
配置详解
我们看一下 .eslintrc.js
中都有哪些配置选项:
-
globals
: Record<string, boolean | “readonly” | “writable” | “off”>ESLint 的一些核心规则依赖于对代码在运行时可用的全局变量的了解。由于这些在不同环境之间可能会有很大差异,并且会在运行时进行修改,因此 ESLint 不会假设执行环境中存在哪些全局变量。如
jQuery
提供的$
符号。在这种情况下,ESLint 会提示错误,需要向 ESLint 规则中添加需要识别的变量。对于这种情况,可以在配置文件中或通过在源代码中使用配置注释来定义全局变量。
{ "globals": { "$": "readonly" } }
- readonly / false 只读
- writable / true 可写
- off 禁用
-
env
: Record<string, boolean>为了避免配置每一个全局变量带来的麻烦,ESLint 具有全局变量集合的预设,如果我们要使用 jQuery 提供的全局变量,只要需要在 env 配置中添加
"jquery": true
就可以了。{ "env": { "jquery": true } }
查看可用的 env:https://eslint.org/docs/latest/use/configure/language-options#specifying-environments
-
root
: boolean在 ESLint 中,可以通过 .eslintrc.* 或 package.json 文件来指定配置。ESLint 会在要检查的文件的目录中自动查找这些配置文件,并且会一直向上查找,直到到达文件系统的根目录 (/)、当前用户的主目录 (~/)。
当配置文件中
root: true
被指定时,ESLint 不会继续向上级目录查找文件,而是让项目内需要被检测的文件都走我们的项目根目录下的这个配置。{ "root": true }
-
rules
: Record<string, 0 | 1 | 2 | “off” | “warn” | “error”>ESLint 内置了大量规则,
rule
用于验证代码是否满足特定期望,以及如果不满足该期望该怎么办。检验规则具有 3 个报错等级:
① “off” 或 0:关闭规则
② “warn” 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
③ “error” 或 2:开启规则,error 级别的错误 (当被触发的时候,程序会退出){ "rules": { "no-console": 2 } }
规则的值可以为字符串,也可以是数组。当规则的值是数组时,数组的第一个值是对规则的报错等级,后面的值就是这个规则的参数。
{ "rules": { "no-confusing-arrow": [ "error", { "onlyOneSimpleParam": true } ] }
-
extends
: string[]extends 是一种配置规则的方式,可以通过继承一个或多个已有的配置来扩展自己的配置。这意味着可以在自己的配置文件中使用
extends
属性来继承其他配置文件中的规则。这样可以避免重复定义相同的规则,同时还可以通过继承其他配置来快速启用一些通用的规则。extends 可以分为以下几种类型:
①eslint:
开头的:eslint 官方的扩展。如:eslint:recommended
(推荐规范)和eslint:all
(所有规范)
②plugin:
/eslint-plugin-
开头的:通过插件共享的规则。如:eslint-plugin-react
,可以使用其中的plugin:react/recommended
规则集
③eslint-config-
开头的:第三方发布到 npm 上的规则。如:eslint-config-standard
④@
开头的:和eslint-config-
一样,但其 npm 包拥有 scope。如:@vue/eslint-config-prettier
⑤ 路径:ESLint 可以解析相对于使用它的配置文件的基本配置文件的相对路径或绝对路径。如:./node_modules/coding-standard/.eslintrc-es6
{ "extends": [ "eslint:recommended", "plugin:vue/essential", "eslint-config-standard", "@vue/prettier", "./node_modules/coding-standard/.eslintrc-es6" ] }
-
parser
: stringparser
属性用于指定 JavaScript 代码的解析器。由于不同的JavaScript
版本和扩展语言有不同的语法和特性,因此需要使用不同的解析器来解析代码。ESLint 支持多种解析器,包括默认的
Espree
解析器和其他第三方解析器。通过配置parser
属性,可以指定要使用的解析器,以便 ESLint 可以正确地解析代码并检查其中的语法错误和其他问题。默认的解析器只支持已经形成 ES 标准的语法特性,对于处于实验阶段以及非标准的需要使用 Babel 转换的语法,需要指定由
Babel
提供的@babel/eslint-parser
。使用这个解释器的前提是你的项目使用了babel
。{ // 使用前需先安装 parser: '@babel/eslint-parser', parserOptions: { // @babel/eslint-parser相关的选项 } }
如果你的项目使用了
ts
,想使用@typescript-eslint/eslint-plugin
这个插件提供的规则来校验你的代码,此时就需要使用@typescript-eslint/parser
来做解释器了。{ "parser": "@typescript-eslint/parser", "extends": ["plugin:@typescript-eslint/recommended"], "plugins": ["@typescript-eslint"], "parserOptions": { // @typescript-eslint/parser的选项 }, }
-
parserOptions
: Record<string, any>parserOptions 是一个可选的配置项,它用于配置解析器的选项。通过这个属性,可以指定要解析的语言选项,以便 ESLint 可以正确地解析代码并检查其中的语法错误和其他问题。parserOptions 可以包含以下选项:
- ecmaVersion:指定要解析的
ECMAScript
版本。如:"ecmaVersion": 2018
表示要解析es2018
版本的代码。 - sourceType:指定要解析的代码是模块代码还是脚本代码。例如,
"sourceType": "module"
表示要解析模块代码。 - ecmaFeatures:指定要解析的
ECMAScript
特性。例如,"ecmaFeatures": { "jsx": true }
表示要解析 JSX 语法的代码。
- ecmaVersion:指定要解析的
-
plugins
: string[]插件是用来扩展 ESLint 功能的工具,可以帮助检查更多的代码类型和问题。虽然 ESLint 提供了许多规则供选择,但是随着 JavaScript 框架和语法的发展,这些规则可能无法检查到一些特定问题。
例如,如果我们使用 Vue 的
Template
或 React 的JSX
语法编写代码,ESLint 就无法检测到其中的问题。因此,可以通过使用插件来扩展 ESLint 的功能,以便检查这些特定问题。要使用 ESLint 插件,需要先安装插件。可以通过 npm 安装插件,例如
npm install eslint-plugin-react
。安装完成后,在配置文件中通过plugins
属性来启用插件。例如,要启用名为"eslint-plugin-react"
的插件,可以在配置文件中添加以下内容:{ "plugins": [ "react" ], "rules": { // 在这里配置规则 } }
在这个示例中,我们启用了名为
"eslint-plugin-react"
的插件,并在配置文件中配置了该插件提供的规则。需要注意的是,不同的插件可能提供不同的规则和功能。可以在插件的文档中查看其提供的规则和功能,并按需启用。ESLint 的插件有两种配置方法:
① plugin + rule 开启部分规则:
使用plugins
字段后,只是引入了插件,并没有使用具体规则。此时的校验是没有效果的,需要在rules
中显式开启你需要使用的规则。{ "plugins": ["prettier"], "rules": { "prettier/prettier": "error" } }
② 使用 extends 引入插件并配置默认规则:
{ extends: ['plugin:prettier/recommended'], }
示例:启用 decorator
要使 ESLint 支持 decorator
特性,需要将 parser
设为如 @babel/eslint-parser
这种支持 experimental语法
的解析器
npm i @babel/eslint-parser @babel/eslint-plugin --save-dev
{
"parser": "@babel/eslint-parser",
"plugins": [
"@babel/eslint-plugin"
]
}
同时,应该安装decorator
对应的babel插件
npm i @babel/plugin-proposal-decorator --save-dev
在 babel
配置中启用该插件:
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
配置jsconfig.json
{
"complierOptions": {
"experimentalDecorators": true
}
}
如何触发 ESLint?
-
命令行方式:在命令行中运行
eslint
命令,并指定要检查的文件或目录。① 校验单个文件:
eslint a.js
② 校验一个目录:
eslint src
③ 校验非 js 格式的文件:
默认情况下,ESLint
仅支持校验js
格式的文件。如果需要校验其他类型的文件,需要配置相关的plugin
。同时,在运行时指定--ext
参数来指定扩展名。eslint --ext .vue,.jsx src
④ 根据配置的规则进行自动修复
eslint src --fix
⑤ 在 package.json 配置 npm 脚本
{ "scripts": { "lint": "eslint src --fix } }
-
集成到编辑器中:通过安装 ESLint 插件,将 ESLint 集成到编辑器中。编辑器会在保存文件时自动触发 ESLint 检查,并在编辑器中显示检查结果。
-
集成到构建工具中:通过在构建工具(如 Webpack、Gulp、Grunt 等)中配置 ESLint 插件,将 ESLint 集成到构建流程中。构建工具会在打包时自动触发 ESLint 检查,并将检查结果输出到控制台或日志文件中。
-
集成到代码托管平台中:通过在代码托管平台(如 GitHub、GitLab 等)中配置 ESLint 插件,将 ESLint 集成到代码提交流程中。代码托管平台会在提交代码时自动触发 ESLint 检查,并根据检查结果决定是否允许代码合并。
Prettier - 使用统一风格格式化代码
Prettier 是什么?
Prettier
是一个代码格式化工具,它可以自动化地格式化你的代码,使其更易于阅读和维护。Prettier
支持多种编程语言,包括 JavaScript、TypeScript、CSS、HTML、JSON
等。你可以在你的项目中集成 Prettier
,以便在提交代码之前自动格式化你的代码。
与 ESLint
不同,Prettier
并不关心代码的语义和逻辑,它只关心代码的外观。当你在编辑器中保存你的代码时,Prettier
可以自动格式化你的代码,使其符合预定义的格式规则。
如何使用 Prettier
安装
npm install prettier --save-dev
配置
首先,在根目录创建 .prettierrc.json
文件(配置 Prettier
并让编辑器知道你正在用 Prettier
)
可以选择创建一个
.prettierignore
让Prettier
知道哪些文件不需要被格式化
Prettier
的配置项比较少,可以查看官方文档:https://www.prettier.cn/docs/options.html#parser
例:
{
"semi": false,
"singleQuote": true,
"arrowParens": "always",
"htmlWhitespaceSensitivity": "ignore",
"trailingComma": "all"
}
如何运行 Prettier?
当你集成了 Prettier 到你的项目中,你可以使用以下方法来执行 Prettier。
-
使用编辑器插件:许多编辑器都有 Prettier 插件可用,你可以直接在编辑器中使用快捷键或右键菜单来执行 Prettier。常见的编辑器插件包括 VS Code、Sublime Text、Atom 等。
-
使用命令行工具:你可以在终端中使用命令行工具来执行 Prettier。在你的项目根目录下运行以下命令即可格式化整个项目:
npx prettier --write .
这将使用 npx 运行 Prettier,并使用 --write
参数来直接修改文件。.
表示格式化整个项目。
- 集成到构建工具:你可以将 Prettier 集成到你的构建工具中,如 webpack、gulp 等。这样,每次构建项目时都会自动执行 Prettier。
无论你选择哪种方法,Prettier 都会自动格式化你的代码,并将其保存到文件中。
如何搭配 ESLint 使用?
ESLint 和 Prettier 是两个不同的工具,它们的作用也不同:
ESLint
是一个静态代码分析工具,用于检查代码中的语法和风格错误。它可以根据预定义的规则来检查代码,也可以根据自定义规则来检查代码。Prettier
是一个代码格式化工具,用于自动格式化代码。它可以根据预定义的规则来格式化代码,也可以根据自定义规则来格式化代码。
在使用 ESLint
和 Prettier
进行代码检查和格式化时,可能会出现某些规则冲突的情况,这会导致 ESLint
和 Prettier
之间的集成出现问题。
为了解决这个问题,可以使用 eslint-config-prettier
。这个包可以关闭 ESLint
中与 Prettier
冲突的规则,确保 ESLint
和 Prettier
之间的无缝集成。
- 安装:
npm install --save-dev eslint-config-prettier
- 在
.eslintrc.json
中添加"prettier"
到extends
数组中,例如:
{
"extends": [
"eslint:recommended",
"prettier"
]
}
上面介绍的工具,仅将部分 ESLint
规则禁用了。那么如何将两者结合起来使用呢?
这时可以使用一个插件 eslint-plugin-prettier
。这个插件用于将 Prettier
的规则作为 ESLint
规则来运行,以便在运行 ESLint
时就可以检测到代码格式问题。它会将 Prettier
的规则作为 ESLint
规则来运行,这样就可以在运行 ESLint
时自动检测代码格式问题。
要使用 eslint-plugin-prettier
,需要执行以下步骤:
-
安装
eslint-plugin-prettier
和prettier
:npm install --save-dev eslint-plugin-prettier prettier
-
在
.eslintrc
文件中添加prettier
插件:{ "plugins": ["prettier"], "rules": { "prettier/prettier": "error" } }
-
上面 ESLint 的相关章节介绍过,你也可以使用插件推荐的配置:
{ "extends": ["plugin:prettier/recommended"] }
提交规范
Commitizen - 自动生成提交说明
什么是约定式提交?
约定式提交是一种标准化的提交消息格式,旨在提高代码库的可读性、可维护性和自动化程度。它规定了一些特定的前缀和格式,用于描述提交的类型、范围、主题和其他元数据。这些提交消息可以被用于生成CHANGELOG、自动化版本号控制、代码审查等自动化流程。
Conventional-commits 详情:https://www.conventionalcommits.org/zh-hans/v1.0.0/
约定式提交有什么要求?
提交信息结构
为了使提交格式清晰、便于阅读,对提交信息做出了一定的约束。Angular
约定包括以下部分:
- 标题(header):用一行简短的描述来总结更改内容,并使用特殊关键字指定更改类型和影响范围。
- 正文(body):提供更详细的更改描述,包括更改原因、影响和解决方案等信息。
- 页脚(footer):提供一些附加信息,如相关链接、关联的BUG编号等。
其中,标题部分又包括:
- 类型(type):描述提交的类型,如feat、fix、docs、style、refactor、test、chore等。
- 范围(scope):描述本次提交影响的范围,如路由、模型、控制器等。
- 主题(subject):简要描述本次提交的内容,通常不超过50个字符。
总结一下,提交信息的结构大致可以描述为:
<类型>[可选 范围]: <描述>
[可选 正文]
[可选 脚注]
提交类型
值 | 描述 |
---|---|
feat | 新功能 |
fix | 修复 Bug |
docs | 文档更变 |
style | 代码格式(不影响代码运行的变动) |
refactor | 重构(既不增加 feature,也不修复 Bug) |
perf | 性能优化 |
test | 增加测试 |
chore | 构建过程或辅助工具的变动 |
revert | 回退 |
build | 打包 |
特殊脚注
-
不兼容变更
软件或系统中的变化,可能会导致已有的代码、API或行为无法向后兼容。这种变化可能会导致现有的代码或系统出现错误或无法正常工作,需要进行修改或更新,则Footer
以BREAKING CHANGE
开头,后面是对变动的描述、以及变动的理由和迁移方法。例:
feat: add new API endpoint for user authentication BREAKING CHANGE: the existing /api/auth endpoint has been removed in favor of the new /api/login endpoint. This change requires all clients to update their authentication code to use the new endpoint.
-
关闭issue
在提交消息中包含Closes #<issue number>
或Fixes #<issue number>
即可关闭对应的 issue。例:
feat(Shared): add new feature Closes #123
-
提及 pull request
在提交消息中包含Ref #<pull request number>
即可提到对应的 pull request。例:
fix: fix bug in login form Ref #456
使用 Commitizen 生成提交说明
安装 Commitizen
可以理解为一个命令行工具,当使用 Commitizen 提交时,系统会提示你在提交时填写必需的提交字段(如:type、scope、subject、body、footer)。
安装 Commitizen
:
npm install -g commitizen
如果你想指定版本,可以选择将它安装为 devDependency
,这样有助于在不同开发者间保持相同的 Commitizen
行为。
Commitizen 适配器
Commitizen adapter
提供了一些预定义的提交类型和格式,并通过用户友好的交互式命令行界面来引导你创建符合规范的提交信息。
cz-conventional-changelog
如果需要在项目中使用 commitizen
生成符合 Angular
规范的提交说明,可以选择 cz-conventional-changelog
适配器:
commitizen init cz-conventional-changelog --save-dev --save-exact
请注意,如果已经有其他适配器了会报错。可以用 --force 参数。
初始化命令做了以下事情:
- 在项目中安装
cz-conventional-changelog
适配器依赖 - 将适配器依赖保存到
package.json
的devDependencies
- 在
package.json
中新增config.commitizen
字段信息,主要用于配置cz工具的适配器路径:
{
"devDependencies": {
"cz-conventional-changelog": "^x.x.x"
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
}
}
你可以在 npm 脚本中添加一个脚本运行 Commitizen:
{
"scripts": {
"commit": "git cz"
}
}
cz-customizable
cz-customizable
是一个 commitizen adapter
,它允许用户自定义提交信息的格式和内容。与 cz-conventional-changelog
不同,cz-customizable
没有预定义的提交类型和格式,而是通过配置文件来定义。用户可以根据自己的需求定义提交信息的各个部分,例如类型、作用域、描述等。
安装:
npm install cz-customizable --save-dev
添加配置:
{
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}
在项目根目录创建 .cz-config.js
,这里附上我项目中的配置文件:
module.exports = {
types: [
{
value: ":sparkles: feat",
name: "✨ feat: 新功能",
},
{
value: ":bug: fix",
name: "🐛 fix: 修复",
},
{
value: ":memo: docs",
name: "📝 docs: 文档变更",
},
{
value: ":lipstick: style",
name: "💄 style: 代码格式 (不影响代码运行的变动)",
},
{
value: ":recycle: refactor",
name: "♻️ refactor: 重构 (既不增加 feature, 也不修复 bug)",
},
{
value: ":zap: perf",
name: "⚡️ perf: 性能优化",
},
{
value: ":white_check_mark: test",
name: "✅ test: 增加测试",
},
{
value: ":wrench: chore",
name: "🔧 chore: 构建过程或辅助工具的变动",
},
{
value: ":rewind: revert",
name: "⏪ revert: 回退",
},
{
value: ":rocket: build",
name: "🚀 build: 打包",
},
],
messages: {
type: "请选择提交的类型:",
scope: "请选择此更改的范围(可选):",
customScope: "请输入修改的范围(可选):",
subject: "请简要描述提交(必填):",
body: "请输入详细描述(可选):",
breaking: "列举破坏性修改(可选):",
footer: "请输入要关闭的 issue(可选):",
confirmCommit: "确认要使用以上信息提交?(y/n):",
},
// 你可以在这里预定义 Scope
scopes: [
{ name: "main" },
{ name: "app" },
{ name: "data-extraction" },
{ name: "interactive-map" },
{ name: "shared" },
],
allowCustomScopes: true, // 允许输入自定义 Scope 信息
allowBreakingChanges: [":sparkles: feat", ":bug: fix"], // 允许添加 Breaking Change 的操作
subjectLimit: 100, // body 主题字数限制
};
Commitlint - 校验提交信息
通过 Commitizen
,你可以根据模板自动生成提交信息。但是 Commitizen
没有任何校验功能,它只是帮你自动生成提交信息。如果此时提交 git commit -m "123abc123"
,你会发现仍然可以成功提交。
你可以使用 Commitlint
在执行 commit
操作时,按照提前配置好的规则进行校验。
安装和配置
首先安装 Commitlint
:
npm install @commitlint/cli @commitlint/config-conventional --save-dev
然后在项目根目录创建 commitlint.config.js
文件,配置 Commitlint
:
module.exports = {
extends: ['@commitlint/config-conventional']
};
这里使用了
@commitlint/config-conventional
适配器是由于上一步中你安装了cz-conventional-changelog
。如果你是用的是cz-customizable
,那么需要安装commitlint-config-cz
,并将commitlint.config.js
中的extends
属性设置为:extends: [ "cz" ]
到这里,Commitlint
已经基本配置完成了,但是如何在 git commit
操作被调用时自动运行 commitlint
呢?
这里先留个悬念,相关技术在下一章节 Husky + Lint-staged
中进行讲解。
为什么提交信息中如果含有 Emoji,在提交时会提示检测不到 type, subject, body?
在 commitlint-config-cz
中,默认的提交信息匹配规则是:<类型>[可选 范围]: <描述>
。
而我们自定义的信息中,header 已经变成了::sparkles: feat
。
这就是问题所在。我们需要修改一下匹配提交信息的正则表达式。在 commitlint.config.js
中添加以下内容:
module.exports = {
parserPreset: {
parserOpts: {
headerPattern: /^(?<type>.*\s\w*)(?:\((?<scope>.*)\))?!?:\s(?<subject>(?:(?!#).)*(?:(?!\s).))$/,
headerCorrespondence: ["type", "scope", "subject"],
},
},
}
为什么已经配置完成了,但 Commitlint 还是不生效?
这里假设已经正确进行所有配置,我们打开终端执行 git commit -m "123123"
。
你会发现,为什么提交信息没有符合规范,但是仍然被成功 commit
了呢?
在之前的操作中,我们只设置了 Commitizen 和 Commitlint 使用哪种解析器,而并没有为它设置 lint 规则。
现在打开 commitlint.config.js
,添加以下内容即可:
module.exports = {
rules: {
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
},
}
你可能比较疑惑,上面配置中的 2
和 "never"
分别代表什么?
我们现在看一下官方文档是怎么说的:https://commitlint.js.org/#/reference-rules
Rules are made up by a name and a configuration array. The configuration array contains:
- Level [0…2]: 0 disables the rule. For 1 it will be considered a warning for 2 an error.
- Applicable always|never: never inverts the rule.
- Value: value to use for this rule.
解释一下,每个规则的值是由一个数组构成的。
- 第一个参数可以参考
ESLint
的错误级别: 0 不提醒错误;1 发出警告;2 报错; - 第二个参数中 “never” 代表不允许规则发生,如果发生了则按第一个参数定义的错误级别发出警- 告。“always” 代表允许规则发生,如果没有发生则按第一个参数定义的错误级别发出警告。
- 某些规则可能需要传入值,如:
header-max-length
。你可以在第三个参数中传入这个规则的值。
这里贴上我项目的配置:
module.exports = {
extends: ["cz"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [2, "always"],
"scope-case": [2, "always", "lower-case"],
"subject-case": [
2,
"never",
["sentence-case", "start-case", "pascal-case", "upper-case"],
],
"subject-empty": [2, "never"],
"subject-exclamation-mark": [2, "never"],
"subject-full-stop": [2, "never", "."],
"type-empty": [2, "never"],
"type-case": [2, "always", "lower-case"],
},
parserPreset: {
parserOpts: {
headerPattern:
/^(?<type>.*\s\w*)(?:\((?<scope>.*)\))?!?:\s(?<subject>(?:(?!#).)*(?:(?!\s).))$/,
headerCorrespondence: ["type", "scope", "subject"],
},
},
};
Husky + Lint-staged - 提交前最后一道防火墙
技术简介
在前面的章节中,我们了解到了一些实现代码规范和提交规范的技术,但是这些它们需要手动执行一些命令才能够被运行。
每次提交代码时都要进行这些重复性的操作,这无疑是复杂的。如果遗忘了某些操作,可能会将不规范的代码提交到仓库中。所以我们需要一个帮助我们在 git 提交前自动执行这些操作的工具。
面对这些需求,Husky
+ Lint-staged
无疑是一种优秀的解决方案。
Husky
是一种可以帮助我们在 Git 执行的关键操作(如:commit
、push
、merge
)前后,执行一些脚本和命令的工具。这些脚本可以包括 Linters
、代码格式化程序、打包发布程序等,以确保代码符合团队中的最佳实践和代码风格要求。
例如,开发人员可以设置 Husky
在每次提交代码时运行 ESLint
,以确保 JavaScript
代码没有语法错误或不良实践。通过使用 Husky
,开发人员可以更快地发现和修复问题,同时确保他们的代码符合团队中的标准。
本段提到:“Husky 是一种可以帮助我们在 Git 执行的关键操作前后,执行一些脚本和命令的工具”。
这意味着 Husky 只能在 Git 仓库中工作。确保使用 Husky 前已在你的项目中初始化 Git 仓库。
在了解 Husky
前,我们首先要了解一下:
什么是 Git Hooks?
Git Hooks 是一种 Git
版本控制系统的功能,它允许开发人员在特定的 Git 事件
发生时运行自定义脚本。这些事件可以包括提交代码、推送代码、合并分支等等。
Git Hooks 可以用于许多不同的目的,例如在代码提交前运行测试、检查代码风格、自动化部署等等。它们通常是通过在 .git/hooks
目录下添加可执行脚本来实现的。
脚本的返回值如果为 0 则该 Hook 执行成功,否则执行失败,本次提交操作将被打断。后面的 Lint-staged 和 commitlint 则是利用这一原理打断不符合规范的提交操作。
Git Hooks 有两种类型:客户端钩子
和 服务器端钩子
。客户端钩子在 本地
运行,而服务器端钩子在 远程 Git 仓库
上运行。通过使用 Git Hooks
,开发人员可以自动化和规范化他们的工作流程,并确保代码质量和一致性。
举个例子:Vue 和 React 都有它们各自的生命周期,如:getDerivedStateFromProps, componentDidMount, onUpdated。这些生命周期分别在应用执行的不同阶段运行。
而 Git Hooks 的概念与它们相近,在 Git 操作的不同阶段运行。如:提交前,验证提交信息时。
列举几个常用的 Git Hook:
pre-commit
:在代码提交前运行,用于检查代码风格、运行测试等。pre-push
:在代码推送到远程仓库前运行,用于确保代码的质量和一致性。post-merge
:在分支合并完成后运行,用于自动化部署和其他后续操作。post-checkout
:在检出分支后运行,用于更新依赖项、运行测试等。prepare-commit-msg
:在Git提交信息编辑器打开前运行,用于自动填充提交信息。commit-msg
:在Git提交信息编辑器关闭后运行,用于检查提交信息是否符合规范和标准。
如何使用 Husky?
前面的章节中讲到了使用 Lerna
+ Yarn workspace
管理 Monorepo
。所以我们使用 yarn
作为包管理工具,进行 Husky
的配置。
如想了解其他包管理工具的配置方法,请访问:https://typicode.github.io/husky/getting-started.html#automatic-recommended
① 在项目中安装 Husky
yarn add husky -D
② 启用 Hooks
npx husky install
③ 为了在仓库被拉取后自动初始化 Git Hooks,编辑 package.json
:
{
"private": true,
"scripts": {
"postinstall": "husky install"
}
}
④ 例:创建一个在 pre-commit 阶段运行的 Hook
npx husky add .husky/pre-commit
编辑 .husky/pre-commit
文件,假设我们想在 pre-commit
阶段格式化代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
为什么需要 husky install?不可以直接在 .git/hooks 中直接添加 Hook 吗?
在 ./git/hooks
目录中手动添加 Git Hooks
也是没问题的,但是使用Husky可以带来以下好处:
① 方便管理:使用 Husky 可以轻松地管理和配置 Git Hooks,而不需要手动复制和粘贴 Hook 脚本。
② 跨平台兼容性:不同的操作系统可能有不同的 Git Hook 脚本语法和命令,使用 Husky 可以避免这些问题,并确保 Hook 脚本在所有平台上运行一致。
③ 版本控制:使用 Husky 可以将 Hook 脚本添加到版本控制系统中,这样就可以在多个开发环境中共享 Hook 脚本,并确保所有开发人员都使用相同的 Hook 脚本。
④ 错误检测:使用 Husky 可以检测配置错误,例如无效的命令或语法错误,并在运行 Hook 脚本之前发出警告或错误提示,以避免意外的问题。
格式化暂存区代码 - Lint-staged
通过对 Husky
的介绍,我们可以得到在 git commit
前自动执行 eslint
或 prettier
的方法。但应该保证 pre-commit
阶段命令的执行速度,如果我们直接 lint 整个项目,速度是非常慢的。
Lint-staged
,这个工具可以帮助我们过滤出 Git
暂存区的文件。与操作整个项目相比,只操作有变化的代码的的耗时大大降低。
安装与配置
① 安装 Lint-staged
yarn add lint-staged -D
② 在 package.json 中添加 npm 脚本
{
"scripts": {
"lint": "lint-staged"
}
}
③ 创建 pre-commit 钩子,运行 lint 脚本
npx husky add .husky/pre-commit
./husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
④ 创建配置文件 .lintstagedrc.js
module.exports = {
'*.{js,jsx,ts,tsx}': ['prettier --write', 'eslint --fix'],
};
我们来看看这段代码。熟悉吗?没错,又是 Glob
(忘记 Glob 的伙伴看看上面的 Lerna 章节)
Lint-staged 使用 Glob 表达式来定义哪些文件要执行哪些命令。
上面的示例代码代表项目中所有的 js
、jsx
、ts
、tsx
文件将会被 prettier
和 eslint
找到不符合规范的地方并进行修复。修复完成后,由自动修复带来的代码变化将会被合并到本次 commit
。(因为我们在 pre-commit
阶段执行 lint-staged
,此时 commit
未完成,不会创建新的提交)
同样地,在 commit-msg
阶段,我们可以使用 commitlint
实现检查提交信息格式:
npx husky add .husky/commit-msg
.husky/commit-msg:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint -e $1
Git Hook 中的 $1 / HUSKY_GIT_PARAMS 是什么?
上面的示例代码中,出现了 $1
这个变量,它是什么呢?
旧版本的 Husky 中该变量为 HUSKY_GIT_PARAMS,所以你在一些教程中可以看见 HUSKY_GIT_PARAMS。但是新版本 Husky 中变量名称已经更改,请使用 $1
在 Husky
中,$1
是指在执行 Git
钩子时传递给该脚本的第一个参数,在不同钩子中 $1
具有不同的值。
例如,在 pre-commit
钩子中,$1
是指提交的 Git SHA
值。开发人员可以在钩子脚本中使用 $1
来获取该参数,并在脚本中进行处理。
而在 commit-msg
钩子中,$1
是指传递给 Git 提交消息的文件路径
。在这个钩子中,开发人员可以使用 $1
来获取提交消息的内容,并在脚本中进行处理,例如检查提交消息是否符合规范。
-e $1
意味着将 commitlint
从 $1
这个路径的文件中读取提交信息并进行 lint
操作。
在 Monorepo 中使用 Lint-staged
https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo
对于使用 Monorepo 策略管理的仓库,仓库中会有多个软件包。各软件包可能由不同技术构成,可能存在 .ts
、.js
、.vue
、.jsx
、.tsx
等文件。对于这种情况,我们需要对不同的包制定不同的 Lint-staged 规则。
① 首先在项目顶层包安装 lint-staged
。
② 在项目根目录创建空的 .lintstagedrc
(不要写入任何内容)。
③ 在各个需要执行 Lint-staged
的软件包中创建 .lintstagedrc.js
,写入相应配置。
任务流程
Lerna & NX
在 Monorepo 中,一个包可能对另一个包的资源有依赖,或一个包可能需要再另一个包执行某操作后才能执行编译。
虽然我们可以手动运行这些任务,但如果有100个包,手动运行的成本则大大提高。这个时候你可以使用 Lerna 将 npm 脚本的运行委托给 Nx。(Lerna 中集成了 NX)
Lerna 利用 Nx 强大的任务运行器来运行脚本,允许你 并行运行它们
、缓存结果
并 将它们发布在多台机器上
,同时 保证包之间的依赖关系
,根据依赖关系 按顺序执行操作
。
配置 NX
在根目录创建 nx.json
:
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": []
}
}
},
"targetDefaults": {
"build": {
"outputs": ["{projectRoot}/dist"] // 指定每个 package 默认输出路径,用于缓存
}
}
}
你可以在配置中使用 {projectRoot} 和 {workspaceRoot} 作为项目根目录和工作空间根目录的占位符
taskRunnerOptions
runner
Nx 中的一切都是可定制的,包括运行 npm 脚本。大多数时候,您将使用默认运行器或 @nrwl/nx-cloud 运行器。
cacheableOperations
这个数组定义了 Nx 缓存的 npm 脚本/操作列表。
你可以在这个数组中添加需要缓存执行结果的操作。如:build
Target Defaults
这个对象的键是 npm 脚本名称。你可以在 targetDefaults
中添加与每个项目的构建脚本相关联的元数据。
dependsOn
构建项目之前必须先构建项目的依赖项。 dependsOn
属性可用于定义单个目标的依赖项。
举个例子:
{
"targetDefaults": {
"build": {
"dependsOn": [ "prebuild", "^build"]
}
}
}
这段配置意思是,每个 build 脚本
都需要 同一个项目 的 prebuild 脚本
和 所有依赖项的 build 脚本
先运行。
^ 代表当前项目的所有依赖,^build 则代表当前项目所有依赖的 build 命令
inputs & namedInputs
inputs 数组告诉 Nx 要根据什么确定脚本的指定命令是否应该是缓存命中。
输入分为三种类型:
-
Glob 文件集合
①{projectRoot}/**.*.ts
②same as {fileset: "{projectRoot}/**/*.ts"}
③{workspaceRoot}/jest.config.ts
-
运行时命令
①{runtime: "node -v"}
-
环境变量
outputs
"outputs": ["{projectRoot}/dist"]
告诉 Nx 构建脚本将在哪里输出打包结果。
如果数组为空,则代表测试目标不会在磁盘上输出任何内容。
Project-Specific Configuration
nx.json 中包含了 nx 的全局配置,如果你需要为某个软件包指定 nx 的配置,你可以在 package.json 中添加一个 nx 属性,用于定义配置。
项目中的自定义 nx 配置的属性和上一小节中的属性相同,不做过多介绍。
这里介绍一个特殊的属性:implicitDependencies
通常情况下,如果我们想要 module-b
在 module-a
执行某项操作前执行,则需要将 module-b
添加为 module-a
的依赖。
lerna add module-b --scope=module-a
通过在 module-a
的 package.json
中配置 "implicitDependencies": ["module-b", "!module-c"]
,可以显式声明两个模块间的引用关系,即使没有在 package.json
中添加两个模块的依赖。
Nx 将以与处理显式依赖项相同的方式处理此类依赖项。它还告诉 Nx,即使存在对 module-c
的显式依赖,也应该忽略它。
React Native 项目改造
当项目到达一定规模时,为了降低各模块的耦合度并更方便地管理分布在多个仓库中的内容,可以考虑使用 Monorepo
来管理你的 RN
项目。由于依赖提升、路径变化等原因,RN
应用可能不能直接在 Monorepo
中运行,需要调整一些构建配置。
项目结构
本篇文章以上述示例项目为例,讲解各组件的配置方式:
- /node_modules # 顶层 node_modules
- /packages/app # React Native 项目
- /packages/module-a # module-a
- /packages/shared # 共享代码
- /packages.json
- /lerna.json
提升依赖
由于 RN
运行时一些组件的依赖关系,react
、react-native
相关的包不能提升到顶层 node_modules
./package.json:
{
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/react",
"**/react-dom",
"**/react-native",
"**/react-native/**",
"**/react-native-codegen",
"**/react-native-dev-menu"
]
},
}
./lerna.json:
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "independent",
"npmClient": "yarn"
}
修改代码格式化配置
./eslintrc.js:
module.exports = {
...
- root: true,
+ root: false
...
}
修改 Metro 配置
安装 react-native-monorepo-tools
:
yarn add react-native-monorepo-tools -D
./metro.config.js:
const {getMetroTools} = require('react-native-monorepo-tools');
const exclusionList = require('metro-config/src/defaults/exclusionList');
const path = require('path');
const monorepoMetroTools = getMetroTools();
module.exports = {
projectRoot: path.resolve(__dirname),
watchFolders: monorepoMetroTools.watchFolders,
resolver: {
blockList: exclusionList(monorepoMetroTools.blockList),
extraNodeModules: monorepoMetroTools.extraNodeModules,
},
};
修改 npm 脚本
./package.json:
{
...
"scripts": {
+ "android:build": "cd android && ./gradlew assembleRelease"
}
...
}
适配 react-native-vector-icons
./android/app/build.gradle:
+ project.ext.vectoricons = [
+ iconFontsDir: "../../../../node_modules/react-native-vector-icons/Fonts",
+ ]
+ apply from: "../../../../node_modules/react-native-vector-icons/fonts.gradle"