Lerna的依赖管理及hoisting浅析
简单分析下lerna中依赖管理的部分
- 在使用lerna时,若使用yarn作为npmClient并开启workspace功能,则bootstrap命令会代理给yarn执行
- 相同的依赖通过提升(hoist)安装在根路径下,本地包之间的依赖通过软链接实现
- 虽然在依赖处理部分yarn的底层具有较好的实现,但lerna提供的上层指令则方便了开发者的使用
处理package的相同依赖
lerna bootstrap
在lerna中,执行默认的bootstrap命令会在每个package下安装各自package.json中的依赖。
该方法的问题是,当多个package中拥有多个相同且同版本的依赖时,它们会被安装多次,分别安装在包含它们的package下,造成空间的浪费,降低构建速度。为了解决这个问题可以使用hoist功能来执行依赖的安装。
lerna bootstrap --hoist
在开启hoist功能的bootstrap命令中,lerna会自动检测package中相同的依赖,并将其安装在根目录下的node_modules中,减少依赖安装次数,提升速度。
在新版的lerna(v3.19.0)中,当npmClient为yarn时加上hoist参数时执行会报错:--hoist is not supported with --npm-client=yarn, use yarn workspaces instead,此时看看接下来的方法即可解决。
[Enable workspace] yarn install
当使用yarn workspace,并在lerna中开启该功能时,lerna bootstrap命令由yarn install
代理,等价于在workspace的根目录下执行yarn install
。
相关配置:
lerna.json
{
"npmClient": "yarn",
"useWorkspaces": true,
}
package.json
{
"workspaces": [
"packages/*"
],
}
这么做是因为yarn本身提供了较lerna更好的依赖分析与hoisting的功能。yarn的hoisting算法。
默认情况下,yarn会开启hoist功能,也可以通过设置nohoist选项手动关闭。
{
"workspaces": {
"packages": [
"Packages/*",
],
"nohoist": [
"**"
]
}
}
虽然yarn提供的较好的底层依赖处理的支持,但lerna提供了更高层的更方便实用的各种命令。
本地package间的依赖
@lerna/bootstrap中有如下简介: Link local packages together and install remaining package dependencies。
上面已经介绍了install package dependencies的部分,那么还有一个更重要的作用就是处理本地package之间的相互依赖,即link local packages
lerna中使用**软链接(symlink)**来实现本地多项目之间的依赖,该功能在执行bootstrap时会自动在根目录的node_modules下创建本地package对应的软链接。
这样,就可以在某一package中通过如下命令来添加workspace中的其他package:
$ lerna add @workspace/package-a --scope @workspace/package-b
hoisting原理
lerna中的hoisting
lerna命令中有很多通用的选项,而--hoist
选项是bootstrap命令独有的。
相关代码在@lerna/bootstrap中。其中比较关键的一个函数是getDependenciesToInstall(),它做了以下工作:
- 解析需要安装的依赖,保存在depsToInstall中
- 判断是否开启hoist功能并且该依赖在packages中是否出现了多次,若不满足则向下执行
- 检测在出现的多次中最常出现的版本号(commonVersion)
- 若在根目录的依赖中也存在则比较版本号(rootVersion),若不同则发出警告
- 在根节点上安装最佳版本的依赖
- 在叶子节点上安装其他出现次数较少的版本且未安装的依赖
关键代码:
// 初始化依赖容器,在之后解析出需要安装的依赖给其赋值
const depsToInstall = new Map();
...
// 遍历依赖,执行安装
for (const [externalName, externalDependents] of depsToInstall) {
let rootVersion
// 若开启了hoist功能并且该依赖在packages中出现了多次
if (this.hoisting && isHoistedPackage(externalName, this.hoisting)) {
// 检测该依赖出现次数最多的版本
const commonVersion = Array.from(externalDependents.keys()).reduce((a, b) =>
externalDependents.get(a).size > externalDependents.get(b).size ? a : b
);
// 得到安装在根依赖中的版本,并取得它所依赖的其他包
rootVersion = rootExternalVersions.get(externalName) || commonVersion;
const dependents = Array.from(externalDependents.get(rootVersion)).map(
leafName => this.targetGraph.get(leafName).pkg
);
externalDependents.delete(rootVersion);
// 安装依赖的最佳版本
rootActions.push(() =>
hasDependencyInstalled(rootPkg, externalName, rootVersion).then(isSatisfied => {
rootSet.add({
name: externalName,
dependents,
dependency: `${externalName}@${rootVersion}`,
isSatisfied,
});
})
);
}
// 将其他出现次数较少的版本安装在原始的叶子节点下
for (const [leafVersion, leafDependents] of externalDependents) {
for (const leafName of leafDependents) {
const leafNode = this.targetGraph.get(leafName);
const leafRecord = leaves.get(leafNode) || leaves.set(leafNode, new Set()).get(leafNode);
// 安装未安装的依赖
leafActions.push(() =>
hasDependencyInstalled(leafNode.pkg, externalName, leafVersion).then(isSatisfied => {
leafRecord.add({
dependency: `${externalName}@${leafVersion}`,
isSatisfied,
});
})
);
}
}
}
yarn中的hoisting
yarn中的hoist则作为一个独立的包存在: package-hoister。
其中的PackageHoister这个类来负责依赖信息的管理,在解析依赖与处理提升方面主要进行了三步:
- 统计最常出现的版本
prepass(patterns: Array<string>) {
patterns = this.resolver.dedupePatterns(patterns).sort();
// 1. 根据传入的pattern解析出现的依赖
const occurences = {};
// 2. 解析根节点的依赖
const rootPackageNames: Set<string> = new Set();
for (const pattern of patterns) {
const pkg = this.resolver.getStrictResolvedPattern(pattern);
rootPackageNames.add(pkg.name);
add(pattern, [], []);
}
// 3. 统计最常出现的依赖版本
for (const packageName of Object.keys(occurences).sort()) {
const versionOccurences = occurences[packageName];
const versions = Object.keys(versionOccurences);
let mostOccurenceCount;
let mostOccurencePattern;
for (const version of Object.keys(versionOccurences).sort()) {
const {occurences, pattern} = versionOccurences[version];
const occurenceCount = occurences.size;
if (!mostOccurenceCount || occurenceCount > mostOccurenceCount) {
mostOccurenceCount = occurenceCount;
mostOccurencePattern = pattern;
}
}
if (mostOccurenceCount > 1) {
this._seed(mostOccurencePattern, {isDirectRequire: false});
}
}
}
-
处理有序队列
本质上处理的是一个依赖数组,之所以是有序是因为在yarn中处理依赖时,很注重依赖间的拓扑顺序,顺序的一致性是保证JS包管理上下文一致性中重要的一环,即determinism,给予相同的package.json与**.lock**文件,总会得到相同的node_modules内容。
seed(patterns: Array<string>) { this.prepass(patterns); while (true) { // 在有序队列中优先提升不包含peer dependencies的依赖 let sortedQueue = []; let queue = this.levelQueue; // 对queue进行排序,得到一个运行时的依赖顺序 queue = queue.sort(([aPattern], [bPattern]) => { return sortAlpha(aPattern, bPattern); }); const availableSet = new Set(); let hasChanged = true; // 处理依赖中的peerDependencies while (queue.length > 0 && hasChanged) { hasChanged = false; const queueCopy = queue; queue = []; for (let t = 0; t < queueCopy.length; ++t) { const peerDependencies = Object.keys(pkg.peerDependencies || {}); const areDependenciesFulfilled = peerDependencies.every(peerDependency => availableSet.has(peerDependency)); if (areDependenciesFulfilled) { // 添加至有序队列中 sortedQueue.push(queueItem); hasChanged = true; } else { queue.push(queueItem); } } } // 将包含peerDependencies的依赖添加到队尾,依次处理有序队列中的依赖 sortedQueue = sortedQueue.concat(queue); for (const [pattern, parent] of sortedQueue) { const info = this._seed(pattern, {isDirectRequire: false, parent}); if (info) { this.hoist(info); } } } }
-
更新提升后的依赖信息
for (const [pattern, parent] of sortedQueue) { const info = this._seed(pattern, {isDirectRequire: false, parent}); if (info) { this.hoist(info); } } ... hoist() { const {key: oldKey, parts: rawParts} = info; this.tree.delete(oldKey); const {parts, duplicate} = this.getNewParts(oldKey, info, rawParts.slice()); const newKey = this.implodeKey(parts); // 更新新的数据信息,比如路径或者key this.declareRename(info, rawParts, parts); this.setKey(info, newKey, parts); }
除此之外,yarn中还进行了大量其他处理,比如计算提升位置,构建依赖树,处理软链接等等。
关于yarn hoisting中存在的问题及其nohoist,可以参考nohoist in Workspaces
- 原文作者:yrq110
- 原文链接:http://yrq110.me/post/tool/how-lerna-manage-package-dependencies/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。