
“vue3vite單頁面改多頁面”這個標題看著就是一個非常典型的工程化改造需求。我最初接手這類任務時也以為只是改個構建配置實際上手才發(fā)現牽涉到目錄結構、路由方案、公共模塊復用、部署路徑等一系列問題。這篇文章就從我實際改造的經驗出發(fā)把完整思路和踩過的坑都寫清楚給準備動手或正在改造的同學一份能直接抄作業(yè)的參考。先說結論Vite 做多頁面MPA的支持遠比 webpack 時代要優(yōu)雅核心只需要在build.rollupOptions.input里配置多個 HTML 入口但工程上的配套改造才是重頭戲。下面逐步拆解。1. 為什么要把單頁面改造成多頁面1.1 什么場景下會出現這種需求很多 Vue 項目初期都是用npm create vitelatest直接拉的模板默認就是單頁面應用SPA入口只有一個index.html所有頁面靠 vue-router 在瀏覽器端切換。這種模式在大部分業(yè)務下沒問題但實際項目做著做著就會出現幾類需求第一類是后臺管理系統(tǒng)要拆子系統(tǒng)。比如我接觸過一個運營平臺剛開始只有一個后臺后來數據看板和用戶管理兩個模塊的上線節(jié)奏完全不一致兩個團隊分別維護再共用一套 SPA 路由和一套構建產物就很別扭。每次發(fā)布都要一起上線風險被強行綁定。第二類是同一套代碼要輸出多個獨立的落地頁、活動頁或 H5 頁面。這些頁面之間沒有頂層導航關系用戶從不同渠道進入需要各自獨立的 HTML 入口方便運營單獨投放、單獨統(tǒng)計。第三類是某些頁面對首屏加載速度有硬性要求。SPA 不管路由怎么懶加載入口 HTML 始終是同一個所有頁面的運行時腳本都擠在一個 bundle 體系里。MPA 天然按頁面拆分產物互不干擾首屏只加載當前頁面需要的資源性能隔離更干凈。1.2 多頁面方案的核心價值與取舍改造成多頁面后最直觀的好處有三個每個頁面有獨立入口 HTML可以各自設置 title、meta、favicon甚至引入不同的第三方腳本。構建產物按頁面目錄拆分互不依賴部署時可以單獨發(fā)布某個頁面。公共依賴如果拆包合理多個頁面之間可以共享瀏覽器緩存訪問過 A 頁面的用戶再訪問 B 頁面時公共 chunk 直接命中緩存。但代價也很明顯頁面之間跳轉需要整頁刷新不再有 SPA 那種“無刷新切換”的流暢感。如果業(yè)務本身是一個連貫的、模塊間頻繁跳轉的強交互應用改成 MPA 反而會變慢。所以開干之前一定要和業(yè)務確認清楚不要為了技術上的“標準”而犧牲使用體驗。1.3 改造前的三個核心問題正式動手前我建議你先重新審視下面三件事它們決定改造方案的形態(tài)頁面邊界怎么劃。哪些功能算一個獨立頁面判斷標準是上線節(jié)奏、維護團隊、入口來源而非功能菜單的層級。菜單里的二級頁面通常不該拆成獨立入口。公共代碼放哪里。登錄態(tài)、請求封裝、公共組件、工具函數是每個頁面都要用的這部分要抽成公共目錄但要注意別把頁面特有的路由配置也塞進公共目錄。部署路徑怎么定。是根路徑部署還是子路徑部署這直接影響 Vite 的base配置和前端路由的base參數。這塊不先想明白后面構建出的資源路徑大概率會踩坑。2. 改造前的目錄重構與入口規(guī)劃2.1 推薦的多頁面目錄結構Vite 官方文檔對 MPA 的推薦做法是配置多個 HTML 入口。但在真實項目中HTML 文件放哪里、腳本和頁面代碼怎么組織直接決定后續(xù)好不好維護。我改造后最終采用的目錄結構是這樣的├── src │ ├── common │ │ ├── api │ │ ├── components │ │ ├── hooks │ │ └── utils │ ├── pages │ │ ├── home │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ ├── App.vue │ │ │ └── router │ │ │ └── index.js │ │ └── admin │ │ ├── index.html │ │ ├── main.js │ │ ├── App.vue │ │ └── router │ │ └── index.js ├── vite.config.js └── package.json每個頁面目錄都是一個“迷你 Vue 應用”有自己的 HTML、入口腳本、根組件和路由。src/common放跨頁面共享的代碼。這樣做的直觀好處是新接一個頁面的成本非常低復制一個目錄改一下入口配置就能跑起來。2.2 獨立入口 HTML 的放置方案對比HTML 文件的放置位置我見過兩種做法第一種是把所有 HTML 都放在項目根目錄比如根目錄下的home.html、admin.htmlVite 配置里直接引用這些文件。優(yōu)點是最貼近官方文檔的寫法缺點是一旦頁面多起來根目錄會被 HTML 文件堆滿而且 HTML 和它的入口腳本距離很遠改起來要多跳幾層目錄。第二種就是我上面用的方案HTML 放在各自頁面目錄下與頁面代碼內聚。Vite 配置里指向src/pages/home/index.html即可。這種方式在工程上更清晰我強烈推薦。有一個細節(jié)要注意HTML 里引入口腳本時盡量使用相對路徑./main.js而不是以/src/pages/home/main.js開頭的根路徑寫法。相對路徑在 dev 和 build 下都不容易出問題后面調整base時也更省心。2.3 公共模塊與業(yè)務模塊的邊界劃分改造過程中最容易犯的錯是把公共模塊當成“垃圾桶”。我在實際操作中的劃分標準很簡單如果一段代碼會被兩個及以上頁面引用才放進src/common否則就留在頁面目錄內部。比如 axios 實例封裝、用戶登錄信息存取、通用 UI 組件、日期格式化這類工具屬于典型的公共模塊。但某個頁面獨有的接口請求、某個業(yè)務域的表格組件就應該留在對應頁面目錄里。這樣劃分后構建時不會出現頁面 A 的代碼被打包進頁面 B 的產物這種尷尬情況。另外建議在vite.config.js里配置好別名指向src目錄并順手配置common指向src/commonimport { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ resolve: { alias: { : resolve(__dirname, src), common: resolve(__dirname, src/common) } }, plugins: [vue()] })這里有個 ESM 環(huán)境的坑如果package.json里設置了type: modulevite.config.js里直接使用__dirname會報錯需要改用fileURLToPath(new URL(./src, import.meta.url))來轉換。順手避坑別等報錯再查。3. Vite 配置多入口從 dev 到 build3.1 核心配置說明多頁面改造最核心的就是build.rollupOptions.input。這個配置告訴 Vite 構建時要把哪些 HTML 作為入口分別打包。我改造后的配置如下import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ resolve: { alias: { : resolve(__dirname, src), common: resolve(__dirname, src/common) } }, build: { rollupOptions: { input: { home: resolve(__dirname, src/pages/home/index.html), admin: resolve(__dirname, src/pages/admin/index.html) } } }, plugins: [vue()] })input 對象里的 keyhome、admin會直接決定構建產物的文件名。比如home對應的入口構建后會在dist目錄生成home.html而不是默認的index.html。如果你希望部署后用戶直接訪問域名根路徑就打開某個頁面可以把這個頁面的 key 設置為index構建生成index.html。我實際項目中通常保留一個index入口作為默認落地頁其余頁面按功能命名。如果后續(xù)新增頁面只需要在 input 里加一行同時在src/pages下新建對應的目錄結構兩步就能完成一個頁面的接入。3.2 dev server 如何訪問多頁面配置完 input 后很多人第一反應是執(zhí)行npm run dev然后訪問http://localhost:5173/發(fā)現頁面白屏或者 404。這不是配置錯了而是 dev server 默認展示根目錄下的index.html而你的入口 HTML 現在散落在各個頁面目錄里。多頁面下 dev 環(huán)境正確的訪問方式是帶路徑訪問http://localhost:5173/src/pages/home/index.html。這樣確實有些難看而且每次都要手打長路徑。更優(yōu)雅的做法是在 Vite 配置里加一個自定義中間件把根路徑重定向到固定入口export default defineConfig({ plugins: [ vue(), { name: mpa-redirect, configureServer(server) { server.middlewares.use((req, res, next) { if (req.url /) { res.statusCode 302 res.setHeader(Location, /src/pages/home/index.html) res.end() return } next() }) } } ] })這樣開發(fā)時訪問根路徑就會自動跳到 home 頁面體驗和單頁面應用沒有差別。額外提醒一點server.open配置這時候也要寫成具體的頁面路徑比如server: { open: /src/pages/home/index.html }否則自動打開瀏覽器時還是會 404。3.3 base 路徑與靜態(tài)資源處理base是 Vite 里一個非常關鍵但又容易被忽略的配置。它的作用是給構建后的 HTML 中引用的 JS、CSS、圖片等資源統(tǒng)一加路徑前綴。如果你的項目要部署在域名根路徑比如https://example.com/base保持默認的/即可構建后的資源路徑是/assets/xxx.js。如果部署在子路徑比如https://example.com/admin/就必須設置base: /admin/否則資源路徑會變成/assets/xxx.js請求打到域名根路徑下直接 404。多頁面 子路徑部署還有一個特殊坑每個頁面可能部署在不同的子路徑下。比如 home 頁面在根路徑部署admin 頁面在/admin/下部署。這種場景下一個 Vite 全局base無法滿足兩個頁面的不同需求。我的處理方案是分開構建。用環(huán)境變量區(qū)分部署場景加載不同的baseexport default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) return { base: env.VITE_BASE || /, // 其他配置 } })然后準備兩套.env文件.env.home里設置VITE_BASE/.env.admin里設置VITE_BASE/admin/。構建時執(zhí)行vite build --mode adminadmin 頁面的資源路徑就會自動帶上/admin/前綴。這里確實要提前規(guī)劃好部署方案否則后面返工成本很高。3.4 按需構建指定頁面多頁面項目還有一個非常實用的進階配置按需構建。比如你只想單獨打包 admin 頁面而不想每次發(fā)布都把所有頁面構建一遍。結合熱詞里提到的vite build --mode test我們可以用 mode 和 loadEnv 實現這個需求。具體做法是在.env.test中配置要構建的頁面列表VITE_BUILD_PAGESadmin然后在vite.config.js中用loadEnv讀取動態(tài)生成 inputimport { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) const allPages { home: resolve(__dirname, src/pages/home/index.html), admin: resolve(__dirname, src/pages/admin/index.html) } let input allPages if (env.VITE_BUILD_PAGES) { const pageNames env.VITE_BUILD_PAGES.split(,) input pageNames.reduce((acc, name) { if (allPages[name]) { acc[name] allPages[name] } return acc }, {}) } return { base: env.VITE_BASE || /, build: { rollupOptions: { input } }, plugins: [vue()] } })執(zhí)行vite build --mode test時只會構建 admin 頁面。對于幾十個入口的大型項目這個配置能顯著縮短發(fā)布耗時。我的建議是把allPages對象單獨抽到一個pages.js文件里維護起來更清晰。4. 多頁面下的代碼組織與公共復用4.1 頁面級路由拆分配置層面搞定后接下來是業(yè)務代碼層面的調整。每個頁面需要有自己的路由實例不能再像 SPA 那樣把所有路由都集中在根目錄的router/index.js里。我在頁面目錄下各自維護一份router/index.js示例import { createRouter, createWebHashHistory } from vue-router const routes [ { path: /, component: () import(../views/Dashboard.vue) }, { path: /list, component: () import(../views/List.vue) } ] const router createRouter({ history: createWebHashHistory(), routes }) export default router這里我推薦用createWebHashHistory而不是createWebHistory。原因比較現實多頁面每個入口都是一個獨立應用如果用 history 模式用戶刷新某個子路由時服務器必須正確配置回退規(guī)則。而多頁面本身已經有多套部署路徑再疊加 history 回退規(guī)則nginx 配置會變得復雜且脆弱。hash 模式雖然 URL 里多個#但在多頁面拆分場景下穩(wěn)定性和免部署配置的優(yōu)勢非常明顯。如果業(yè)務確實要求 history 模式那么createWebHistory的 base 參數要傳對應頁面的部署子路徑比如createWebHistory(/admin/)并且 nginx 要對這個子路徑配置try_files回退下面第 5 章會給出檢查清單。4.2 頁面入口腳本與狀態(tài)初始化每個入口的main.js大同小異我在 home 頁面的入口腳本長這樣import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import router from ./router const app createApp(App) const pinia createPinia() app.use(pinia) app.use(router) app.mount(#app)這里特別提醒一個多頁面場景下的狀態(tài)管理問題每個頁面都是獨立的應用實例所以每個入口里都要createPinia()不能像 SPA 那樣全局共享一個 store。如果你在頁面之間使用了 localStorage 或 sessionStorage 做登錄態(tài)持久化還需要在入口腳本里顯式初始化并校驗用戶狀態(tài)。我實際踩過的一個坑是改造前 SPA 里有一個全局的userStore登錄后存在內存里路由跳轉也能拿到。改造后 A 頁面和 B 頁面各是一個應用內存中的狀態(tài)完全不互通刷新后 store 直接空了。最后統(tǒng)一改為從 localStorage 讀取并校驗 token在各自入口的main.js里完成初始化問題才解決。4.3 公共復用策略與構建踩包優(yōu)化在改造前SPA 里公用的組件和工具函數大概率分布在各層目錄中。改造后我建議統(tǒng)一收斂到src/common下并且只保留真正跨頁面復用的代碼。但是更重要的是公共代碼在構建時容易重復打包的問題。我用一個具體數字說明沒配置拆包前home 和 admin 兩個頁面構建產物里各自都包含了一份完整的 Vue 運行時。兩個頁面加起來Vue 相關代碼被打了兩次整體體積增加了將近 120KB。這是因為每個入口都是獨立的 Rollup 入口公共依賴默認跟隨入口各自打包。解決辦法是用manualChunks把第三方依賴單獨拆出來build: { rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(vue) || id.includes(pinia) || id.includes(vue-router)) { return vue-vendor } if (id.includes(echarts) || id.includes(lodash)) { return utils-vendor } return vendor } } } } }配置后home 和 admin 頁面共享的第三方庫會被抽取成公共 chunk瀏覽器第一次訪問某個頁面后第二個頁面就能命中緩存。這個配置在頁面數量增多后收益很大建議在改造一開始就加上。這里也要留個心眼manualChunks不是拆得越細越好。拆分粒度過小會導致很多小文件HTTP 請求數增多反而影響加載性能。我的經驗是把 Vue 生態(tài)單獨拆一個把體積較大的可視化庫、工具庫拆一個業(yè)務代碼保持跟隨頁面構建即可。4.4 頁面級 HTML 的標題與 SEO 處理多頁面相比 SPA 還有一個附帶紅利每個頁面可以獨立設置 HTML 的 title、description、keywords不需要依賴document.title這種運行時手段。我在每個頁面的index.html里直接寫死對應頁面的內容!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / meta namedescription content運營數據看板 - 實時掌握業(yè)務核心指標 / title數據看板/title /head body div idapp/div script typemodule src./main.js/script /body /html如果頁面是面向用戶的落地頁這種靜態(tài) SEO 信息比 SPA 的運行時注入更友好爬蟲直接讀取 HTML 就能拿到主要內容描述。這是改造成 MPA 后幾乎零成本的收益順手就做了。5. 構建優(yōu)化與踩坑排查實錄5.1 常見報錯與解決辦法速查表改造過程中我整理了一批高頻問題很多都是多人團隊里反復被問到的直接列一個表格方便對癥狀查問題問題現象根本原因解決辦法dev 環(huán)境訪問根路徑 404未配置重定向中間件在 configureServer 里加根路徑重定向build 后頁面白屏控制臺 JS/CSS 404vite.config 的 base 與實際部署路徑不一致設置正確的 base 或按環(huán)境構建HTML 中 script src 路徑解析錯誤入口腳本路徑寫成了絕對路徑改為相對路徑如./main.js組件中import img from /assets/xxx.png構建后圖片路徑錯誤alias 路徑在構建資源引用時不統(tǒng)一改用new URL(./assets/xxx.png, import.meta.url)或相對路徑input 配置里寫resolve(__dirname)報錯項目啟用了 ESM 的type: module改用fileURLToPath(new URL(./src/pages/xxx/index.html, import.meta.url))多個入口構建后 Vue 被重復打包未配置 manualChunks配置 output.manualChunks 抽取公共依賴刷新某個子路由 404history 模式未做服務端回退改用 hash 模式或為對應路徑配置 try_files某個頁面引用了其他頁面的組件導致產物膨脹跨頁面目錄直接 import公共組件抽到 src/common 下頁面私有組件留在頁面目錄5.2 Windows 環(huán)境下的 NODE_OPTIONS 命令坑熱詞里能看到一串$ node_options--max-old-space-size4096 vite并且后面跟著 “node_options 不是內部或外部命令” 這種報錯。這個我在 Windows 環(huán)境下也遇到過。問題出在 Windows 的 cmd 和 PowerShell 不認 Linux 風格的VARvalue command寫法。命令行直接設置環(huán)境變量Windows 要用setset NODE_OPTIONS--max-old-space-size4096 vite build但這樣每次都要手動敲而且容易忘記。更好的方案是在package.json里用cross-env統(tǒng)一處理{ scripts: { build:home: cross-env NODE_OPTIONS--max-old-space-size4096 vite build --mode home, build:admin: cross-env NODE_OPTIONS--max-old-space-size4096 vite build --mode admin } }注意環(huán)境變量名必須大寫NODE_OPTIONS同時--max-old-space-size4096表示將 Node 的堆內存上限提升到 4GB。多頁面項目入口多、依賴多時構建進程出現JavaScript heap out of memory的概率明顯高于單頁面提前把這個配置加進構建腳本能省去不少線上構建失敗后排查的焦慮。5.3 構建體積與緩存策略多頁面改造完成后我建議在dist產物目錄下核對一下各頁面 HTML 的大小和資源引用路徑。正常情況下每個頁面的 HTML 應該只有幾 KB里面的 JS 路徑指向assets目錄下帶 hash 的文件。Vite 默認對 JS 和 CSS 文件名加了內容 hash這是靜態(tài)資源長效緩存的基石。每次發(fā)布只有內容變化的文件 hash 會變未變化的部分能繼續(xù)命中瀏覽器緩存。不要手動關掉這個行為除非你有極強的理由。對于公共 chunk還可以在服務器端設置更長的緩存時間。比如 Vue 運行時這類極少變動的文件nginx 里可以緩存一年location /assets/ { expires 1y; add_header Cache-Control public, immutable; }但要注意HTML 文件本身不要設置長緩存。因為 HTML 里引用的資源路徑帶有 hash如果用戶命中了舊 HTML就還會去請求舊資源導致發(fā)布后用戶看到舊版本。建議 HTML 設置no-cache讓瀏覽器每次回源校驗。5.4 上線部署的檢查步驟最后是部署環(huán)節(jié)。多頁面項目部署前我一般按以下清單逐項檢查避免線上翻車確認base是否匹配部署路徑。子路徑部署時頁面上的靜態(tài)資源請求路徑前綴應該與部署目錄一致。確認 nginx 靜態(tài)文件 root 指向構建產物目錄。比如構建產物在dist根目錄部署時root /var/www/dist;子路徑部署時location /admin/ { alias /var/www/dist/; }。確認路由模式。hash 模式無需額外配置history 模式需要在對應 location 里配置try_files $uri $uri/ /admin/index.html;注意這里不能直接寫根路徑的 index.html。確認接口代理是否生效。如果 dev 環(huán)境用 Vite 的 proxy 代理了/api線上也要在 nginx 里配置對應的proxy_pass否則所有接口請求都會打到靜態(tài)資源服務器上。確認各入口 HTML 的 title、meta 符合預期打開頁面后逐個抽查。開發(fā)環(huán)境和構建環(huán)境的base不同時尤其要注意構建后的資源引用路徑。這一套檢查下來基本能覆蓋多頁面部署的絕大多數問題。6. 我的幾點體會多頁面改造這件事配置本身并不復雜真正的復雜度在工程結構設計和歷史代碼的合規(guī)性改造上。我從單體 SPA 改動到三入口 MPA前期最花時間的不是寫 Vite 配置而是梳理哪些代碼是頁面私有的、哪些是公共的以及登錄態(tài)怎么跨頁面保持。如果你也是從既有項目改造建議先做代碼結構梳理再動vite.config.js順序反了會不斷返工。最后分享一個小技巧改造時可以先用一個最簡單的頁面比如一個登錄頁走通 MPA 全鏈路確認 dev、build、部署都正常后再批量遷移其他頁面。不要一次性把全部頁面遷完再驗證問題范圍會大到你不想面對。