简单分析下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(),它做了以下工作:

  1. 解析需要安装的依赖,保存在depsToInstall
  2. 判断是否开启hoist功能并且该依赖在packages中是否出现了多次,若不满足则向下执行
  3. 检测在出现的多次中最常出现的版本号(commonVersion)
  4. 若在根目录的依赖中也存在则比较版本号(rootVersion),若不同则发出警告
  5. 在根节点上安装最佳版本的依赖
  6. 在叶子节点上安装其他出现次数较少的版本且未安装的依赖

关键代码:

// 初始化依赖容器,在之后解析出需要安装的依赖给其赋值
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这个类来负责依赖信息的管理,在解析依赖与处理提升方面主要进行了三步:

  1. 统计最常出现的版本
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});
    }
  }
}
  1. 处理有序队列

    本质上处理的是一个依赖数组,之所以是有序是因为在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); }
        }
      }
    }
    
  2. 更新提升后的依赖信息

    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