Vue3 + Vite 打包后打开空白?

🛠️ 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(如 PromiseArray.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:// 协议和现代前端工程化之间的"代沟"。
填平它,只需要上面三把铲子。⛏️


如果这篇文章帮你解决了问题,欢迎收藏转发! 🎉

有问题欢迎在评论区留言,看到都会回复~ 💬

相关推荐
张龙6871 小时前
终端效率翻倍实战:fzf + zoxide + ripgrep + bat 组合拳,告别重复敲命令
前端
宿6741 小时前
vue3-env环境
前端·vue.js
蔬菜_1 小时前
前端转全栈-day5(数组、list、set)
java·前端·数据结构·list
汉堡大王95271 小时前
Vue 3 + TS + Element Plus 实战:如何从零搭建企业级违章记录管理 SaaS 前端
前端·javascript·vue.js
minimoon_jojo1 小时前
Ant Design 树形表格渲染原理
前端
paopaokaka_luck1 小时前
基于springboot3+vue3的音乐推荐系统(协同过滤算法、Echarts图形化分析)
前端·echarts
八号当铺2 小时前
使用 Figma Agent Kit:插件 + MCP + 还原 Skill,打通本地设计协作
前端·人工智能·ai编程
一心只读圣贤书3 小时前
AI 辅助前端国际化实践:从文案梳理到多语言资源治理
前端
无责任此方_修行中3 小时前
搓了一个国产大模型与 AI Agent 比价工具
前端·后端·ai编程