通过上一课时的学习,我们分析了前端构建工具 webpack 的底层原理,在理解原理之后再来探索构建工具的具体应用——如何合理搭建前端项目。当然,前端项目搭建并不只是使用构建工具这么简单,本课时我们将从项目组织、代码规范 2 个方面来进行分析。
项目组织
考虑这样一个场景,在开发项目 projectA 的时候,发现其中的 codeX 也可以用于项目 projectB,最简单直接的处理方式就是把 codeX 的代码直接复制到 projectB 下,按照 “三次原则”(三次原则是指同一段代码被使用到 3 次时再考虑抽象)这种处理方式没什么问题。但如果此时项目 projectC 和 projectD 也会用到 codeX,那么这种方式维护起来会很麻烦。
有经验的工程师会想到将 codeX 发布成模块,作为依赖模块引入所需的项目中。此时对于 codeX 会涉及两种组织代码的方式:multirepo 和 monorepo。
multirepo
multirepo 就是将项目中的模块拆分出来,放在不同的仓库中进行独立管理。例如,用于 Node.js 的 Web 框架 Koa,它依赖的模块 koa-convert 和 koa-compose 分别拆分成了两个仓库进行管理。
这种方式的好处是保证仓库的独立性,方便不同团队维护对应的仓库代码,可以根据团队情况选择擅长的工具、工作流等。
但这种方式也会存在一些问题,具体如下。
- 开发调试及版本更新效率低下。比如在仓库 A 用到的仓库 B 中发现了一个 bug,就必须到仓库 B 里修复它、打包、发版本,然后再回到仓库 A 继续工作。在不同的仓库间,你不仅需要处理不同的代码、工具,甚至是不同的工作流程;还有,你只能去问维护这个仓库的人,能不能为你做出改变,然后等着他们去解决。
- 团队技术选型分散。不同的库实现风格可能存在较大差异(比如有的库依赖 Vue,有的依赖 React),还有可能会采用不同的测试库及校验规则,维护起来比较困难。
而 monorepo 方式恰好就能解决这些问题。
monorepo
monorepo 就是将所有相关的模块放在同一个项目仓库中。这种方式在管理上会更加方便,项目所有代码可以使用统一的规范及构建、测试、发布流程。
很多著名的开源项目都采取了这种管理方式,比如开源项目 babel,它依赖的模块都放在了 packages 目录下。
babel 的依赖模块
通过查看 babel 项目,发现根目录下有一个 lerna.json 的配置文件,这是开源工具 lerna的配置文件。lerna 是一个用于管理带有多个包的 JavaScript 项目工具,用 lerna 管理的项目会有 3 个文件目录:packages 目录、learna.json 文件和 package.json 文件。通过 lerna 命令行工具在初始化项目的时候就可以创建它们。
lerna 支持两种模式,分别是 Fixed/Locked 和 Independent 模式。
Fixed/Locked 模式为默认模式,babel 采用的就是这种模式,该模式的特点是,开发者执行 lerna publish 后,lerna 会在 lerna.json 中找到指定 version 版本号。如果这一次发布包含某个项目的更新,那么会自动更新 version 版本号。对于各个项目相关联的场景,这样的模式非常有利,任何一个项目大版本升级,其他项目的大版本号也会更新。
Independent 模式顾名思义,各个项目都是相互独立。开发者需要独立管理多个包的版本更新。也就是说,我们可以具体到更新每个包的版本。每次发布,lerna 会配合 Git,检查相关包文件的变动,只发布有改动的 package。
虽然众多开源项目采用了 monorepo,但它也并不是最完美的代码组织方式,也会带来一些问题,比如由于将多个模块集中在一个仓库中会导致仓库体积变大,目录结构也会变得更复杂。而 monorepo 也需要额外的工具来管理各个模块,这意味着相对 multirepo 而言会有一定的学习成本。
代码规范
什么样的代码才是好代码?不同的工程师可能给出不同的答案,比如:
- 少用全局变量
- 高内聚、低耦合
- 遵循单一原则
- 拥有注释说明
切换角度思考会帮助我们得到更全面的答案:从人的角度考虑,维护代码的开发者会不断地变更;从时间的角度考虑,代码会不断地被修改。我们可以总结一个最简单实用的答案:风格一致。 “风格一致” 就是让参与项目开发的工程师形成一种开发上的契约,从而降低维护成本。要达到这个目的,我们可以从代码编写和代码管理两个方向入手,分别对应编写规范和提交规范。
编写规范
网上关于 HTML、JavaScript、CSS 编写规范(也称编写风格)之类的文档资料很多,一般大型互联网公司都会制定自己的编写规范,比如 Google 的 JavaScript 风格指南、 Airbnb 风格指南,而对应的工具也不少。以 JavaScript 为例,比如 JSLint、JSHint、JSCS、ESLint 等多种规则校验工具。
不管我们在团队中制定怎样的编写规范,只要把握好下面 3 个核心原则,就能制定出合理的编写规范。
- 可执行。制定编写规范首先要保证的就是规范的可执行性。制定好规范如果只能靠工程师的自觉性去执行,靠代码审核去检查,那么执行效率会很低。所以建议编写规范中的每一条规则都能有对应的校验工具规则与之对应。
- 可配置。代码的可读性有时候是一个比较主观的问题,比如空格缩进问题,有的工程师认为 2 个空格缩进可以查看更多代码内容,而有的会认为 4 个空格缩进层次感更强。使用具有丰富配置项的代码校验工具就可以很轻松地解决这些分歧。
- 可扩展。这一点也是对于校验工具的要求,即当校验工具的已有配置规则无法支持项目需求时,可以自行编写插件来扩展校验规则。
最常用的 ESlint 就可以满足可配置、可扩展的原则,它的核心功能是通过一个叫 verify() 的函数来实现的,该函数有两个必传参数:要验证的源码文本和一个配置对象(通过准备好的配置文件加命令行操作会生成配置)。该函数首先使用解析器生成抽象语法树(AST),同时为规则中所有的选择器添加监听事件,在触发时执行;然后从上到下遍历 AST。在每个节点触发与该节点类型同名的一个事件(即 “Identifier”“WithStatement” 等),监听函数校验完相关的代码之后把不符合规则的问题推送到 lintingProblems 数组中返回。
提交规范
虽然在开发过程中,每次在使用 Git 提交代码时都会编写提交消息(Commit Message),但提交规范仍然是一个很容易被忽视的点。而良好的提交规范和编写规范一样,也能较大地提升代码的可维护性,一方面能保证在代码回退时能快速找到对应的提交记录,另一方面也可以直接将提交消息生成修改日志(Change Log)。
Angular 的提交日志
虽然 Git 自带 template 功能,这个功能可以定义一个提交消息的模板文件,然后通过 git config 命令指向这个模板文件。这样在每次提交的时候就会使用默认的编辑器打开一个模板文件,编辑对应信息后保存即可。但不具有强制性,推荐使用工具 @commitlint/cli 和 husky。commitlint 可以设置提交消息模板并校验,而 husky 可以设置 pre-commit 钩子,在提交代码时调用 commitlint 进行强制校验,避免生成不符合规范的提交消息。
下面的 husky 配置文件会在提交之前执行命令 npm test,在生成提交消息时执行 commitlint。
{
"hooks": {
"pre-commit": "npm test",
"commit-msg": "commitlint"
}
}
从上面的例子可以看到,husky 同构监听 git 钩子,不仅可以校验提交消息,还可以调用自定义的 npm 脚本进行代码校验或执行测试代码。随着项目不断增大,对整个项目上运行 lint 或 test 会变得非常耗时,我们一般只想对更改的文件进行检查,这时候可以借助 lint-staged。
下面是 Vue 的 lint-staged 相关配置。它表示对于 js 后缀的文件执行 eslint —fix 命令来校验和修复代码 ,通过之后再进行 git add 添加到暂存区。
{
...
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
},
...
}
总结
这一课时站在前端工程的角度,从项目组织和代码规范两个方面分析了如何搭建可维护性的前端项目。
- 在项目组织上,对于相关性低的模块可以采用 multirepo 方式进行独立管理,相关度高的模块则可以采用 monorepo 方式对其进行集中管理。
- 在制定代码规范时,对于编写规范,尽量做到可执行、可配置、可扩展,对于提交规范,可以选择适当的工具,比如 commitlint、husky 来保证提交消息的规范化和可读性。
希望你通过本课时的内容,能对前端项目搭建有更深入的理解和思考,不只是停留在会用命令行工具或脚手架来创建项目的层次。而是在空间维度和时间维度上来考虑如何组织项目代码和规范项目代码。
最后布置一道思考题:你在开发中还用到了哪些代码校验工具,它们都有什么特点?欢迎你在留言区一起分享交流。