3756 字
19 分鐘

npm Workspaces 入門:用 Vue 3 整理多專案共用程式碼

當手上只有一個 Vue 3 專案,共用程式碼放在 src/utilssrc/components 很自然。但專案增加到後台、官網、會員中心之後,同一個日期格式化函式、按鈕元件或 ESLint 設定就會開始散落各處。修正一次 bug,還要記得貼到另外兩個 repository,久了很難確認哪一份才是最新版。

npm Workspaces 適合處理的正是這種情況:把多個應用程式與共用套件放在同一個 repository,由根目錄統一安裝依賴與維護 package-lock.json,各專案仍保有自己的 package.json、開發指令與部署流程。

這篇會用兩個 Vue 3 + Vite 應用程式示範:

  • apps/admin:後台
  • apps/shop:前台商店
  • packages/shared:共用 TypeScript 工具
  • packages/ui:共用 Vue 元件

最後要記住的核心是:workspace 負責管理多個套件與本機連結;真正的共用邊界,仍要由你依照變動原因來切。

npm Workspace 解決了什麼問題#

Workspace 是 npm CLI 管理多個本機 package 的機制。根目錄 package.jsonworkspaces 宣告哪些資料夾屬於工作區,執行 npm install 時,npm 會把這些本機 package 連結到 node_modules,因此應用程式可以直接用 package name 匯入,不必另外執行 npm link

它帶來的改變可以整理成四點:

原本的做法改成 workspace 之後
每個專案各自安裝依賴在根目錄安裝,維護一份 package-lock.json
共用程式碼靠複製抽成有名稱、有版本的本機 package
修改後逐一同步修改共用 package,所有使用者立即讀到同一份來源
進入各資料夾執行指令從根目錄用 -w 指定 workspace

Workspace 經常和 monorepo 一起出現,但兩者不是同一件事。monorepo 是把多個專案放在同一個 repository 的管理方式;npm Workspaces 則是其中一套套件與指令管理機制。它不會自動替你決定模組邊界,也不是會依照相依圖平行排程的建置系統。

先決定哪些東西值得共用#

剛開始整理時,不要看到重複就全部抽出去。先從「兩個專案真的使用相同規則,而且會一起修改」的內容開始:

  • 純 TypeScript 工具:日期、金額、字串與驗證函式
  • 不綁特定頁面的 Vue 元件:按鈕、Modal、表單欄位
  • 共用型別:API response、domain model、表單資料結構
  • 開發設定:ESLint、TypeScript、Prettier 的基礎設定

以下內容先留在各自的 app 通常比較清楚:

  • router、頁面與 layout
  • 各環境的 API URL
  • 綁定單一產品流程的 Pinia store
  • 只在一個專案出現的元件

環境變數尤其不適合因為名稱相同就直接共用。Vite 的 VITE_ 變數會進入前端產物,正式站與後台也可能指向不同 API;可先參考 Vite 環境變數的模式、優先順序與安全邊界 再決定設定放哪裡。

建立 Vue 3 workspace 的目錄結構#

假設你已經有兩個 Vue 3 + Vite 專案,可以先整理成以下結構:

vue-workspace/
├── apps/
│ ├── admin/
│ │ └── package.json
│ └── shop/
│ └── package.json
├── packages/
│ ├── shared/
│ │ ├── src/index.ts
│ │ └── package.json
│ └── ui/
│ ├── src/AppButton.vue
│ ├── src/index.ts
│ └── package.json
├── package.json
└── package-lock.json

1. 在根目錄宣告 workspaces#

建立根目錄 package.json

{
"name": "vue-workspace",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev:admin": "npm run dev -w @example/admin",
"dev:shop": "npm run dev -w @example/shop",
"build": "npm run build --workspaces --if-present",
"typecheck": "npm run typecheck --workspaces --if-present"
}
}

private: true 很重要,它可以避免誤把 workspace 根目錄發布到 npm registry。apps/*packages/* 會尋找下一層含有有效 package.json 的資料夾。

2. 保留每個 Vue app 的 package.json#

後台的 apps/admin/package.json 可以長這樣:

{
"name": "@example/admin",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@example/shared": "1.0.0",
"@example/ui": "1.0.0",
"vue": "^3.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "latest",
"vite": "latest",
"vue-tsc": "latest"
}
}

apps/shop/package.json 使用另一個唯一名稱 @example/shop,其餘依賴可依專案需求調整。範例用 latest 是為了避免文章把讀者鎖在某個過期工具版本;正式專案安裝完成後,應提交 package-lock.json,CI 使用 npm ci 重現已鎖定的版本。

本機套件使用 1.0.0,是因為它和接下來兩個 package 的 version 相符。不要把 pnpm 常見的 workspace:* 直接貼進 npm 專案;npm Workspaces 會在版本範圍符合本機 package 時建立連結。

把共用工具抽成獨立 package#

先處理最單純的共用程式碼。建立 packages/shared/package.json

{
"name": "@example/shared",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": "./src/index.ts"
}

接著在 packages/shared/src/index.ts 放入共用函式:

export function formatTwd(value: number): string {
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
maximumFractionDigits: 0,
}).format(value);
}

兩個 Vue app 都可以用套件名稱匯入:

import { formatTwd } from '@example/shared';
const totalLabel = formatTwd(1280);

這裡讓 exports 直接指向 TypeScript 原始碼,是配合 Vite 開發的簡化做法。如果這個 package 之後要發布到 registry、提供給非 Vite 專案,或需要產生 .d.ts,就應替它加入獨立 build,讓 exports 指向 dist 產物。

把 Vue 元件抽成共用 UI package#

建立 packages/ui/package.json

{
"name": "@example/ui",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": "./src/index.ts",
"peerDependencies": {
"vue": "^3.5.0"
},
"devDependencies": {
"vue": "^3.5.0"
}
}

共用元件把 Vue 放在 peerDependencies,意思是由使用它的 app 提供 Vue。devDependencies 則讓 UI package 自己執行型別檢查與測試時也有開發依賴。兩邊的版本範圍應保持相容,避免 app 與元件套件各自載入不同 Vue runtime。

新增 packages/ui/src/AppButton.vue

<script setup lang="ts">
defineProps<{
disabled?: boolean;
}>();
const emit = defineEmits<{
click: [event: MouseEvent];
}>();
</script>
<template>
<button
type="button"
:disabled="disabled"
class="app-button"
@click="emit('click', $event)"
>
<slot />
</button>
</template>
<style scoped>
.app-button {
border: 0;
border-radius: 8px;
padding: 8px 16px;
color: white;
background: #2563eb;
cursor: pointer;
}
.app-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
</style>

再從 packages/ui/src/index.ts 匯出:

export { default as AppButton } from './AppButton.vue';

app 端就能像一般 npm package 一樣使用:

<script setup lang="ts">
import { AppButton } from '@example/ui';
import { formatTwd } from '@example/shared';
function submitOrder() {
console.log(`送出訂單:${formatTwd(1280)}`);
}
</script>
<template>
<AppButton @click="submitOrder">送出訂單</AppButton>
</template>

如果元件開始包含跨層狀態、provide/inject 或複雜事件流,可搭配 Vue 3 <script setup> 元件溝通整理 重新判斷 API,不要為了共用而把產品流程塞進單一萬用元件。

安裝依賴與執行指定專案#

回到 workspace 根目錄,只需要安裝一次:

Terminal window
npm install

npm 會在根目錄產生 package-lock.json,並把 @example/shared@example/ui 連結到本機 workspace。可以用下列指令檢查解析結果:

Terminal window
npm ls --workspaces --depth=0

啟動單一 Vue app:

Terminal window
npm run dev -w @example/admin

也可以使用資料夾路徑:

Terminal window
npm run dev -w apps/admin

只替後台安裝 axios

Terminal window
npm install axios -w @example/admin

只替 UI package 安裝測試工具:

Terminal window
npm install -D vitest -w @example/ui

在所有有 typecheck script 的 workspace 執行檢查:

Terminal window
npm run typecheck --workspaces --if-present

--if-present 會略過沒有這個 script 的 package。這很適合統一跑短任務,但不適合同時啟動兩個長時間執行的 Vite dev server;最簡單的方式是開兩個終端機分別跑 dev:admindev:shop,需要單一指令並行時再引入專門的並行工具。

從多個既有 Vue 專案遷移的順序#

如果現在每個 app 都是獨立 repository,不建議一次搬完所有程式碼。可以依照以下順序降低排錯範圍:

  1. 建立新的 workspace 根目錄與 apps/*
  2. 先搬入兩個 app,但暫時不抽共用程式碼。
  3. 刪除 app 內原本的 node_modules 與 lockfile,回根目錄執行 npm install
  4. 分別執行兩個 app 的 dev、build 與測試,確認搬家本身沒有改變行為。
  5. 先抽純函式與型別到 packages/shared
  6. 再抽沒有產品流程的 Vue 元件到 packages/ui
  7. 每抽一批就搜尋舊 import、跑 typecheck 與 build,不要累積到最後一次處理。

可以用這些指令做基本驗證:

Terminal window
npm ci
npm run typecheck --workspaces --if-present
npm run build --workspaces --if-present
npm ls --workspaces --depth=0

CI 應從 repository 根目錄執行 npm ci。如果 admin 與 shop 分開部署,後續 job 再以 -w 指定各自的 build script,並把對應的 app、共用 package、根目錄 package.jsonpackage-lock.json 納入變更判斷。

Workspace 裡只部署單一 Vue 系統#

改成 npm Workspaces 後,部署 admin 並不代表只把 apps/admin 交給 CI。admin 的依賴還包含根目錄 lockfile、packages/sharedpackages/ui,所以安裝階段仍要看得到整個 repository。

部署單一系統時,可以把設定拆成四個值:

設定admin 的值原因
Working directoryrepository 根目錄npm ci 要讀根目錄的 workspace 與 lockfile
Install commandnpm ci安裝整個 workspace 的鎖定依賴與本機連結
Build commandnpm run build -w @example/admin只執行 admin 的 build script
Output directoryapps/admin/distVite 預設把產物放在該 app 的 dist
不要直接把 Root Directory 設成 apps/admin

如果部署平台會因此只 checkout 或只上傳 apps/admin,建置時就拿不到根目錄 package-lock.jsonpackages/*。平台應保留完整 repository 作為 build context,再用 build command 和 output directory 指定單一 app。

GitHub Actions:只建置 admin#

新增 .github/workflows/deploy-admin.yml

name: Build admin
on:
push:
branches: [main]
paths:
- 'apps/admin/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/deploy-admin.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
build-admin:
runs-on: ubuntu-latest
environment: admin-production
env:
VITE_API_BASE_URL: ${{ vars.ADMIN_API_BASE_URL }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- name: Install workspace dependencies
run: npm ci
- name: Type check admin
run: npm run typecheck -w @example/admin
- name: Build admin
run: npm run build -w @example/admin
- name: Upload admin artifact
uses: actions/upload-artifact@v7
with:
name: admin-dist
path: apps/admin/dist
if-no-files-found: error

這個 workflow 的安裝範圍是整個 workspace,執行範圍則只有 admin。setup-node 快取的是 npm 的下載快取,不是直接保存 node_modules;真正安裝仍由 npm ci 與已提交的 package-lock.json 決定。

最後的 artifact 可以交給另一個 deploy job、部署 CLI 或主機同步流程。若平台本身已連接 GitHub 並負責部署,就不一定需要 upload-artifact,直接把前面表格的 working directory、install、build 和 output directory 填進平台設定即可。

shop 要有自己的部署入口#

shop 可以使用另一份 workflow,把下列項目換掉:

觸發路徑:apps/shop/**、packages/**、根目錄 manifests
Build command:npm run build -w @example/shop
Output directory:apps/shop/dist
部署環境:shop-production

兩個 app 分開設定,可以讓 admin 變更只走 admin 部署;但只要 packages/**、根目錄 package.jsonpackage-lock.json 改變,兩邊都應重新建置,因為共同依賴可能影響兩份產物。

如果 packages 數量增加,可以把 packages/** 收斂成該 app 真正依賴的 package 路徑。不過這份清單要包含間接依賴:假設 admin 使用 packages/ui,而 UI 又依賴 packages/shared,兩個路徑都要列入。

建置時才注入 Vue 環境變數#

Vite 會在 build 時把 VITE_ 變數寫進前端產物,因此 admin 與 shop 應使用不同 deployment environment 或平台變數。上面的 ADMIN_API_BASE_URL 是 GitHub environment variable,再映射成 Vite 讀取的 VITE_API_BASE_URL

真正的 API secret、資料庫密碼與私密 token 不能因為放在 GitHub Secrets 就安全地交給 VITE_。只要進入靜態前端 build,瀏覽器使用者就能讀到;需要保密的值必須留在後端或 server-side function。

部署前檢查的不是只有 admin 資料夾#

單一 app 的 CI/CD 至少要監控這些輸入:

apps/admin/**
packages/shared/**
packages/ui/**
package.json
package-lock.json
.github/workflows/deploy-admin.yml

GitHub Actions 的 paths filter 會在符合路徑時觸發 workflow。若這個 workflow 同時被設為 pull request 的 required check,要注意路徑過濾造成 workflow 未執行時,相關 check 可能維持 Pending;這種情況可以讓 PR 驗證 workflow 固定執行,再把路徑過濾只放在 push 後的部署 workflow。

最常遇到的五個問題#

1. package name 重複或沒有 scope#

每個 workspace 都要有唯一的 name。使用 @example/admin 這類專案 scope,比 adminshared 更不容易和 registry 上的套件撞名,也能從 import 看出它屬於哪個團隊。

2. 本機 package 的版本範圍對不上#

app 寫了 "@example/ui": "^2.0.0",但本機 UI package 仍是 1.0.0 時,npm 不能把它當成滿足範圍的本機版本。先檢查兩邊 version 與 dependency range,再判斷是否真的要升 major。

3. 程式能跑,但 package.json 沒宣告依賴#

npm 可能把依賴提升到根目錄,讓未宣告的 import 暫時能被解析。不要把「node_modules 找得到」當成依賴已正確設定;哪個 workspace 直接 import 套件,就在哪個 workspace 的 dependenciesdevDependencies 明確宣告。

4. 共用 UI 出現兩份 Vue#

UI package 若把 Vue 當成一般 runtime dependency,又和 app 使用不相容版本,可能得到多份 Vue runtime。共用元件庫應以 peerDependencies 表達由 app 提供 Vue,並用 npm ls vue --all 檢查實際依賴樹。

5. npm run build --workspaces 不懂相依圖#

--workspaces 可以逐一執行 script,但不等於會分析 @example/ui 必須先於 @example/admin 建置。本文讓 Vite 直接讀共用 package 的原始碼,因此沒有獨立產物順序;若未來每個 package 都輸出 dist,就要明確安排 scripts,或導入能理解任務相依關係的建置工具。

什麼情況先不要合成 monorepo#

npm Workspaces 很適合同一個團隊維護、需要一起修改與測試的 Vue 專案。若多個專案的權限、發布週期、技術棧與維護團隊完全不同,硬放進同一個 repository 反而會讓 CI、版本與權限變得更難管理。

你也不需要等所有共用內容都規劃完成才開始。先把兩個 app 放進 workspace,抽出一個純函式 package,確認本機開發與部署流程走通,再決定 UI、型別與設定是否值得繼續拆分。這比先設計十個空 package 更容易看出真正的共用邊界。

實測環境為 Node.js 24.13.0、npm 11.6.2、Vue 3.5.41(2026-08-14)。最小驗證包含兩個 app、兩個本機 package、單一 lockfile,以及 workspace symlink 解析。

常見問題#

Q: npm Workspaces 和 pnpm workspace 有什麼差別?#

A: 兩者都能管理 monorepo 裡的多個 package,但設定檔、lockfile、安裝策略與部分依賴語法不同。npm 在根目錄 package.json 使用 workspaces;pnpm 通常使用 pnpm-workspace.yaml,也提供 workspace: protocol。不要只替換指令名稱就共用設定,遷移時要連 lockfile 與 dependency spec 一起檢查。

Q: 共用 Vue 元件一定要發布到 npm 嗎?#

A: 不需要。只要 app 和 UI package 位於同一個 workspace,npm install 就能建立本機連結,開發時可直接 import。只有其他 repository 也要使用、需要獨立版本或公開散布時,才需要建立 build、版本與發布流程。

Q: 每個 Vue app 還需要自己的 package.json 嗎?#

A: 需要。根目錄負責宣告 workspaces 與統一操作,每個 app 的 package.json 仍要記錄自己的名稱、scripts、dependencies 與 devDependencies。這個邊界讓你能單獨啟動、建置或部署某一個 app。

Q: npm Workspaces 可以讓兩個 Vue 專案同時啟動嗎?#

A: Workspace 能替指定 package 執行 script,但 npm 的 --workspaces 不是專門的長時間程序並行器。開發時可先用兩個終端機分別執行;確定需要單一指令後,再加入並行工具並處理 log、退出碼與關閉行為。

Q: 只部署 admin,可以在 apps/admin 裡執行 npm ci 嗎?#

A: 不建議。這個架構只有根目錄 package-lock.json,而且 admin 依賴 packages/sharedpackages/ui。CI 應在 repository 根目錄執行 npm ci,再用 npm run build -w @example/admin 限定建置目標,最後部署 apps/admin/dist

參考資料:

npm Docs:Workspaces

npm Docs:package.json 的 workspaces 欄位

npm Docs:npm install

npm Docs:npm run-script

GitHub Docs:Workflow syntax 與 paths filter

GitHub:actions/setup-node

GitHub Docs:Workflow artifacts

npm Workspaces 入門:用 Vue 3 整理多專案共用程式碼
https://laplusda.com/posts/npm-workspaces-vue3-monorepo-guide/
作者
Zero
發佈於
2026-08-14
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。