乐趣区

关于前端:workspaces-monorepo实战

前言

npm 自从 v7 开始,引入了一个非常弱小的性能,那就是 workspaces。另外,yarn 和 pnpm 也领有workspaces 的能力。不过,从用法上来说,简直是截然不同的。所以,学会了 npm workspaces 的话,自然而然也就学会了 yarn 和 pnpm 的了。

概览

本文会分四个局部进行介绍:

  1. 什么是 workspaces;
  2. 多包治理;
  3. 多项目管理;
  4. 避坑;
  5. 总结;

什么是 workspaces?

顾名思义,workspaces 就是多空间的概念,在 npm 中能够了解为多包。它的初衷是为了用来进行多包治理的,它能够让多个 npm 包在同一个我的项目中进行开发和治理变得十分不便:

  • 它会将子包中所有的依赖包都晋升到根目录中进行装置,晋升包装置的速度;
  • 它初始化后会主动将子包之间的依赖进行关联(软链接);
  • 因为同一个我的项目的关系,从而能够让各个子包共享一些流程,比方:eslint、stylelint、git hooks、publish flow 等;

这个设计模式最后来自于 Lerna,但 Lerna 对于多包治理,有着更强的能力,而且最新版的 Lerna 能够齐全兼容 npm 或 yarn 的 workspaces 模式。不过因为本文讲的是 workspaces,所以,对于 Lerna 有趣味的同学,能够自行去 Lerna 官网学习。

多包治理

多包治理下面曾经说过它绝对单包独自治理的益处。所以,咱们通过实例的例子来让同学们感受一下 workspaces 为什么被我吹的这么牛批。

例子演示

我的项目地址我挂在 github 上了,有趣味的同学能够自行查看源码。

1. 降级 npm 到 7 或最新版

npm i -g npm@latest

2. 创立我的项目

mkdir demo-workspaces-multi-packages

3. 初始化我的项目

npm init -y
.
└── package.json

4. 申明本我的项目是 workspaces 模式

package.json新增配置:

"private":"true",
"workspaces": ["packages/*"],

这里的 packages/* 示意咱们的子包都在 packages 文件夹下。(对于 workspaces 的细节和更多用法本文不会一一介绍,文档十分分明,本文考究实战)

5. 初始化子包m1

创立子包m1

npm init -w packages/m1 -y
.
├── package.json
└── packages
    └── m1
        └── package.json

创立 m1 的主文件index.js

echo "exports.name ='kitty'" >> packages/m1/index.js
.
├── package.json
└── packages
    └── m1
        ├── index.js
        └── package.json

6. 初始化子包m2

同样的形式,创立子包m2

npm init -w packages/m2 -y
.
├── package.json
└── packages
    ├── m1
    │   ├── index.js
    │   └── package.json
    └── m2
        └── package.json

创立 m2 的主文件index.js

echo "const {name} = require('m1')\nexports.name = name" >> packages/m2/index.js
.
├── package.json
└── packages
    ├── m1
    │   ├── index.js
    │   └── package.json
    └── m2
        ├── index.js
        └── package.json

因为这里 require('m1'),所以须要增加m1 依赖到 m2package.json中:

npm i -S m1 --workspace=m2

7. 初始化子包demo

为了不便咱们看到成果,再创立一个 demo 文件夹(多包治理举荐搞个 demo 子包进行整体成果测试):

npm init -w packages/demo -y
echo "const {name} = require('m2')\nconsole.log(name)" >> packages/demo/index.js
.
├── package.json
└── packages
    ├── demo
    │   ├── index.js
    │   └── package.json
    ├── m1
    │   ├── index.js
    │   └── package.json
    └── m2
        ├── index.js
        └── package.json

额定的,这个 demo 包,咱们并不像他进行公布,为了避免不小心公布,咱们在 demopackage.json中新增:

"private":"true",

因为这里 require('m2'),所以须要增加m2 依赖到 demopackage.json中:

npm i -S m2 --workspace=demo

咱们看看这时候我的项目根目录的 node_modules 吧:
<img width=”300px” src=”https://user-images.githubusercontent.com/17001245/148692468-e94beeca-3205-40f9-9e2b-b4c145a2f4a4.png”>
是不是很有意思?全是软链接,链接的指向就是 packages 文件夹下的各子包。

OK,搞了半天,咱们运行 demo 看下成果吧:

node packages/demo/index.js
# 输入:kitty

通过下面的例子,咱们能够看出,workspaces对于本地子包之间的依赖解决的十分奇妙,也让开发者更加不便,尤其是多人开发的时候。另一个人在拉取完我的项目当前,只须要运行npm install,即可进行开发,软链接会主动建设好。

接下来,咱们看 workspaces 我的项目中如果装置三方包的状况。

8. 装置两个不同版本的包

npm i -S vue@2 --workspace=m1
npm i -S vue@3 --workspace=m2

例子中,咱们想看看,因为咱们的包都会被晋升到根目录进行装置,那么不同版本的 vue 它会怎么解决呢?难道只会装置 vue3 的包吗?

后果:
<img width=”300px” src=”https://user-images.githubusercontent.com/17001245/148693126-3426d7b8-a011-4634-87e4-e1e52e5c798b.png”>
这样,咱们就无需放心版本抵触的问题了,workspaces显然曾经很好地解决了。

重点参数--workspace

workspaces 我的项目中,一个很外围的参数就是 --workspace,因为从前文的安装包到子包的命令能够发现,和传统的安装包一样,都是应用npm i -S 包名 或者npm i -D 包名,不同的仅仅是开端加了--workspace

那是不是对于其它的命令,比方 runversionpublish 等也是样的应用形式呢?答案是:Yes!

另外,如果咱们子包的 package.jsonscprits全都有一个叫 test 的命令,咱们想一次性运行所有子包的这个命令,能够应用 npm run test --workspaces 即可。
这样的话,对于咱们的 Lint 校验 或是 单测 都是十分不便的。

到此,workspaces 在多包治理中启到的作用就根本介绍完了。值得一提的是,多包治理,理论我的项目中还是举荐应用Lerna,它对于版本依赖主动降级、发包提醒、主动生成 Log(Change Log / Release Note)、CI 等都具备一套非常成熟的流程机制了。

多项目管理

目前的 npmworkspaces,集体认为是非常适合用来做多我的项目的整合(Monorepo)治理的。

例子演示

我的项目地址我挂在 github 上了,有趣味的同学能够自行查看源码。

1. 创立我的项目

mkdir demo-workspaces-multi-project

2. 初始化我的项目

npm init -y
.
└── package.json

3. 申明本我的项目是 workspaces 模式

package.json新增配置:

"private":"true",
"workspaces": ["projects/*"],

4. 初始化子项目zoo

创立子项目zoo

npm init -w projects/zoo -y
.
├── package.json
└── packages
    └── zoo
        └── package.json

创立模板文件index.html,主内容为:

<!-- projects/zoo/index.html -->
<body>
  <h1>Welcome to Zoo!</h1>
  <div id="app"></div>
</body>

创立我的项目入口 js 文件index.js,内容为:

console.log('Zoo')

装置我的项目构建依赖包:

npm i -S webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-merge --workspace=zoo

# projects/zoo/package.json
"private":"true",
"dependencies": {
  "html-webpack-plugin": "^5.5.0",
  "webpack": "^5.65.0",
  "webpack-cli": "^4.9.1",
  "webpack-dev-server": "^4.7.2"
}

创立 webpack 配置:

// projects/zoo/webpack/base.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

function resolve(dir) {return path.join(__dirname, '../' + dir)
}

exports.config = {entry: resolve('src/index.js'),

  plugins: [
    new HtmlWebpackPlugin({
      title: 'Zoo',
      filename: 'index.html',
      template: resolve('src/index.html')
    })
  ],
}

exports.resolve = resolve
// projects/zoo/webpack/dev.config.js
const {config, resolve} = require('./base.config')
const {merge} = require('webpack-merge')

exports.default = merge(config, {
  mode: 'development',

  output: {filename: 'bundle.js',},
})
// projects/zoo/webpack/prod.config.js
const {config, resolve} = require('./base.config')
const {merge} = require('webpack-merge')

exports.default = merge(config, {
  mode: 'production',

  output: {filename: 'bundle.js',},
})

zoo 下的 package.json 新增命令:

"scripts": {
  "dev": "webpack-dev-server --config webpack/dev.config.js --open",
  "prod": "webpack --config webpack/prod.config.js"
},

接下来就能够运行了,只须要在我的项目根目录应用:

npm run dev --workspace=zoo

即可进行本地开发。

成果:
<img width=”200px” src=”https://user-images.githubusercontent.com/17001245/148756703-5d1a71e7-ecf3-4d56-947d-2a8ad2f2c4d1.png”>

运行 prod 同理。

5. 初始化子项目shop

创立子项目shop

npm init -w projects/shop -y

其余步骤同初始化子项目 zoo 简直截然不同,所以不再赘述。

最初的目录构造:
<img width=”150px” src=”https://user-images.githubusercontent.com/17001245/148758564-84a086c0-caf1-4042-b6e7-d0e232787490.png”>

共享

对于 Monorepo,共享是最重要的一个劣势。所以,咱们来做一些共享的事件。

1. 在根目录创立 share 文件夹,作为共享资源目录,并创立共享文件Fish.js

mkdir share
mkdir share/js
touch share/js/Fish.js
// share/js/Fish.js
class Fish {constructor(name, age) {
    this.name = name
    this.age = age
  }

  swim() {console.log('swim~')
  }

  print() {return '🐟'}
}

module.exports = Fish

2. 子项目中的 webpack 配置新增alias

子项目 zooshop都加上雷同的 alias 即可:

resolve: {extensions: ['.js'],
  alias: {'$share': resolve('../../share'),
  },
},

子项目 zoo 的入口文件改为:

// projects/zoo/src/index.js
const Fish = require('$share/js/Fish')
const fish = new Fish()
document.getElementById('app').textContent = fish.print()

运行 zoodev看成果:
<img width=”150px” src=”https://user-images.githubusercontent.com/17001245/148770309-3fe66464-6b1e-4780-948b-6873ee4cab5e.png”>

批改子项目 shop 的入口文件后,会呈现同样的成果。

也就是说,share 文件夹下的货色,zooshop 能够专用了,须要做的仅仅是新增一个 webpack 的 alias 而已!🎉

🤔思考 —— 咱们为什么应用 workspaces 做汇合我的项目,用传统形式不行吗?

传统形式:

  1. 各个子项目都汇合到一个我的项目中来。和上文不同的是,package.json只有一份,在根目录,所有我的项目中的 npm 包都装置到根目录,在根目录的 package.json 中定义 开发 部署 子项目的命令;
  2. 各个子项目都汇合到一个我的项目中来。和上文不同的是,尽管根目录和各个子包都各自有一份package.json,但根底的构建工具在根目录进行装置,比方下面提到的webpackwebpack-cliwebpack-dev-serverhtml-webpack-pluginwebpack-merge,全都在根目录进行装置,和业务相干的 npm 包都装置到各自子项目中;
  3. 各个子项目都汇合到一个我的项目中来。和上文不同的是,各个子包都各自有一份package.json,根目录无package.json

形式 1 —— 毛病:

  • 命令凌乱;
  • 无奈应答子项目之间存在 npm 包抵触的问题;(比方,A 我的项目想用 webpack4,B 我的项目想用 webpack5;或者 A 我的项目想用 Vue2,而 B 我的项目想用 Vue3)

形式 2 —— 毛病:

  • 如果子项目有雷同的包,不得不在各个子项目中反复装置;
  • 同样无奈应答子项目之间存在 npm 包抵触的问题;(比方,A 我的项目想用 webpack4,B 我的项目想用 webpack5)
  • 如果某天想把 B 我的项目移除,老本很高;

形式 3 —— 毛病:

  • 如果子项目有雷同的包,不得不在各个子项目中反复装置;

那应用 workspaces 就很好的解决了下面的所有问题!

另外,对于曾经存在的我的项目而言,比方我往年所接手的我的项目,一个是 Web 的,一个是 Wap 的,而后发现,因为他们属于同一个业务,所以有大量的代码能够复用,又因为只波及这两个我的项目而已,把公共代码做成 npm 包又有点太杀鸡用牛刀,所以,过来始终采纳的是复制、粘贴的模式。这显然是十分低效的。另外就是,mock 服务也是个字我的项目独自一套,然而大多数接口的数据都是能够专用的,只是 url 前缀不同。最离谱的就是几百个银行图标都截然不同。所以,我打算将它俩合并成一个我的项目。而 workspaces 对于我来说,是一个对原我的项目改变量最小的计划。

怎么独自部署?

咱们想要在构建机上只部署我的项目zoo,应该怎么做?

1. 装置依赖包

npm install --production --workspace=zoo 

这样的话,构建机上就只会装置 zoo 我的项目下的依赖包了。

2. 构建

npm run prod --workspace=zoo 

这样的话,就构建胜利了!

避坑

npm 的 workspaces 其实有暗藏的坑,所以我也列举下。

坑一:npm install 默认模式的坑

npm v7 开始,install 会默认装置依赖包中的 peerDependencies 申明的包。新我的项目可能影响不大,然而,如果你是革新现有的我的项目。因为用了对立治理的形式,所以个别都会把子我的项目中的 lock 文件删掉,在根目录用对立的 lock 治理。而后,当你这么做了当前,可能坑爹的事件就呈现了。
场景:我的子项目中用的是 webpack4,而后,咱们的构建相干的工具(webpack、babel、postcss、eslint、stylint 等)都会封装到根底包中。这些包的依赖包中有一个包,在package.json 申明中应用这样写:

"peerDependencies": {"webpack": "^5.1.0"},

而后,在根目录中 npm install,而后再跑子项目发现我的项目跑不起来了。起因就是,我的项目竟然装置的是webpack5 的版本!

解决方案

  • 计划 1:在子项目的 package.json 中显示申明用的 webpack 版本;
  • 计划 2:去 github 和作者磋商修复依赖包,如果他的包即兼容 webpack4 也兼容webpack5,应该写成,把申明改为: "webpack": "^4.0.0 || ^5.0.0"
  • 计划 3:npm install --legacy-peer-deps

集体真的感觉这是 npm 作者脑袋被驴踢了。对于 yarn 或者 pnpm,他们的 workspaces 都不会用这种默认装置 peerDependencies 的模式。
作者本来是想,因为如果 npm 包的开发者申明了 peerDependencies,如果咱们应用过程中没有装置匹配的版本的包就可能导致我的项目跑不了,为了方便使用,他就采纳了默认装置的模式。
然而,这种做法会导致那些 peerDependencies 不合乎书写标准的包,在我的项目中配合应用呈现问题。而且,即便新的包中包作者们开始留神书写标准,然而无奈解决那些曾经公布进来的老包,总不可能全都回收,而后一个个版本从新再公布一遍吧!

坑二:小版本包抵触

这其实是集体大意导致的。

举个例子:zoo应用命令 npm i -S @vue2.2.1 引入 vue,shop应用命令 npm i -S @vue2.2.2 引入 vue。那么,我的项目会有两个版本的 vue 吗?不会。
起因咱们能够看 zoo 我的项目下的package.json

"dependencies": {
  "html-webpack-plugin": "^5.5.0",
  "vue": "^2.2.1",
  "webpack": "^5.65.0",
  "webpack-cli": "^4.9.1",
  "webpack-dev-server": "^4.7.2",
  "webpack-merge": "^5.8.0"
}

豁然开朗。

解决方案

  • 计划 1:其实去掉 ^ 即可;
  • 计划 2:咱们装置的时候能够应用npm i --save-exact vue@2.2.1 --workspace=zoo

总结

本文,利用了 workspaces 来做多包治理,以及多项目管理,体现出了 workspaces 的弱小。因为我集体负责的我的项目始终以来都是应用 npm 来治理的,所以想要迁徙到 yarn 或者 pnpm 存在未知的危险,而且,也尝试过,因为一些老包 yarn2 和 pnpm 都跑不起来。对于新的我的项目,集体也更举荐 yarn2 或者 pnpm 进行治理,它们比 npm 更加弱小。

本原文来自于集体 github 博客,感觉好的小伙伴能够点个赞哈~
<(~▽~)/

文中多包治理和多项目管理的源码别离在:

  • 多包治理
  • 多项目管理

有趣味的同学能够自行下载学习。

退出移动版