🛠️ Vue3 + Vite 打包后打开空白?
三招教你从"白屏恐惧"中解脱
Vue 3 Vite Build Bug Fix
🎬 场景还原
你吭哧吭哧写完了 Vue3 项目,
npm run build一气呵成,dist文件夹新鲜出炉。你满怀期待地双击打开dist/index.html,结果------一片空白。 😱
F12 打开控制台,一堆红色报错像烟花一样炸开。你的心,也跟着凉了半截。
别慌,这篇文章就是来救你的。
🔍 问题诊断:先搞清楚"病"在哪
在动手治疗之前,我们先做个"体检"。打开 dist/index.html,按 F12 看看控制台报了什么错:
vbnet
┌─────────────────────────────────────────────────────────────┐
│ 🩺 常见报错对照表 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ❌ Failed to load resource: net::ERR_FILE_NOT_FOUND │
│ → 文件路径问题(第 1 招) │
│ │
│ ❌ Access to script at 'file://...' from origin 'null' │
│ has been blocked by CORS policy │
│ → 跨域问题(第 2 招) │
│ │
│ ❌ Loading module from "file://..." was blocked │
│ because of a disallowed MIME type │
│ → ES Module + file 协议冲突(第 2 招) │
│ │
│ ❌ 页面空白,但控制台没报错,路由切换无效 │
│ → 路由模式问题(第 3 招) │
│ │
└─────────────────────────────────────────────────────────────┘
对号入座,找到你的"病因",然后对症下药。
💊 第一招:修正资源路径 ------ base: './'
1.1 问题现象
控制台报错:
arduino
Failed to load resource: net::ERR_FILE_NOT_FOUND
或者打开 dist/index.html 的源码,发现 <script> 标签的 src 长这样:
html
<script type="module" crossorigin src="/assets/index-abc123.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-abc123.css">
注意那个 / 开头!这意味着浏览器会从根目录 去找 assets 文件夹。但你是用 file:// 协议直接打开 HTML 文件的,哪来的根目录?
1.2 原理讲解
bash
🌲 文件路径解析差异
开发环境(vite dev) 生产环境(file:// 打开)
├─ http://localhost:5173/ ├─ file:///C:/Users/xxx/dist/index.html
│ ├─ /assets/... │ ├─ /assets/... ← ❌ 从 C盘根目录找?
│ │ ↑ 根目录是项目根目录 │ │ ↑ 根目录是系统根目录!
│ │ 能正确找到 │ │ 根本找不到!
解决方案:把路径改成相对路径
<script src="./assets/index-abc123.js"></script>
↑ 从 index.html 所在目录开始找
✅ 无论在哪打开,都能找到!
1.3 解决方案
在 vite.config.js(或 vite.config.ts)中添加 base 配置:
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
// 🎯 关键配置:使用相对路径
base: './',
// 或者,如果你要部署到子目录(如 /my-app/)
// base: '/my-app/',
})
💡
base配置说明:
base: '/'(默认)→ 绝对路径,适合部署到域名根目录base: './'→ 相对路径,适合本地打开或部署路径不确定base: '/my-app/'→ 指定子目录,适合部署到 GitHub Pages 等
修改后重新 npm run build,生成的 index.html 会变成:
html
<script type="module" crossorigin src="./assets/index-abc123.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-abc123.css">
双击打开,资源就能正确加载了!🎉
💊 第二招:解决跨域与 ES Module 冲突 ------ @vitejs/plugin-legacy
2.1 问题现象
修正路径后,你可能还会遇到这样的报错:
vbnet
Access to script at 'file:///C:/.../dist/assets/index.js'
from origin 'null' has been blocked by CORS policy.
或者:
typescript
Loading module from "file:///C:/.../dist/assets/index.js"
was blocked because of a disallowed MIME type ("text/plain").
2.2 原理讲解
sql
🧨 为什么 file:// 协议下 ES Module 会爆炸?
┌─────────────────────────────────────────────────────────────┐
│ │
│ Vite 打包默认输出 ES Module(type="module") │
│ │
│ ES Module 的加载规则: │
│ 1. 必须遵守同源策略(CORS) │
│ 2. file:// 协议没有"源"的概念(origin 是 null) │
│ 3. 所以浏览器认为:file:// 加载 file:// = 跨域!😂 │
│ │
│ 结果:浏览器直接拒绝加载,页面一片空白 │
│ │
│ 解决方案:让 Vite 同时生成传统脚本(非 ES Module) │
│ 传统 script 标签不受 CORS 限制! │
│ │
└─────────────────────────────────────────────────────────────┘
2.3 解决方案
安装 @vitejs/plugin-legacy 插件:
bash
npm install @vitejs/plugin-legacy -D
# 或
yarn add @vitejs/plugin-legacy -D
# 或
pnpm add @vitejs/plugin-legacy -D
在 vite.config.js 中引入并配置:
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
vue(),
// 🎯 关键配置:生成兼容版本
legacy({
// 目标浏览器,设置支持的最低版本
targets: ['defaults', 'not IE 11'],
// 为现代浏览器生成原生 ES Module
// 为旧浏览器生成 SystemJS 格式的 polyfill 版本
modernPolyfills: true,
// 是否生成 legacy 包(非 ES Module)
// 这个包可以用传统 script 标签加载,不受 CORS 限制
renderLegacyChunks: true,
}),
],
base: './', // 别忘了第一招的配置
})
2.4 这个插件做了什么?
perl
📦 打包产物变化
没装 plugin-legacy 时:
├─ dist/
│ ├─ index.html
│ └─ assets/
│ ├─ index-xxx.js ← ES Module,file:// 打不开
│ └─ index-xxx.css
装了 plugin-legacy 后:
├─ dist/
│ ├─ index.html
│ └─ assets/
│ ├─ index-xxx.js ← ES Module(现代浏览器)
│ ├─ index-xxx-legacy.js ← 传统脚本(兼容模式)👈 救星!
│ ├─ polyfills-legacy.js ← 必要的 polyfill
│ └─ index-xxx.css
index.html 会自动插入一段特性检测脚本:
html
<script type="module">
// 检测浏览器是否支持 ES Module 动态导入
import("./assets/index-xxx.js");
</script>
<script nomodule>
// 不支持 ES Module 的浏览器会执行这段
// 加载 legacy 版本
var script = document.createElement('script');
script.src = './assets/index-xxx-legacy.js';
document.head.appendChild(script);
</script>
nomodule 是个神奇的属性:支持 ES Module 的浏览器会忽略它,不支持的浏览器会执行它。完美 fallback!
💡 额外福利 :这个插件还会自动注入必要的 polyfill(如
Promise、Array.prototype.includes等),让你的项目兼容更多旧浏览器。
💊 第三招:切换路由模式 ------ createWebHashHistory
3.1 问题现象
前两招都用了,资源加载没问题,但页面还是空白。或者首页能显示,点击路由跳转就白屏,控制台报错:
arduino
Failed to load resource: net::ERR_FILE_NOT_FOUND
URL 栏显示的是 file:///C:/.../dist/index.html/about 这样的奇怪地址。
3.2 原理讲解
bash
🗺️ 两种路由模式的本质区别
┌─────────────────────────────────────────────────────────────┐
│ createWebHistory() ------ History 模式 │
│ │
│ URL 样子:http://example.com/about │
│ │
│ 原理:利用 History API(pushState/replaceState) │
│ 改变 URL 但不刷新页面 │
│ │
│ ⚠️ 问题:需要服务器配合! │
│ 当用户直接访问 /about 时,服务器必须返回 index.html │
│ file:// 没有服务器,直接访问 /about 会找不到文件 │
│ │
├─────────────────────────────────────────────────────────────┤
│ createWebHashHistory() ------ Hash 模式 │
│ │
│ URL 样子:http://example.com/#/about │
│ │
│ 原理:利用 URL 的 hash 部分(# 后面的内容) │
│ hash 变化不会触发页面刷新,纯前端处理 │
│ │
│ ✅ 优势:不需要服务器配合! │
│ 无论 URL 怎么变,浏览器始终只请求 index.html │
│ 路由切换完全由前端控制,file:// 也能正常工作 │
│ │
└─────────────────────────────────────────────────────────────┘
3.3 解决方案
修改 src/router/index.js:
javascript
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = createRouter({
// ❌ 原来可能是这样(History 模式)
// history: createWebHistory(import.meta.env.BASE_URL),
// ✅ 改成这样(Hash 模式)
history: createWebHashHistory(),
routes
})
export default router
修改后,路由 URL 会从:
perl
file:///C:/.../dist/index.html/about ← ❌ 找不到文件
变成:
perl
file:///C:/.../dist/index.html#/about ← ✅ 正常显示
⚠️ 注意 :Hash 模式虽然解决了本地打开的问题,但 URL 里会有个
#,不太美观。如果你要部署到服务器,建议改回createWebHistory,并在服务器配置 URL 重写规则(如 Nginx 的try_files)。
🧪 完整配置参考
vite.config.js
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['defaults', 'not IE 11'],
modernPolyfills: true,
renderLegacyChunks: true,
}),
],
// 🎯 第一招:相对路径
base: './',
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
outDir: 'dist',
assetsDir: 'assets',
// 可选:代码分割配置
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
},
},
},
},
})
src/router/index.js
javascript
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue') // 懒加载
}
]
const router = createRouter({
history: createWebHashHistory(), // 🎯 第三招:Hash 模式
routes
})
export default router
📋 排查清单:三步走
✅ 打包后本地打开检查清单
| 步骤 | 检查项 | 配置位置 |
|---|---|---|
| Step 1 | vite.config.js 中是否设置了 base: './' |
vite.config.js |
| Step 2 | 是否安装了 @vitejs/plugin-legacy 并配置 |
vite.config.js |
| Step 3 | 路由是否使用 createWebHashHistory() |
src/router/index.js |
🟢 如果以上三步都完成,双击 index.html 应该能正常显示了!
💡 进阶:为什么建议用服务器打开?
虽然上面三招能让你双击打开 index.html,但强烈建议用本地服务器预览:
bash
# 方法一:Vite 自带的预览
npm run preview
# 方法二:安装 serve
npm install -g serve
serve dist
# 方法三:Python 临时服务器(如果你装了 Python)
cd dist
python -m http.server 8080
# 方法四:VS Code 的 Live Server 插件
# 右键 index.html → Open with Live Server
为什么?
perl
┌─────────────────────────────────────────────────────────────┐
│ file:// 协议 vs http:// 协议 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ❌ file:// 协议 │
│ · 有 CORS 限制 │
│ · 没有 Cookie/LocalStorage 的域隔离 │
│ · 某些 API(如 fetch 本地文件)行为异常 │
│ · 路由 History 模式无法工作 │
│ · 不适合真实环境测试 │
│ │
│ ✅ http:// 协议(本地服务器) │
│ · 和线上环境行为一致 │
│ · 支持所有 Web API │
│ · 可以用 History 路由模式 │
│ · 可以测试接口请求、Cookie 等 │
│ · 这才是"正经"的测试方式 │
│ │
└─────────────────────────────────────────────────────────────┘
🎯 最佳实践 :开发时用
npm run dev,打包后用npm run preview,部署到服务器后用createWebHistory()。file://打开只是"应急方案",不是"常态"。
🏁 总结
🎯 三招制敌
第一招 base: './' ------ 让资源路径"脚踏实地"
第二招 @vitejs/plugin-legacy ------ 让 ES Module"退一步海阔天空"
第三招 createWebHashHistory() ------ 让路由"不依赖服务器也能跑"
记住:打包后双击打开空白,不是 Vue 的错,不是 Vite 的错,
是 file:// 协议和现代前端工程化之间的"代沟"。
填平它,只需要上面三把铲子。⛏️
如果这篇文章帮你解决了问题,欢迎收藏转发! 🎉
有问题欢迎在评论区留言,看到都会回复~ 💬