pnpm + workspace
思考 ??:什么是工作空間?
答案:工作空間可以看作是一個(gè)共享的區(qū)域,所有用于工作的資源都可以從這個(gè)區(qū)域獲取到。
Monorepo 背后的思想:創(chuàng)建一個(gè)公共的空間,服務(wù)于多個(gè)項(xiàng)目,把多個(gè)項(xiàng)目用到的共有的一些東西,提取出來
生活中工作空間
在這個(gè)工作空間中,通常會包含與工作相關(guān)的所有工具和資源,比如辦公桌、電腦、文具和文件柜等。這個(gè)工作空間是一個(gè)集中完成特定任務(wù)的地方,所有需要用到的東西都可以在這里找到,方便你高效地完成工作。
軟件開發(fā)中的工作空間
在軟件開發(fā)中,工作空間通常指一個(gè)用于組織和管理項(xiàng)目文件、資源和工具的邏輯容器。它通常是一個(gè)文件夾結(jié)構(gòu),用于將相關(guān)的項(xiàng)目文件、代碼、設(shè)置和其他資源集中放置在一起。
工作空間的概念在不同的編程語言和開發(fā)工具中可能略有不同,但其基本目標(biāo)都是提供一個(gè)集中式環(huán)境,以幫助開發(fā)者管理和協(xié)同開發(fā)多個(gè)項(xiàng)目。主要功能包括:
- 組織和管理項(xiàng)目文件
- 跨項(xiàng)目共享設(shè)置和工具
- 支持協(xié)同開發(fā)
pnpm 中的工作空間
在 pnpm 中,工作空間就是一個(gè)管理多個(gè)包的環(huán)境,它通過獨(dú)特的依賴管理方式極大地提高了效率。pnpm 的工作空間支持符號鏈接和硬鏈接機(jī)制,使得不同包之間能夠高效地共享依賴,同時(shí)保證每個(gè)包的獨(dú)立性。
pnpm 工作空間特點(diǎn):
- 高效的依賴管理
- 節(jié)省磁盤空間
- 跨項(xiàng)目的高效協(xié)作
pnpm 的工作空間為大型 Monorepo 項(xiàng)目提供了一個(gè)強(qiáng)大而靈活的開發(fā)環(huán)境,使得管理和開發(fā)多個(gè)包變得更加簡單和高效。
pnpm 中定義工作空間
在根目錄有一個(gè) pnpm-workspace.yaml 的文件,該文件用于定義哪些包會被包含在 workspace 工作空間中,默認(rèn)情況下,所有子目錄下的所有包都會被包含在 works ace 里面。
示例:
packages:
# packages/ 下所有子包,但是不包括子包下面的包
- "packages/*"
# components/ 下所有的包,包含子包下面的子包
- "components/**"
# 排除 test 目錄
- "!**/test/**"
注意這里表示包范圍的語法使用的是 Glob 表示法。
實(shí)戰(zhàn)演練
創(chuàng)建基于 pnpm + workspace 的 Monorepo 工程,并在工程中封裝一個(gè)公共的函數(shù)庫。
安裝依賴到工作空間里面:
pnpm add <包名> --workspace-root
or
pnpm add <包名> -w
安裝工作空間的一個(gè)包到工作空間另一個(gè)包里面:
pnpm add <包名B> --workspace --filter <包名A>
該命令表示將 B 包安裝到 A 包里面,也就是說 B 包成為了 A 包的一個(gè)依賴。其中 B 包后面的 --workspace 參數(shù)表示該包來自于工作空間,而非 npm 遠(yuǎn)程倉庫,--filter 表示安裝到 A 包里面。
創(chuàng)建一個(gè)基于 pnpm + workspace 的 Monorepo 工程,完整步驟:
-
創(chuàng)建一個(gè)根目錄,并初始化 pnpm workspace,例如:
mkdir monorepo cd monorepo pnpm init // 初始化 pnpm workspace,如果本地沒有安裝pnpm包的話,需要先安裝或者使用 npx pnpm init -
創(chuàng)建 pnpm-workspace.yaml 文件,并定義工作空間,例如:
packages: - "components/\*" - "utils/\*" - "projects/\*" 創(chuàng)建目錄:
monorepo/components //公共組件存放的目錄
monorepo/utils //公共函數(shù)存放的目錄
monorepo/projects //存放多個(gè)項(xiàng)目的目錄
4.在utils下創(chuàng)建一個(gè)公共的工具庫,tools目錄,并切換到該目錄下,使用pnpm init初始化項(xiàng)目,并安裝依賴,例如:
pnpm init
現(xiàn)在tools目錄下已經(jīng)創(chuàng)建了package.json文件,可以安裝依賴,tools目錄就是一個(gè)包了
接下來我們將會使用typescript來開發(fā)我們的工具庫,那么我們要把typescript安裝在那個(gè)目錄呢?
我們的typescript多個(gè)包都需要用到,所以我們需要安裝到工作空間里面,而不是安裝到某個(gè)包里面,這樣可以保證多個(gè)項(xiàng)目使用的typescript版本保持一致
pnpm add typescript -D --workspace-root
執(zhí)行完上面的代碼之后就可以在項(xiàng)目的根目錄package.json文件里面看到typescript的依賴了
接下來我們就可以愉快的開發(fā)我們的工具庫了,在tools下面創(chuàng)建一個(gè)src目錄,并創(chuàng)建一個(gè)index.ts文件,一個(gè)sum.ts文件,并編寫我們的工具庫,例如:
//sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
//index.ts
export * from './sum';
至此,我們的工具庫就算開發(fā)完成了,接下來我們需要對我們的工具庫進(jìn)行測試,在tools目錄下創(chuàng)建一個(gè)tests目錄存放我們的測試文件,這里我們選擇vitest作為我們的測試框架,同樣,除了tools需要測試,我們也需要對其他的模塊進(jìn)行測試,所以我們還是把vitest安裝到工作空間里面:
pnpm add vitest -D --workspace-root
安裝好之后,我們就可以開始編寫我們的測試文件了,在tests目錄下創(chuàng)建一個(gè)sum.test.ts文件,并編寫我們的測試代碼,例如:
import { sum } from "../src/sum";
test("測試sum方法", () => {
const result = sum(10, 2);
expect(result).toBe(12);
});
測試文件寫完之后,我們需要運(yùn)行測試文件,檢查測試結(jié)果,我們在tools/package.json文件里面添加一下命令:
"test": "vitest"
然后在終端中運(yùn)行該命令:
pnpm test
這個(gè)時(shí)候,終端中匯報(bào)一個(gè)錯(cuò):
ReferenceError: test is not defined
這個(gè)時(shí)候我們需要在項(xiàng)目根目錄下創(chuàng)建一個(gè)vitest.config.ts文件,并編寫我們的配置文件,例如:
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: true,//表示vitest啟用全局模式
environment: 'node',
},
})
配置完成之后,再次運(yùn)行測試文件,就會發(fā)現(xiàn)我們的測試結(jié)果是正確的,我們的工具庫開發(fā)完成了。
然后我們就需要對我們的工具庫函數(shù)進(jìn)行打包,以便其他開發(fā)人員可以使用
對于打包我們主要有兩件事情要做,一個(gè)是調(diào)整typescript的配置選項(xiàng),另一個(gè)是使用rollup打包
首先我們使用npx tsc --init在tools目錄下創(chuàng)建一個(gè)tsconfig.json文件,并編寫我們的配置選項(xiàng),例如:
{
"compilerOptions": {
"target": "ES2015",//表示編譯成es6的代碼
"module": "ES2015",//指定使用的模塊系統(tǒng)
"declaration": true,//生成類型聲明文件
"declarationDir": "./dist/types",//生成聲明文件的目錄
"esModuleInterop": true,//允許導(dǎo)入非模塊的包(啟用es模塊和common模塊操作的互相支持)
"forceConsistentCasingInFileNames": true//強(qiáng)制文件名大小寫匹配
},
"include": ["src/**/*"]//告訴編譯器要編譯的文件
}
配置好tsconfig.json文件之后,我們就可以開始打包我們的工具庫了,首先安裝打包相關(guān)的依賴:
pnpm add rollup rollup-plugin-typescript2 @babel/preset-env @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve -D --workspace-root
在tools目錄下創(chuàng)建一個(gè)rollup.config.js文件,并編寫我們的配置文件:
// tools/rollup.config.js
// 導(dǎo)入各種插件
import typescript from "rollup-plugin-typescript2";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import json from "@rollup/plugin-json";
import babel from "@rollup/plugin-babel";
const extensions = [".js", ".ts"];
// 導(dǎo)出一個(gè)數(shù)組,數(shù)組里面每一個(gè)對象對應(yīng)一種格式的配置
export default [
// CommonJS
{
input: "src/index.ts",
output: {
file: "dist/index.cjs",
format: "cjs",
},
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
resolve({ extensions }),
commonjs(),
json(),
],
},
// ESM
{
input: "src/index.ts",
output: {
file: "dist/index.js",
format: "es",
},
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
resolve({ extensions }),
commonjs(),
json(),
],
},
// Browser-compatible
{
input: "src/index.ts",
output: {
file: "dist/index.browser.js",
format: "iife",
name: "jsTools",
},
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
resolve({ extensions }),
commonjs(),
json(),
babel({
exclude: "node_modules/**",
extensions,
babelHelpers: "bundled",
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead",
},
],
],
}),
],
},
];
安裝好依賴之后,我們需要添加一個(gè)打包的命令,在tools目錄下創(chuàng)建一個(gè)package.json文件,并添加以下代碼:
"scripts": {
"build": "rollup -c"
}
接下來,我們就可以運(yùn)行打包命令了,在終端中運(yùn)行以下命令:
pnpm build
打包完成之后,我們就可以得到一個(gè)dist目錄,里面有index.cjs,index.js,index.browser.js三個(gè)文件,分別對應(yīng)三種格式的代碼,我們可以根據(jù)需要選擇使用哪種格式的代碼。
接下來我們需要在別的項(xiàng)目中引入我們的工具庫,還有一件非常重要的事情,我們需要在package.json中指定一下我們的入口文件,
以便其他包含我們的工具庫時(shí),可以正確地解析我們的代碼。
在package.json中添加以下代碼:
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",//esm
"require": "./dist/index.cjs"http://commonjs
}
},
做完所有的這些操作之后,我們huidao項(xiàng)目根目錄下,就可以使用我們的工具庫了。
我們回到項(xiàng)目根目錄下的projects目錄下,來測試下是否可以正常使用我們的工具庫。
新建一個(gè)test-tools的目錄
mkdir test-tools //projects/test-tools
然后使用pnpm init 初始化一個(gè)package.json文件
然后通過mkdir src &&cd ./src &&touch index.ts一個(gè)src目錄,并創(chuàng)建一個(gè)index.ts文件
接下來我們要在index.ts里面使用我們的工具庫,tools是存放在工作空間里面的,那么我們?nèi)绾伟惭b一個(gè)工作空間的包呢?我們可以通過一下的命令來實(shí)現(xiàn):
pnpm add <包名B> --workspace --filter <包名A>
所以我們只需要運(yùn)行:
pnpm add tools --workspace --filter test-tools
就可以了,運(yùn)行完這個(gè)命令之后,就可以在test-tools的package.json文件中看到我們的工具庫依賴了。

這表示test-tools的依賴tools來自于工作空間,而不是單獨(dú)的包。這樣當(dāng)我們的tools發(fā)生變化時(shí),我們在使用的時(shí)候也會自動(dòng)更新。
現(xiàn)在我們就可以愉快的使用工具庫了。
在index.ts里面添加以下代碼:
import { sum } from "tools";
console.log(sum(1, 2));
console.log(sum(10, 2));
然后在package.json中添加以下代碼:
"type": "module",
"scripts": {
"dev": "tsc && node dist/index.js"
}
然后運(yùn)行pnpm dev就可以看到在控制臺打印出了我們的結(jié)果了。