随着前端工程规模的增长,各类第三方与自有依赖包的关系也日趋复杂。这时候可能产生什么问题,又该如何解决呢?这里分享咱们前端团队的一些实践。前端
安装依赖包,对于前端开发者来讲不过就是一句 npm install xxx
的事。那么,单纯靠这种方式给一个项目安装了不少依赖,就算是复杂的依赖关系吗?这里咱们这样定义「复杂」:node
若是纯粹只靠 npm install
,那么全部的包都必须发布到 NPM 以后才能被其余的包更新。在「联调」这些包的时候,每次稍有更改都走一遍正式的发布流程,无疑是很是繁琐而影响效率的。咱们有什么现成的工具来解决这个问题呢?git
提到管理多个包之间的依赖关系,不少同窗应该能立刻想到很多现成的工具,好比:github
这里的「万恶之源」就是 npm link
命令了。虽然熟悉它的同窗多半知道它有很多问题,但它确实能解决基本的连接问题。快速复习一下使用方式:假设你维护的下游业务项目叫作 app,上游的依赖叫作 dep,那么要想作到「dep 一改动,app 就能同步更新」,只须要这样:npm
# 1. 在 dep 所在路径执行
npm link
# 2. 在 app 所在路径执行
npm link dep
复制代码
这样就造成了 app 与 dep 之间基本的「连接」关系。只要进入 app 的 node_modules 查看一下,不难发现 NPM 其实就是替你创建了一个操做系统的「快捷方式」(软连接)跳到 dep 下而已。在存在多个互相依赖的包的时候,手动维护这个连接关系很是麻烦并且容易出错,这时候你能够用社区的 yarn workspace 或 Lerna 来自动帮你管理这些包。因为这两者至关接近,在此咱们只介绍在咱们生产环境下使用的 Lerna 工具。json
Lerna 的使用也是很是傻瓜的,你只需按下面的风格把各个依赖包放在同一个目录下就行,无需对它们具体的构建配置作任何改动:bootstrap
my-lerna-repo/
package.json
packages/
dep-1/
package.json
dep-2/
package.json
dep-3/
package.json
...
复制代码
而后一句 lerna bootstrap
就可以自动处理好它们之间的依赖关系了——这里每一个包的 package.json
均可以放心地写上其它包的名字了(注意这里依据的是 package.json
中的 name 字段,而非目录名)。这样,你能够放心地把这些包放置在同一个 Git 仓库里管理,而不用担忧繁琐的初始化过程了——如今的 Babel 和 React 就是这么干的。bash
固然了,实际的场景并非有了现成的命令或者工具就万事大吉了。下面总结一些实践中的依赖管理经验吧:app
在刚开始使用 Lerna 这样的依赖管理工具时,一些同窗可能会倾向于把依赖拆分得很是零散。这时是有可能出现循环依赖的情形的——A 包依赖了 B,而 B 包又依赖了 A。怎么会出现这种状况呢?举一个例子:编辑器
这时候就出现了循环依赖。虽然 NPM 支持这种场景下的依赖安装,可是它的出现会让依赖关系变得难以理解,所以咱们但愿尽可能作到直接避免它。这里的好消息是,循环依赖多数都和不太符合直觉的需求有关,在上面的例子里,做为上游的 editor 包去依赖了下游的 editor-ui 包,这能够在方案评审时就明确指出,并只需改成在 editor-ui 包中展现 Demo 页便可——若是出现了循环依赖,大胆地运用「这个需求不合理」的否决权吧。
咱们已经提到,lerna boostrap
可以正确地完成多个包的依赖安装和连接操做。但这是否意味着一个装载了多个包的 Lerna 仓库,只要这条命令就可以让这些包都正常地跑起来呢?这里存在一点细节须要注意。
若是你管理的多个包先是配置了各自的构建和发布命令,而后才经过 Lerna 合并到一块儿的话,可能出现这样的问题:它们在 package.main
字段下指定的入口都是形如 dist/index.js
下的构建后文件,但相应的产物代码在如今通常是不提交到 Git 的。这时候拉下全新的代码想要跑起来时,即使工具正确地处理了连接关系,仍然有可能出现某个子包没法打包成功的状况——这时,就去被依赖的包目录下手动 npm run build
一次了。固然,在这种状况下,更新了一个包的源码后,也须要对这个包作一次 build 操做生成产物后,其它的包才能同步。虽然这并无多少理解上的困难,但每每形成一些没必要要的困扰,故而在此特意说起。
在真实场景中,依赖其实并不能彻底经过 Lerna 等工具管理,而是存在着上下游的区分的。这是什么概念呢?以下图:
通常来讲,上游的基础库(如 Vue / Lodash 等)并不适合直接导入自有的宏仓库中维护,而下游的具体业务项目多数也是与这些自有依赖独立的,它们一样在 Lerna 工具的控制范围以外。这时,咱们仍然须要回到基本的 npm link
命令来创建本地的连接关系。但这可能会带来更多的问题。例如,假设你在 Lerna 中管理 editor 与 editor-ui 两个依赖,而业务项目 app 依赖了它们,这时候你不难把 editor 与 editor-ui 都 link 到 app 下。但这时的连接关系很容易被破坏,考虑下面的工做流:
npm publish
了 editor 与 editor-ui 的新版本。npm install editor editor-ui
并提交相应的改动。Boom!执行了最后一步后,不光 app 与 editor 之间的连接关系会被破坏,editor 与 editor-ui 之间的连接关系也会被破坏。这就是软连接的坏处了:下游的变动也会影响上游。这时,你须要从新作一次 lerna bootstrap
与 npm link
才能把这些依赖关系从新创建好,对于频繁迭代的业务项目来讲,这是至关棘手的。对这个问题,咱们提出的变通方案包括两部分:
npm link
。前者听起来麻烦,但实际上只须要把 app 目录复制一份便可。假设复制后获得了 app-deps 目录,那么:
npm install editor
便可。这不会 app 项目中破坏原有的连接关系。固然,这时候 app 与 app-deps 之间的依赖可能不彻底同步——这个问题只要有 pull 代码的习惯就能解决。另外的一种问题情形在于,若是下游的业务项目采用了 CNPM 等非 NPM 的包管理器来安装依赖,那么这时候原生的 link 命令容易失败。仍是套用前面的例子,这时候咱们能够在 editor 项目中创建 link 命令,来替代 npm link
:
// link.js
const path = require('path');
const { exec } = require('./utils'); // 建议将 childProcess.exec 封装为 Promise
const target = process.argv[2];
console.log('Begin linking……');
if(!target) {
console.warn('Invalid link target');
return;
}
const baseDir = path.join(__dirname, '../');
// 区分相对路径与绝对路径
const targetDepsDir = target[0] === '/'
? path.join(target, 'node_modules/my-editor')
: path.join(__dirname, '../', target, 'node_modules/my-editor');
console.log(`${baseDir} → ${targetDepsDir}`);
exec(`rm -rf ${targetDepsDir} && ln -s ${baseDir} ${targetDepsDir}`)
.then(() => {
console.log('🌈 Link done!');
})
.catch(err => {
console.error(err);
process.exit(1);
});
复制代码
这样只要在 editor 的 package.json
中增长一条 "link": "node ./link.js"
配置,就能经过 npm link path/to/app
的形式来完成连接了。这个连接操做跳过了很多中间步骤,所以比 NPM 原生的 link 速度要高得多,也能适配 CNPM 安装的业务项目。
对于「自有依赖 → 下游业务」的情形,这两个方式基本能保证开发节奏的顺畅。但还有一个问题,就是「上游依赖 → 自有依赖」的时候,仍然可能须要折腾。这对应于什么状况呢?
通常来讲,最上游的基础库应当是至关稳定的。可是你一样可能须要修改甚至维护这样的基础库。好比,咱们的 editor 编辑器依赖了咱们开源的历史状态管理库 StateShot,这时候就须要本地连接 StateShot 到 editor 中了。
这个场景不能继续前面的 npm link
套路吗?固然能够,不过上游的基础库并不须要频繁的迭代来同步时,咱们建议使用 npm pack
命令来替代 link,以保证依赖结构的稳定性。如何使用这个命令呢?只须要这样:
npm pack
。base.tgz
以后,在 Lerna 管理的 editor 包下运行 npm install path/to/base.tgz
。lerna bootstrap
保证连接关系正确。pack 的好处在于避开了软连接的坑,还能更真实地模拟一个包从发布到安装的流程,这对于保证发布的包可以正常安装使用来讲,是颇有用的。
前端的工程化还在演化之中,从最简单的 npm install
到各色命令与工具,相信将来的趋势必定是可以让咱们更加省心地维护好更大规模的项目,也但愿文中的一些实践可以对前端同窗有所帮助。