当pnpm遇到上了monorepo
`pnpm` 是新一代的包管理工具,它主要的优点有两个:一是采用了 `hard-link` 机制,避免了包的重复安装,节省了空间,同时提高项目依赖的安装速度;二是对 `monorepo` 的支持非常友好,只需要一条配置即可实现。
`monorepo` 是一种新的仓库管理方式。过去的项目,大多采用一个仓库维护一个项目的方案。对于庞大的项目,哪怕只是一处小小的修改也会影响到整体。而采用 `monorepo` 的方式,我们可以在一个仓库中管理多个项目,每个项目都可以单独发布和使用,就像是一个仓库中又有若干个小仓库。
## 搭建开发环境
### 创建项目
首先需要安装 `pnpm`:
```bash
npm i -g pnpm
```
新建一个目录并初始化:
```bash
mkdir pnpm-monorepo
cd pnpm-monorepo
pnpm init
mkdir packages
```
### 配置 monorepo
在项目的根目录中创建 `pnpm-workspace.yaml` 文件,并添加以下内容:
```yaml
packages:
- 'packages/*'
```
这段代码的意思就是将 `packages` 目录下所有的目录都当作单独的包进行管理。通过上面简单的配置,`monorepo` 的开发环境就搭建完成了。
## 安装依赖
正如 `vite` 一样,我们在开发阶段采用 `esbuild` 作为构建工具,在生产阶段采用 `rollup` 进行打包,同时采用 `typescript` 作为开发语言:
```bash
pnpm add -D -w typescript esbuild rollup rollup-plugin-typescript2 @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-commonjs minimist execa
```
使用 `-w` 参数整依赖安装到根目录。下面简单说一下各个依赖的作用:
+ `esbuild`:开发阶段的构建工具;
+ `rollup`:生产阶段的打包工具;
+ `rollup-plugin-typescript2`:rollup 编译 ts 的插件;
+ `@rollup/plugin-json`:将 json 解析为 esm 供 rollup 处理;
+ `@rollup/plugin-node-resolve`:解析安装在 node_modules 下的第三方模块;
+ `@rollup/plugin-commonjs`:将 commonjs 解析为 esm;
+ `minimist`:解析命令行参数;
+ `execa`:生产阶段开启子进程。
## 初始化 typescript
执行以下命令,该命令会在项目的根目录生成一个 `tsconfig.json` 文件:
```bash
pnpm tsc --init
```
对 `tsconfig.json` 进行配置:
```json
{
"compilerOptions": {
"outDir": "dist", // 输出的目录
"sourceMap": true, // 开启 sourcemap
"target": "es2016", // 转译的目标语法
"module": "esnext", // 模块格式
"moduleResolution": "node", // 模块解析方式
"strict": false, // 关闭严格模式,就能使用 any 了
"resolveJsonModule": true, // 解析 json 模块
"esModuleInterop": true, // 允许通过 es6 语法引入 commonjs 模块
"jsx": "preserve", // jsx 不转义
"lib": ["esnext", "dom"], // 支持的类库 esnext 及 dom
"baseUrl": ".", // 当前目录,即项目根目录作为基础目录
"paths": { // 路径别名配置
// "@test/*": ["packages/*/src"] // 当引入 @test/xxxx 时,去 packages/*/src 中找
"helper": ["packages/helper/src"],
"core": ["packages/core/src"]
},
}
}
```
## 创建模块
正如上面 `tsconfig.json` 中 `paths` 的配置一样,我们需要创建 `helper` 和 `core` 两个模块。
### helper 模块
首先是创建 `helper` 模块,在 `packages` 目录下新建 `helper` 目录,并进行目录初始化:
```bash
cd packages
mkdir helper
cd helper
# 初始化
pnpm init
# 创建 src 目录
mkdir src
# 新建 index.ts 文件
touch src/index.ts
```
在 `src/index.ts` 添加测试用代码:
```typescript
export const hello = () => {
return 'Hello world!'
}
```
然后在 `package.json` 中增加以下代码:
```json
"main": "dist/helper.cjs.js",
"module": "dist/helper.esm-bundler.js",
"buildOptions": {
"name": "Helper"
},
```
### core 模块
接下来创建 `core` 模块,与 `helper` 模块类似:
```bash
mkdir core
cd core
pnpm init
mkdir src
touch src/index.ts
```
修改 `packjson.json` 文件:
```json
"main": "dist/core.cjs.js",
"module": "dist/core.esm-bundler.js",
"buildOptions": {
"name": "Core"
},
```
在 `src/index.ts` 中编写测试代码:
```typescript
import { hello } from 'helper'
const str = hello()
console.log(str)
```
可以看到,我们在 `core` 模块中引入了 `helper` 模块的内容,所以需要安装一下:
```bash
pnpm add helper@workspace --filter core
```
这段命令的意思是将 `worksapce` 中的 `helper` 模块安装到 `core` 模块中去,此时可以看到在 `core/package.json` 中已经有了相关的依赖信息:
```json
"dependencies": {
"helper": "workspace:^1.0.0"
}
```
## 编写构建脚本
在根目录创建 `scripts` 目录,并增加 `dev.js` 作为开发阶段的构建脚本:
```bash
mkdir scripts
touch scripts/dev.js
```
在 `dev.js` 中增加以下代码:
```js
const minimist = require('minimist')
const path = require('path')
const { build } = require('esbuild')
const args = minimist(process.argv.slice(2))
// 需要打包的模块,默认打包 core 模块
const target = args._[0] || 'core'
// 打包的格式,默认为 global 即 IIFE 模式
const format = args.f || 'global'
// 打包的入口文件
const entry = path.resolve(__dirname, `../packages/${target}/src/index.ts`)
// 打包文件的输出格式
const outputFormat = format.startsWith('global')
? 'iife'
: format === 'cjs'
? 'cjs'
: 'esm'
// 输出文件路径
const outfile = path.resolve(__dirname, `../packages/${target}/dist/${target}.${format}.js`)
// 读取模块中的 package.json 文件
const pkg = require(path.resolve(__dirname, `../packages/${target}/package.json`))
// 获取 buildOptions 中的 name 作用 IIFE 模式的全局变量名
const pkgGlobalName = pkg?.buildOptions?.name
// 使用 esbuild 进行打包
build({
entryPoints: [entry], // 入口
outfile, // 输出文件路径
bundle: true, // 将依赖的文件递归地打包到一个文件中,默认不会进行打包
sourcemap: true, // 开启 sourcemap
format: outputFormat, // 打包文件输出的格式 'iife' | 'cjs' | 'esm'
globalName: pkgGlobalName, // 如果输出格式为 iife,则需要指定一个全局变量名
platform: format === 'cjs' ? 'node' : 'browser',
// 监听文件变化,进行重新构建
watch: {
onRebuild (error, result) {
if (error) {
console.log('构建失败:', error)
} else {
console.log('构建成功:', result)
}
}
}
}).then(() => {
console.log('监听中...')
})
```
编写完开发阶段的构建脚本后,给根目录的 `package.json` 增加一条 `scripts` 命令进行测试:
```json
"scripts": {
"dev": "node scripts/dev.js core -f global"
},
```
在终端中运行该命令:
```bash
pnpm run dev
```

然后给 `core` 模块根目录中增加一个 `index.html` 文件:
```html
Core
```
然后在控制台中打开 `index.html` 文件,打开浏览器控制台,可以看到:

至此,一个简单的 `monorepo` 开发环境就搭建完毕了。
## 直接使用 Vite 来搭建
首先是创建项目:
```bash
pnpm create vite pnpm-monorepo
```
选择 `Vanilla`

和 Typescript`

接下来进入 `pnpm-monorepo` 目录,移除 `src` 和 `public` 目录,并增加 `packages` 目录,然后在根目录新增 `pnpm-workspace.yaml`:
```yaml
packages:
- 'packages/*'
```
### 创建模块
进入 `packages` 目录,通过以下命令创建 `helper` 模块 和 `core` 模块。
#### helper
```bash
cd packages
# 同样是选择 Vanilla 和 Typescript
pnpm create vite helper
pnpm create vite core
```
接下来进入 `helper` 模块,删除 `src` 和 `public` 目录,并删除 `index.html` 文件,之后新增 `index.ts` 文件:
```bash
cd helper
rm -rf src public index.html
touch index.ts
```
然后在 `index.ts` 增加测试代码:
```ts
export const plus = (a: number, b: number): number => {
return a + b
}
```
由于我们没有了 `src` 目录,所以需要移除 `tsconfig.json` 中的 `includes` 字段。完成之后创建 `vite.config.ts` 来指定模块打包的入口文件:
```ts
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: './index.ts',
name: 'helper',
fileName: 'helper'
}
}
})
```
执行打包命令:
```bash
pnpm install
pnpm run build
```
然后给 `packages.json` 增加字段:
```json
"main": "dist/helper.js",
```
#### core
移除 `public` 目录,删除 `src` 目录中除了 `main.ts` 之外的其他文件,然后把 `helper` 包引入:
```bash
rm -rf public
rm -f src/counter.ts src/style.css src/typescript.svg
pnpm install
pnpm add helper@workspace --filter core
```
然后在 `src/main.ts` 写入以下代码:
```ts
// src/main.ts
import { plus } from 'helper'
const sum = plus(1, 2)
console.log(sum)
```
执行开发环境命令:
```bash
pnpm run dev
```
在浏览器中打开指定地址,可以在控制台中看到打印 `3`。至此,`core` 模块也处理完毕。
### 开发环境构建
首先需要监听 `helper` 代码的变化,进行构建:
```bash
# packages/helper
pnmp run build --watch
```
然后还需要进入 `core` 的开发环境:
```bash
# packages/core
pnmp run dev
```
我们都知道,`build --watch` 会阻止后续的进程执行,所以我们需要引入 `concurrently` 来完成同步执行这两条脚本的操作。
进入 `pnpm-monorepo` 根目录,安装 `concurrently`:
```bash
cd pnpm-monorepo
# -w 表示把依赖安装到根目录
pnpm add concurrently -D -w
```
接下来在 `package.json` 里面增加两条 `script` 脚本命令:
```json
"script": {
"build:helper": "cd packages/helper && pnpm run build --watch",
"dev:core": "concurrently \"pnpm run build:helper\" \"cd packages/core && pnpm run dev\""
}
```
然后运行 `pnpm run dev:core` 脚本,启动 `core` 的开发环境。我们可以在浏览器的控制台看到打印 `3`。
接下来在 `helper` 中的 `index.ts` 里面增加一个方法:
```ts
// packages/helper/index.ts
export const plus = (a: number, b: number): number => {
return a + b
}
export const minus = (a: number, b: number): number => {
return a - b
}
```
在 `core` 中的 `src/main.ts` 里面引入新增加的 `minus()` 方法,并使用:
```ts
// packages/core/src/main.ts
import { plus, minus } from 'helper'
const sum = plus(1, 2)
console.log(sum)
const result = minus(1000, 7)
console.log(result)
```
保存之后,我们可以看到控制台输出:

如此一来,我们使用 `vite` 来搭建的 `monorepo` 也完成了。