從零到一打造 Vue3 響應式系統 Day 2 - 開發環境建置:monorepo

Image

前言

Vue 3 的原始碼由多個模組構成,除了我們常用的核心功能外,還包含了響應式、工具函數等多個獨立模組。為了模擬 Vue 官方的開發環境,管理這些分散的模組,我們會採用 Monorepo 架構來進行專案管理,並且使用 pnpm workspace。

強烈建議大家一定要跟著動手編碼,如果只是看,很容易停留在「知道」的層面。

什麼是 Monorepo?

Monorepo 是一種程式碼管理方式,指將不同的專案放在單一的程式碼倉庫 (repository) 中,對多個不同的專案進行版本控制。

Monorepo 的特點

  • 集中式開發:所有專案的程式碼都集中在同一個 repository 中。
  • 工具共享:因為統一管理,所以 CI/CD、程式碼風格工具等都可以共享,並且只需配置一次。
  • 統一版本控制:在 monorepo 中進行 commit,可以橫跨多個子專案。

什麼是 pnpm workspace?

pnpm workspace 是 pnpm 套件管理工具提供的一個功能,核心目標是在 repo 內部安裝依賴包,並且共享 node_module,子專案在 repo 中可以互相引用。

pnpm workspace 的特點

  • 依賴提升至根目錄:節省磁碟空間。
  • 模組共享簡單:用 workspace:* 直接引用。
  • 集中管理:一個命令可以管理所有子專案,例如 pnpm install → 安裝全部專案的依賴包。

環境搭建

  1. 我們先創建一個資料夾,執行 pnpm init

  2. 新建 pnpm-workspace.yaml,並且我們要管理 packages 下面的子專案。

    packages:
     - 'packages/*'
  3. 在根目錄下新建 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",
    
       // 允許直接導入 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": "./"
     }
    }
  4. 新建 packages 資料夾,裡面會加入許多子專案,包含響應式系統等。

  5. 執行 pnpm i typescript esbuild @types/node -D -w,其中 -w 表示安裝到 workspace 的根目錄。

  6. 執行 pnpm i vue -w,安裝 vue,方便之後進行比較。

  7. 執行 npx tsc --init,初始化專案中的 TypeScript 配置。

  8. 在根目錄的 package.json 中加入 "type": "module"

    • 這會讓 Node.js 預設將 .js 檔案視為 ES Module (ESM)。
    • 若沒有此項設定,.js 檔案則會被當作 CommonJS 模組處理。
  9. 接下來,我們在 package 資料夾下新建三個子專案目錄 reactivitysharedvue,以及下列檔案:

    • 響應式模組 reactivity: reactivity/src/index.tsreactivity/package.json
    • 工具函數 shared: shared/src/index.tsshared/package.json
    • 核心模組 vue: vue/src/index.tsvue/package.json
  10. 為了讓我們的子專案擁有和 Vue 官方包類似的配置,我們先將 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"
        ]
      }
    }
  11. 執行 pnpm i @vue/shared --workspace --filter @vue/reactivity 將工具函數專案作為依賴安裝到響應式模組中。

  12. 接著在根目錄下新建一個 scripts/dev.js

    • 在根目錄的 package.json 中加入 "dev": "node scripts/dev.js --format esm" 命令。
    • 開發時,我們將透過執行此腳本來啟動編譯。它會使用 esbuild 進行即時編譯,並在首次編譯後持續監聽檔案變動。
    // 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)
    
    /** 
     * 解析要打包的目標
     * - 如果提供了位置參數,則取第一個;否則默認打包 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 編譯上下文並進入 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"
      }
    }

運行測試

  • packages/reactivity/src/index.ts 中編寫一個導出函數

    export function fn(a, b) {
     return a + b;
    }
  • 執行 pnpm dev,你應該會在 packages/reactivity/dist/reactivity.esm.js 中看到以下內容

    // packages/reactivity/src/index.ts
    function fn(a, b) {
     return a + b;
    }
    export { fn };

那就代表環境搭建成功了!

Image


原文出處:https://juejin.cn/post/7548341600638468131


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬6   ❤️9
458
🥈
我愛JS
📝1   💬5   ❤️4
90
🥉
AppleLily
📝1   💬4   ❤️1
50
#4
💬1  
5
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次