当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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Core</title>
</head>
<body>
<script src="./dist/core.global.js"></script>
</body>
</html>
然后在控制台中打开 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
也完成了。