23

在开发若干个有相互依赖关系的库的时候,通常都会采用 symlink 的方式互相引用,比较典型的一种场景就是使用 lerna 开发多个 package 。

lerna 简介

lerna 是用于管理拥有多个 package 的 JavaScript 项目,其典型目录结构为

lerna-repo/
  packages/
    package1/
      package.json
    package2/
      package.json
  package.json
  lerna.json

packages 目录下面就是各个 package 了。

lerna 有两个比较常用的命令:

lerna clean
lerna bootstrap

lerna clean 用于清理 packages ,会删掉各个 package 下面的 node_modules 目录。

lerna bootstrap 用于处理各个 package 的依赖,处理步骤为:

  1. 在每个 package 下面执行 npm install

  2. 根据各个 package 下 package.json 里面的 dependencies 和 devDependencies 配置,使用 symlink 在各个 package 的 node_modules 下面建立引用关系。

  3. 在每个 package 下执行 npm run prepublish

  4. 在每个 package 下执行 npm run prepare

symlink 的问题

假设 package 下面有一个包 pkg1 ,依赖 package 下面的另一个包 pkg2

运行 lerna bootstrap 之后, pkg1/node_modules 下就会出现 pkg2 的 symlink 。

如果使用 webpack 系列工具来编译运行 pkg1 ,由于 webpack loader 判断路径默认是按照真实路径来的,所以 pkg2 对应到的路径是 [project root]/package/pkg2 ,而不是 [project root]/package/pkg1/node_modules/pkg2

这样一来,如果需要 pkg2 中的源码过 pkg1 的 loader (比如 pkg2 中的 ES6 代码过 pkg1babel-loader),就需要在 webpack 相应 loader 配置中加上这个特殊的路径匹配,这和不涉及 symlink真实场景存在较大差异。

同时,很多配置(比如 postcssrcbabelrceslintrc 等)是以 resolve 到的文件去解析的,比如要用 babel 编译 pkg2 下面的 [project root]/package/pkg2/src/Dialog.es6 源码,会按照如下目录顺序查找 babelrc 配置:

[project root]/package/pkg2/src/
[project root]/package/pkg2/
[project root]/package/
[project root]/
...

而此时很可能希望能在 [project root]/package/pkg1/ 目录下寻找 babelrc 配置。

所以此时其实很希望 webpack loader 基于 symlink 的路径去解析判断 include / exclude 等配置,而不是按照真实文件的路径。

resolve.symlinks

webpack 提供了 resolve.symlinks 来解决这个问题,具体参见官方文档

新的问题

虽然使用 symlink 解决了基准路径的问题,但是还存在另外的问题。

如果 pkg2 依赖了 babel-runtime ,那么在 pkg1 的配置中就要注意不要让 babel-runtimebabel-loader 了,不然 babel 可能会在 babel-runtime 的源码里面插入一些 ES6 的代码。

如果 pkg1pkg2 同时依赖了第三方模块 externalPkg3 ,那么在 lerna bootstrap 之后,会存在两个 externalPkg3

[project root]/package/pkg1/node_modules/externalPkg3
[project root]/package/pkg1/node_modules/pkg2/node_modules/externalPkg3 -> [project root]/package/pkg2/node_modules/externalPkg3

externalPkg3 里面有个 module 提供了全局的 object :

const obj = {};

export function register(name, value) {
    obj[name] = value;
}

export function getValue(name) {
    return obj[name];
}

此时 pkg1pkg2 会用各自的 obj 对象,如果 pkg1 中想用 pkg2 注册进去的 value ,就会拿不到。

可以考虑在 lerna.json 中配置 commands.bootstrap.ignore["pkg2"] ,在 lerna bootstrap 的时候不安装 pkg2 的依赖,使得最终只会有一个 externalPkg3

[project root]/package/pkg1/node_modules/externalPkg3

这种方式肯定不会是万能的,具体怎么做还要看真正的场景,可能还得各种配置互相配合才能解决问题。


yibuyisheng
480 声望3 粉丝

前端工程师