Vue 3 的原始碼由多個模組構成,除了我們常用的核心功能外,還包含了響應式、工具函式等多個獨立模組。為了模擬 Vue 官方的開發環境,管理這些分散的模組,我們會採用 Monorepo 架構來進行專案管理,並且使用 pnpm workspace。
強烈建議大家一定要跟著 coding,只是看過,容易停留在僅是知道的階段。
Monorepo 是一個管理程式碼的方式,指將不同的專案在單一的程式碼倉庫 (repository) 中,對多個不同的專案進行版本控制。
pnpm workspace 是 pnpm 套件工具提供的一個功能,核心目標是可以在 repo 裡面安裝相依套件,並且共用 node_module
,子專案在 repo 中,可以互相引用。
workspace:*
直接引用。pnpm install
→ 安裝全部專案的相依套件。pnpm init
。pnpm-workspace.yaml
,並且我們要管理 packages
下面的子專案。packages:
- 'packages/*'
tsconfig.json
,這是typescript 設定檔(偉哉GPT幫我寫註解):{
"compilerOptions": {
// 編譯輸出 JavaScript 的目標語法版本
// ESNext:永遠輸出到最新的 ECMAScript 標準
"target": "ESNext",
// 模組系統類型
// ESNext:使用最新的 ES Modules(import / export)
"module": "ESNext",
// 模組解析策略
// "node":模仿 Node.js 的方式去解析模組 (例如 node_modules, index.ts, package.json 中的 "exports")
"moduleResolution": "node",
// 編譯後的輸出資料夾
"outDir": "dist",
// 允許直接 import JSON 檔案,編譯器會把 JSON 當作模組
"resolveJsonModule": true,
// 是否啟用嚴格模式
// false:關閉所有嚴格型別檢查(比較寬鬆)
"strict": false,
// 編譯時會包含哪些內建 API 定義檔(lib.d.ts)
// "ESNext":最新 ECMAScript API
// "DOM":瀏覽器環境的 API,例如 document, window
"lib": ["ESNext", "DOM"],
// 自訂路徑對應(Path Mapping)
// "@vue/*" 會對應到 "packages/*/src"
// 例如 import { reactive } from "@vue/reactivity"
// 會被解析到 packages/reactivity/src
"paths": {
"@vue/*": ["packages/*/src"]
},
// 基準目錄,用來搭配 paths 做相對解析
"baseUrl": "./"
}
}
packages
資料夾,裡面會加入許多子專案,包含響應系統等等。pnpm i typescript esbuild @types/node -D -w
,-w
表示是安裝在 workspace。pnpm i vue -w
,安裝 vue,之後更好可以比較。npx tsc --init
,初始化專案下的 typescript。package.json
中加上 type:module
。
.js
會讓 Node.js 預設將 .js 檔案視為 ES Module (ESM)。.cjs
如果沒有這個設定,.js 檔案會被當作 CommonJS 模組處理。package
資料夾下新增三個子專案目錄reactivity
、shared
、vue
,以及下方檔案:
reactivity/src/index.ts
、reactivity/package.json
shared/src/index.ts
、shared/package.json
vue/src/index.ts
、vue/package.json
node_modules/.pnpm/@vue+reactivity/reactivity/package.json
複製一份到reactivity/package.json
,簡化後的內容如下:{
"name": "@vue/reactivity",
"version": "1.0.0",
"description": "響應式模組",
"main": "dist/reactivity.cjs.js",
"module": "dist/reactivity.esm.js",
"files": [
"index.js",
"dist"
],
"sideEffects": false,
"buildOptions": {
"name": "VueReactivity",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
},
}
{
"name": "@vue/shared",
"version": "1.0.0",
"description": "工具函式",
"main": "dist/shared.cjs.js",
"module": "dist/shared.esm.js",
"files": [
"index.js",
"dist"
],
"sideEffects": false,
"buildOptions": {
"name": "VueShared",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
}
}
{
"name": "vue",
"version": "1.0.0",
"description": "vue核心模組",
"main": "dist/vue.cjs.js",
"module": "dist/vue.esm.js",
"files": [
"dist"
],
"sideEffects": false,
"buildOptions": {
"name": "Vue",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
}
}
pnpm i @vue/shared --workspace --filter @vue/reactivity
將工具函式專案安裝到響應式模組。scripts/dev.js
:
package.json
加入script:node scripts/dev.js --format esm
指令//scripts/dev.js
/**
* 打包「開發環境」使用的腳本
*
* 用法示例:
* node scripts/dev.js --format esm
* node scripts/dev.js -f cjs reactive
*
* - 位置參數(第一個)用來指定要打包的子套件名稱(對應 packages/<name>)
* - --format / -f 指定輸出格式:esm | cjs | iife(預設 esm)
*/
import { parseArgs } from 'node:util'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import esbuild from 'esbuild'
import { createRequire } from 'node:module'
/**
* 解析命令列參數
* allowPositionals: 允許使用位置參數(例如 reactive)
* options.format: 支援 --format 或 -f,型別為字串,預設 'esm'
*/
const {
values: { format },
positionals,
} = parseArgs({
allowPositionals: true,
options: {
format: {
type: 'string',
short: 'f',
default: 'esm',
},
},
})
/**
* 在 ESM 模式下建立 __filename / __dirname
* - ESM 沒有這兩個全域變數,因此透過 import.meta.url 轉換得到
*/
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* 在 ESM 中建立一個 require()
* - 用來載入 CJS 風格資源(例如 JSON)
*/
const require = createRequire(import.meta.url)
/**
* 解析要打包的 target
* - 若有提供位置參數,取第一個;否則預設打包 packages/vue
*/
const target = positionals.length ? positionals[0] : 'vue'
/**
* 入口檔案(固定指向 packages/<target>/src/index.ts)
*/
const entry = resolve(__dirname, `../packages/${target}/src/index.ts`)
/**
* 決定輸出檔路徑
* - 命名慣例:<target>.<format>.js
* 例:reactive.cjs.js / reactive.esm.js
*/
const outfile = resolve(__dirname, `../packages/${target}/dist/${target}.${format}.js`)
/**
* 讀取目標子套件的 package.json
* - 常見做法是從中讀 buildOptions.name,作為 IIFE/UMD 的全域變數名
* - 若 package.json 沒有 buildOptions,請自行調整
*/
const pkg = require(`../packages/${target}/package.json`)
/**
* 建立 esbuild 編譯 context 並進入 watch 模式
* - entryPoints: 打包入口
* - outfile: 打包輸出檔案
* - format: 'esm' | 'cjs' | 'iife'
* - platform: esbuild 的目標平台('node' | 'browser')
* * 這裡示範:如果是 cjs,就傾向 node;否則視為 browser
* - sourcemap: 方便除錯
* - bundle: 把相依打進去(單檔輸出)
* - globalName: IIFE/UMD 下掛在 window 的全域名稱(esm/cjs 不會用到)
*/
esbuild
.context({
entryPoints: [entry], // 入口檔
outfile, // 輸出檔
format, // 輸出格式:esm | cjs | iife
platform: format === 'cjs' ? 'node' : 'browser',// 目標平台:node 或 browser
sourcemap: true, // 產生 source map
bundle: true, // 打包成單檔
globalName: pkg.buildOptions?.name, // IIFE/UMD 會用到;esm/cjs 可忽略
})
.then(async (ctx) => {
// 啟用 watch:監聽檔案變更並自動重建
await ctx.watch()
console.log(
`[esbuild] watching "${target}" in ${format} mode → ${outfile}`
)
})
.catch((err) => {
console.error('[esbuild] build context error:', err)
process.exit(1)
})
{
"name": "vue3-source-code",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js reactivity --format esm"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^24.2.1",
"esbuild": "^0.25.9",
"typescript": "^5.9.2"
},
"dependencies": {
"vue": "^3.5.18"
}
}
package/reactivity/src/index.ts
寫一個導出函式export function fn(a, b) {
return a + b;
}
pnpm dev
,你應該會在package/reactivity/dist/reactivity.esm.js
看到以下內容// packages/reactivity/src/index.ts
function fn(a, b) {
return a + b;
}
export {
fn
};
那就代表環境建置成功了!
檔案結構如下:
同步更新《嘿,日安!》技術部落格
接著在根目錄下新增一個script/dev.js
這邊是不是錯字@@「接著在根目錄下新增一個scripts
/dev.js」
是錯字 我修正了
感謝你認真看哈哈 我好感動