某次前端需求开发中,新增了一个 npm 包,在进行合码时发现 lockfile
出现冲突。
❝lockfile,即包管理工具的 lock 文件,比如
package-lock.json
、yarn.lock
、pnpm-lock.yaml
手动解冲突非常低效,又容易出错。以下是几种常用的解决方案:
方案 1 会丢失 lock 记录,通常不会选择。
那方案 2 和方案 3 可行么?需要注意什么问题? 本文将对这些问题进行讨论,并在最后给出最佳实践。
❝如果不想关注细节,也可以滑到最后直接查看「最佳实践」。
在此之前,我们先来讲讲什么情况下会出现 lockfile 合并冲突。
Git 合并出现冲突的原因在于两个分支版本对一个文件的同一区域做了修改。如果是不同区域,Git 会尝试自动合并(auto-merge,默认策略)解决冲突。
❝如果对 Git 合并冲突不熟悉,可以先看 How to Resolve Merge Conflicts in Git – A Practical Guide with Examples[1] 这篇文章
注意合并冲突的关键:同一区域发生变化。以 package.json
的依赖配置为例,下面这两个例子,第一个会冲突而第二个不会冲突。
对于 lockfile 而言,同一区域发生变化一般有以下两种情况:
package.json
依赖配置都发生了变更,并修改了 lockfile 的同一区域。什么情况下会修改 lockfile 的同一区域?
由于不同包管理工具以及不同版本的生成策略都不一样,这个问题说来话长,开发者也很难通过调整 package.json
的写法来避免冲突,因此无需过多关注,只要知道当出现这个问题的时候怎么解决即可。
下面将讨论两种常用的解决策略。
❝太长不看版:重置分支 lockfile ,即还原 lockfile 到目标分支或者当前分支中的某个版本,也意味着会丢失某个分支的 lock 记录,可能会导致错误。该问题很难彻底解决,只能通过改进开发流程和必要的人工 review 来避免。
重置分支 lockfile 指的是「合并时以目标分支或者当前分支的 lockfile 文件为准」,后面需要再重新执行依赖安装命令去更新 lockfile 。
以下三种方案可以方便的重置分支 lockfile:
git checkout --ours "*lock*"
或 git checkout --theirs "*lock*"
命令,将以当前分支或目标分支为基准自动修复 lockfile 冲突。.gitattributes
文件,配置文件的合并策略,示例:# .gitattributes
# 当 pnpm-lock.yaml 出现冲突时,将以当前分支为准
pnpm-lock.yaml merge=ours
git config merge.ours.driver true
命令,开启合并驱动配置(如果用了 theirs
的合并策略,则命令改成 merge.theirs.driver
)。无论是以当前分支还是目标分支为基准,重置分支 lockfile 再更新依赖意味着会丢失一部分 lock 记录,可能会引发错误。
举个业务中遇到过的例子:
image.pngfeat1
,feat1
中新增了依赖 A^1.0.0
,此时装的版本是 1.0.0
feat1
开发的这段时间,有另一个开发分支 feat2
先合到了主分支,并新增了依赖 B^2.0.0
,此时装的版本是 2.0.0
feat1
开发完毕,准备合码到主分支,发现 lockfile 冲突了BREAKING CHANGE
的 A@1.0.1
和 B@2.0.1
。由于「重置分支 lockfile」方案会忽略 feat1
或者 主分支(feat2
)新增的 lock 记录,导致安装了较新版本的 A@1.0.1
或 B@2.0.1
feat1
直接合到了主分支,导致线上代码报错有同学说,直接使用固定版本安装依赖不就好了?比如新增依赖时使用 A@1.0.0
和 B@2.0.0
,而不是使用 ^
这种版本范围的写法。
{
"dependencies": {
"A": "1.0.0",
"B": "2.0.0",
}
}
首先,对于应用项目来说,可以直接使用固定版本;但是对于类库项目,不推荐固定版本,有以下两点原因:
^1.0.0
和 ^1.1.0
可以合并成 ^1.1.0
)其次,锁定直接依赖的版本也不完全有效,丢失 lock 后,直接依赖的间接依赖还是会进行升级,进而导致 BREAKING CHANGE
因此,一旦选择该方案,要么就信任其他依赖不会出现问题(听天由命) ,要么就需要必要的 lockfile 人工 review ,并通过合理的开发流程来保障。
pnpm-lock.yaml
文件的 specifier
和 version
部分)的版本变更,对于直接依赖引入的间接依赖,自动升级出错的概率较小(一旦出错影响的不只一个项目),且 review 成本太高,选择信任社区,也可选择「变更复测」来保障。❝太长不看版:解析冲突文件得到不同版本的 lock 对象,再对 lock 对象进行合并。每种包管理工具的合并方案都不一样,总体来说 pnpm 的最为出色。但无论如何,有合并肯定有丢失,不能保证 100% 没问题。
lockfile 出现合并冲突,目前主流的包管理工具都支持运行依赖安装命令(npm install/yarn/pnpm install
)来自动解决冲突。
❝可以认为大部分用户使用的包管理工具的版本都是支持的
那么这些包管理工具是怎么解决冲突的呢?
我在另一篇文章 《浅谈 package-lock.json 的合并冲突修复算法[8] 》中已经分析过这个问题。
总体来说,策略是基于目标分支( theirs
),并应用上当前分支( ours
)的变更。
举个例子,在主分支
上合入开发分支
(git merge feat-branch
),theirs
指的就是开发分支
,ours
指的是主分支
,那么将基于开发分支
的 lock 记录,并应用上主分支
的变更。
也就是说,如果两个分支同时更新同一模块的版本号,则会以主分支(ours
)的版本为准,这在 极少数情况下
会出错。
文章给出的解决办法是通过流程和复测来降低影响,下文会讲。
PR 在 Auto detect and merge lockfile conflicts[9] ,看最新的实现代码 /src/lockfile/parse.js[10]
实际上只要看其中的一行代码就能明白
Object.assign({}, parse(variants[0], fileLoc), parse(variants[1], fileLoc));
浅合并两个分支的 yaml 对象,对象属性都存在则以目标分支(theirs
)的为准
换句话说,当同一依赖的版本发生冲突,会以目标分支的为准。这点与 npm 的策略相反,但存在的问题和解决方案是类似的。
pnpm 的冲突修复算法由 @pnpm/merge-lockfile-changes[11] 项目维护,
整体实现上也是先将冲突部分拆解为目标分支内容(theirs
)和当前分支内容( ours
),然后做合并。
但是这个合并不是像 yarn 那样简单粗暴的浅合并,而是做了深合并( lockfile[12] 结构其实一共也就两层),当出现版本冲突时取版本号较大的。
写个 demo 测试下
const { mergeLockfileChanges } = require("@pnpm/merge-lockfile-changes");
const simpleLockfile = {
importers: {},
lockfileVersion: 5.2,
};
const mergedLockfile = mergeLockfileChanges(
{
...simpleLockfile,
packages: {
".": {
version: "1.1.0",
dependencies: {
foo: "1.2.0",
bar: "3.0.0_qar@1.0.0",
zoo: "4.0.0_qar@1.0.0",
ktv: "5.0.0"
},
},
},
},
{
...simpleLockfile,
packages: {
".": {
version: "1.2.0",
dependencies: {
foo: "1.1.0",
bar: "4.0.0_qar@1.0.0",
zoo: "3.0.0_qar@1.0.0",
pua: "5.0.0"
},
},
},
}
);
console.log(JSON.stringify(mergedLockfile, null, 2));
输出得到
{
"importers": {},
"lockfileVersion": 5.2,
"packages": {
".": {
"version": "1.2.0",
"dependencies": {
"foo": "1.2.0",
"bar": "4.0.0_qar@1.0.0",
"zoo": "4.0.0_qar@1.0.0",
"ktv": "5.0.0",
"pua": "5.0.0"
}
}
}
}
也就是说,pnpm 选择了更新的版本,如果会出问题,则表示新版本出现了 BREAKING CHANGE
,但这个情况比起选择旧版本而出现 BREAKING CHANGE
的概率更小。
目前,pnpm 官方还在持续优化 lockfile 方案以减少冲突,包括:
此外,pnpm 还提供了一个 resolution-mode[13] 配置,用户可以通过配置来决定依赖安装时版本的选择策略:最低(默认)、最高、time-base(与最后一个直接依赖有关)。
三种方案都选择对 lockfile 进行合并,但在合并的时候策略又不相同:
ours
)的为准theirs
)的为准yarn 虽是第一个提出解决 lockfile 冲突的,但过去这么久了方案没咋更新。。
npm 的理念是版本合并应该尽量以主分支的为准,更稳定。
pnpm 的理念是更信任社区 semver
,选择新版本出现的问题会比旧版本更少。
整体来说,pnpm 方案出现问题的概率最小,但也不是一定不会出现问题,正如官方文档[14]所说:
❝建议您提交之前查看更改,因为我们无法保证 pnpm 会选择正确的头(head) - 它会构建大部分更新的锁文件,这在大多数情况下是理想的。
包管理自带机制相比重置分支 lockfile,丢失的 lock 信息更少,出现的问题也更少。
总结一下上文,我们得到如下最佳实践:
pnpm
此外,包管理工具方案不是一劳永逸,极端情况也可能出现问题。如果项目有这个价值,最好还是做下人工 review 和需求复测,具体行为指南可以参考「方案分析:重置分支 lockfile」一节中的解决方案。
本文系统分析了 lockfile 冲突的常见方案,并在最后提供了一份最佳实践。
前端底层很多设计都是在修修改改,或许需要拓宽视野,上升软件层面的最佳实践,再反哺社区。
How to Resolve Merge Conflicts in Git – A Practical Guide with Examples: https://www.freecodecamp.org/news/resolve-merge-conflicts-in-git-a-practical-guide/
[2]为什么保证前端依赖一致这么难? - 掘金: https://juejin.cn/post/7250383386183876645
[3]Have Git Select Local Version On Merge Conflict on a Specific File?: http://stackoverflow.com/a/930495/958481
[4]Merge Strategies: https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes#Merge-Strategies:~:text=further%20development%20work.-,Merge%20Strategies,-You%20can%20also
[5]Resolving lockfile conflicts: https://docs.npmjs.com/cli/v6/configuring-npm/package-locks#resolving-lockfile-conflicts
[6]Auto-merging of lockfiles: https://engineering.fb.com/2017/09/07/web/announcing-yarn-1-0/#:~:text=Auto%2Dmerging%20of%20lockfiles
[7]Merge conflicts: https://pnpm.io/git#merge-conflicts
[8]浅谈 package-lock.json 的合并冲突修复算法: https://juejin.cn/post/7251895470548697143
[9]Auto detect and merge lockfile conflicts: https://github.com/yarnpkg/yarn/pull/3544
[10]/src/lockfile/parse.js: https://github.com/yarnpkg/yarn/blob/master/src/lockfile/parse.js#L334
[11]@pnpm/merge-lockfile-changes: https://github.com/pnpm/pnpm/tree/main/lockfile/merge-lockfile-changes
[12]lockfile: https://github.com/pnpm/spec/blob/master/lockfile/6.0.md
[13]resolution-mode: https://pnpm.io/npmrc#resolution-mode
[14]官方文档: https://pnpm.io/zh/git#%E5%90%88%E5%B9%B6%E5%86%B2%E7%AA%81
[15]Avoid lockfile conflicts in Rush: https://7tonshark.com/posts/avoid-conflicts-in-pnpm-lock/
[16]一次yarn.lock的conflict引发的思考 - 掘金: https://juejin.cn/post/6953948250671939591
- END -
如果您关注前端+AI 相关领域可以扫码进群交流
扫码进群2或添加小编微信进群1😊
奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。