项目工程之断舍离
TIP
此文档编写于 2022 年 4 月,结合当下前端领域的发展和编辑器的项目现状给出的一些建议。
由于编辑器项目过于庞大,本文提出的建议不必马上实施,但应该在团队内达成共识,目标一致。
代码即产品
我们平常讨论的产品一般是用户最终接触到的网页、app、客户端工具等。但从程序员的角度来看,代码项目
才是我们的第一手产品。而网页、app、客户端工具只是我们的代码项目
的构建产物。
我们提供良好的构建产物的前提是维护好自己的代码项目
。
所以我们今天不讨论如何做好产品(广义),而是讨论如何维护好一份代码。
基本原则
当我们在讨论最佳实践时,有必要对一些基本原则达成共识。我假定如下原则是我们大家都认可的:
- 遵循标准: 如果相关规范已经表明某项技术就是未来的标准,那么我们应该毫不犹豫的往标准靠拢。
- 保持更新: 如今的前端工程已经是一个庞大的依赖体系,保持更新能让代码保持最佳性能,也不会给后期带来升级灾难。
- 跟上时代: 新技术往往带着更好的解决方案和处理性能,所以跟上时代也很有必要,比如构建从 grunt -> gulp -> wabpack -> vite 的一路更新。
最佳实践
基于以上原则,我们拟定编辑器项目的几个最佳实践:
1、保持跟踪 node 生态的升级。
对于前端项目,node 和 npm 是我们的依赖根基,保持对它们的更新,才能让我们顺利的跟进其他上层的更新。而且随着升级带来的新功能,我们可以抛弃更多的第三方依赖。
比如 npm7 带来了 workspace 功能,我们就可以用它来管理monorepo
的项目,从此抛弃 yarn。(我对于 yarn 最大的需求就是 workspace )
比如我们有一份读取 json 文件的代码:
node@v14 之前:
import { readFile } from 'fs';
readFile('./package.json', (err, data) => {
if (!err) {
const packageJson = JSON.parse(data);
}
});
node@v14:
import { promises as fs } from 'node:fs';
const packageJson = JSON.parse(await fs.readFile('package.json'));
node@v18:
import packageJson from './package.json' assert { type: 'json' };
以上的业务场景,只是由于我们是使用的 node 版本不同,优雅程度就完全不同。写出简洁、易维护的代码,不需要引入第三方库,只需要及时升级 node 版本即可。
及时更新的意思是,我们应该按照 12 -> 14 -> 16 -> 18 这样步骤,追随官方的发布来更新。避免出现跨版本更新的情况。跨版本会带来更多的破坏性变更,给升级带来麻烦。小步、及时的更新会让项目稳定性更佳。
2、遵循标准
对于一个需要长期维护,生命周期往 10 年以上的项目来说,及早的遵循标准至关重要。这样可以避免历史债务的堆积。
回归到编辑器项目来说就是我们应该整体切换到 ESM 标准。 require 终将成为历史,而且会成为历史负债。我们应该逐渐的将之前使用 require 的地方改为 esm。这是一份使用 pure ESM 的重构指南。
由于 Electron 自身的原因,暂时不支持 ESM ,可以时刻保持关注,只要官方支持,及时着手升级准备。技术栈的统一可以共享基础配置,维护不割裂。
关于 Electron 支持 ESM 的一些调研可供参考:
3、克制依赖
npm 带来了繁荣的生态,但是任何东西都是有代价的。如果我们不克制 npm 包的依赖,那么我们的包体就会无序膨胀。所以团队成员在引入新依赖的同时,一定要再三思考,引入成本是否大于带来的收益 。甚至可以约定,项目的依赖包,需要由项目 leader 决定是否可以增加。
非必要不引入新依赖
举例:ui-kit 项目引入了vm-browserify 包,只是为了实现计算字符串 '1 + 2'
import vm from 'vm';
// 支持表达式,如 1 + 2
const sandbox = { value: defaultConfig.errorValue };
try {
vm.runInNewContext(`value = ${inputValue};`, sandbox);
} catch (e) {}
let value = sandbox.value;
像这种整个大项目引入一个 npm 包,只是为了解决这样的一个小需求,我们就要考虑它的必要性。因为它是可以被相对简单的实现的。
// 支持表达式,如 1 + 2
let value = new Function(`return ${inputValue}`)() ?? defaultConfig.errorValue;
及时删除旧依赖
举例:在使用旧版的 node 时,fs 模块没有 promise 的 api,为了代码的简洁我们都会引入 fs-extra 。但是随着 node 的升级,fs 模块已经内置了 promise 的 api,我们就应该及时考虑去除 fs-extra
了。
及时升级 node,就可以使用上更多的原生的功能,减少外部的依赖。 一份断舍离式的 dependencies
清单可以减少维护者的心理负担。
4、积极升级 vue 等框架依赖
为什么用积极
而非及时
,因为由于业务代码量一般都比较大,升级框架需要的工作量是需要慎重评估的。但是我们它仍然需要我们积极的面对,不能有锁死
版本的念头。
现在是 2022 年 4 月,vue 3 已经成为官方的默认版本,且围绕着 vue 3 的周边生态同样是全部 ready 的状态。升级 vue 3 就是需要被提上日程。越早升级,就会尽量减少犹豫期产生的更多的 vue 2 代码。也能及时的用上新版的特性,享受到新版本带来的性能提升和可维护性增强。
像 eslint 、husky 等依赖,都建议时刻保持更新,我们可以约定每次发布大版本,都检查一下 package.json 里声明的依赖列表,如果有更新的 npm 包,我们都及时做对应的升级。
这样断舍离式的依赖清单,加上强迫症式的版本升级,能让我们的项目永远处于最健康的状态
打包构建
我们前面列举了 4 项最佳实践,可以通过打包构建
这个工作流环节来统一体现。
由于我们编辑器最终会汇集所有项目(**-extension,ui-kit)等,所以我们的依赖有极大可能是会重复的,所以将依赖声明为 peerDependencies 并且在构建配置里排出 node_module 的包体比如 webpack 的 externals。而且从 npm@7 开始, npm install 能够自动处理 peerDependencies 的依赖项。这样我们可以最大程度的降低 bundle 的体积。
符合 esm 规范的 bundle,能享受到 tree Shaking 带来的进一步体积优化。
所以我们应该:
- 编辑器周边的 extension 和 tools 都以 esm 的规范导出
- 统一升级 node 到最新的 LTS 版本(包括 npm)
- 统一构建流程,弃用 tsc ,以享受 tree shaking 带来的体积优化
- 构建目标设置为当前 electron 对应的 chrome 版本。最大程度的减少不必要的编译&转换。
- 可以的话,统一升级 vue 到最新版本。
结尾
只要按照最佳实践的方式去维护项目,我们的项目不管迭代多久,都能“与时俱进”,保持最佳的可维护状态。