告别 Webpack 时代的龟速!我的 Vue3 项目 Vite 升级实战复盘
前言
相信很多使用 Vue CLI (Webpack) 的同学都深有体会:随着项目规模的增长,那缓慢的冷启动速度和不尽人意的热更新(HMR)反馈,逐渐成了开发流程中的一个痛点。我的项目也不例外,一个基于 Vue 3 + Webpack 4 的中后台系统,启动时间一度达到了令人抓狂的几十秒。
为了追求极致的开发体验和更优的生产性能,我下定决心,将其升级到当前最火的构建工具 —— Vite!本文将完整记录我从 Webpack 迁移到 Vite 的全过程,包括详细的步骤、遇到的各种报错(全是干货)以及最终惊人的性能提升。
成果展示:性能提升不止亿点点!
先上结果,让大家看看这次升级的价值有多大:
- 打包体积:从 12MB 优化到了 8MB,减少了约 33% 。
- 初次加载时间:从平均 20秒 缩短到了 5-6秒,性能提升约 60-65% !
- 开发服务器启动:从分钟级提升到了秒级,几乎是瞬间启动。
这一切的努力,都是值得的!
正文:迁移步骤详解
迁移过程主要分为以下几个核心步骤:
第一步:依赖管理——“断舍离”
首先,我们需要卸载 Vue CLI 和 Webpack 的相关依赖,换上 Vite 的核心套件。
-
卸载旧依赖:
Bash
npm uninstall @vue/cli-plugin-babel @vue/cli-plugin-eslint @vue/cli-plugin-router @vue/cli-plugin-vuex @vue/cli-service -
安装新依赖:
Bash
npm install vite @vitejs/plugin-vue --save-dev # 别忘了更新 less 等 CSS 预处理器 npm install less -D
第二步:配置文件迁移 (vue.config.js -> vite.config.js)
这是迁移的核心,我们需要将 vue.config.js 的配置“翻译”成 vite.config.js 的格式。
publicPath->basedevServer->servercss.loaderOptions->css.preprocessorOptionsconfigureWebpack.optimization.splitChunks->build.rollupOptions.output.manualChunksBundleAnalyzerPlugin->rollup-plugin-visualizer(需要安装rollup-plugin-visualizer)
第三步:index.html 的改造
Vite 将 index.html 视为应用的入口。
-
移动文件:将
public/index.html移动到项目根目录。 -
修改内容:
- 移除所有 Webpack 的 EJS 模板语法(如
<%= BASE_URL %>)。 - 在
<body>底部添加入口脚本:<script type="module" src="/src/main.js"></script>。
- 移除所有 Webpack 的 EJS 模板语法(如
第四步:环境变量的适配
Vite 加载环境变量的方式与 Vue CLI 不同。
- 修改前缀:将
.env文件中的VUE_APP_前缀全部改为VITE_。 - 修改访问方式:在业务代码中,通过
import.meta.env.VITE_XXX访问变量,而不是process.env.VUE_APP_XXX。
第五步:修改 package.json -> script
"scripts": {
"dev": "vite",
"build": "vite build",
"build:test": "vite build --mode test",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"prettier": "npx prettier --write ."
},
踩坑与填坑:Vite 迁移中的常见报错及解决方案
这部分是本文的精华,记录了我在迁移过程中遇到的几乎所有报错和解决方案。
坑一:less 版本冲突 (ERESOLVE)
-
报错信息:
Conflicting peer dependency: less@4.x.x -
原因:旧项目中的
less-loader依赖了less@3.x,而 Vite 需要less@4.x。 -
解决方案:Vite 自带 Less 支持,不再需要
less-loader。直接卸载它,并更新less即可。Bash
npm uninstall less-loader npm install less --save-dev
坑二:@ 路径别名失效
-
报错信息:
Failed to resolve import "@/components/..." -
原因:Vite 不会自动创建
@指向/src的别名。 -
解决方案:在
vite.config.js中手动配置resolve.alias。JavaScript
import path from 'path' // vite.config.js export default { // ... resolve: { alias: { '@': path.resolve(__dirname, 'src') } } }
坑三:require() 语法报错
-
报错信息:
require is not defined -
原因:
require是 CommonJS 的语法,Vite 使用的是浏览器原生支持的 ES Modules。 -
解决方案:
-
静态资源:使用
import导入。JavaScript
// before const img = require('@/assets/logo.png'); // after import img from '@/assets/logo.png'; -
动态资源(如在变量或循环中):使用
new URL('...', import.meta.url).href。JavaScript
// before { icon: require('@/assets/icon.svg') } // after { icon: new URL('@/assets/icon.svg', import.meta.url).href }
-
坑四:模块导入/导出不匹配
-
报错信息:
does not provide an export named 'debounce' -
原因:代码中使用了命名导入
import { debounce } from '...',但实际文件使用了默认导出export default debounce。 -
解决方案:修改导入方式,去掉花括号
{}。JavaScript
// before import { debounce } from '@/utils/debounceThrottle.js'; // after import debounce from '@/utils/debounceThrottle.js';
坑五:生产打包后的 MIME Type 错误
-
报错信息:
Failed to load module script: ... MIME type of "text/html" -
原因:
vite.config.js中的base配置为'/'(绝对路径),但打包后直接通过file:///协议打开index.html。这导致浏览器无法正确找到 JS 文件。 -
解决方案:不要直接打开文件!使用 Vite 内置的
preview命令来启动一个本地静态服务器进行预览。Bash
# 1. 先打包 npm run build # 2. 再预览 npm run preview
进阶优化:画龙点睛的预加载
在基础迁移完成后,我还引入了空闲时间预加载的策略,进一步提升了用户体验。当主页加载完成且浏览器空闲时,后台会悄悄地加载用户接下来可能会访问的页面组件。这样当用户真正点击时,页面几乎可以“秒开”,交互体验非常流畅。
JavaScript
// preloadAssets.js
// 利用 requestIdleCallback 在浏览器空闲时执行
requestIdleCallback(() => {
// 使用动态 import() 预加载路由组件
import('@/views/some-heavy-page/index.vue');
});
可参考文章 juejin.cn/post/755316…
这个策略和 Vite 自身的 modulepreload 形成了很好的互补,一个是应用层面的逻辑预加载,一个是浏览器层面的资源预加载。
总结
从 Webpack 迁移到 Vite 的过程虽然遇到了一些挑战,但每解决一个问题,都让我对现代前端工程化有了更深的理解。最终,项目开发体验和线上性能的巨大提升,证明了这次升级是完全值得的。
如果你还在忍受 Webpack 的缓慢,别再犹豫了,快来拥抱 Vite 吧!希望这篇实战记录能为你提供一份可靠的避坑指南。
附完整的 vite.config.js
根据自身依赖调整 manualChunks、plugins 等配置。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import { loadEnv } from 'vite'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
console.log('当前环境:', mode)
const isProduction = mode === 'production';
const env = loadEnv(mode, process.cwd(), '')
// List of vendor chunks that should not have a hash
const vendorChunkNames = [
'vue-core',
'ui-vendor',
'utils-vendor',
'misc-vendor'
];
return {
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'vue-router',
{
'ant-design-vue': ['message']
},
'vuex'
],
dts: 'src/auto-imports.d.ts'
}),
// 在生产环境中生成打包分析报告
isProduction && visualizer({
open: false,
gzipSize: true,
brotliSize: true,
filename: 'bundle-report.html'
})
],
// CSS 相关配置
css: {
preprocessorOptions: {
less: {
modifyVars: {
'primary-color': '#5975fb',
'link-color': '#5975fb',
'border-radius-base': '4px'
},
javascriptEnabled: true
}
}
},
// 构建输出配置
build: {
outDir: `cephr-web`,
assetsDir: 'assets',
sourcemap: !isProduction,
// 配置模块预加载策略
modulePreload: {
polyfill: true
},
// 构建优化配置
chunkSizeWarningLimit: 1000,
rollupOptions: {
// 缓存优化:确保 vendor 包的稳定性
external: (id) => {
// 不要将这些库外部化,保持它们在 vendor chunk 中
return false
},
output: {
// 自定义 chunk 文件名 - 对 vendor chunk 不使用 hash
chunkFileNames: (chunkInfo) => {
if (vendorChunkNames.includes(chunkInfo.name)) {
return `js/vendor/${chunkInfo.name}.js`;
}
const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop().replace(/\.[^.]*$/, '') : 'chunk'
return `js/${chunkInfo.name || facadeModuleId}-[hash].js`
},
// 使用 [hash] 修复错误
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const extType = assetInfo.name.split('.').pop()
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i.test(assetInfo.name)) {
return 'assets/media/[name]-[hash].[ext]'
}
if (/\.(png|jpe?g|gif|svg|ico|webp)$/i.test(assetInfo.name)) {
return 'assets/images/[name]-[hash].[ext]'
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return 'assets/fonts/[name]-[hash].[ext]'
}
return `assets/[name]-[hash].[ext]`
},
// 手动分包配置 - 优化缓存和预加载策略
manualChunks: {
// 核心框架 - 立即加载
'vue-core': ['vue', 'vue-router', 'vuex'],
'ui-vendor': ['ant-design-vue', '@ant-design/icons-vue'],
// 工具包 - 按需加载
'utils-vendor': ['axios'],
'misc-vendor': ['nprogress', 'core-js']
}
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
},
extensions: ['.js', '.json', '.vue', '.less', '.ts']
},
// 开发服务器配置
server: {
port: 8250,
open: true,
proxy: {
'/api': {
target: 'https://localhost:3000/api', // 测试环境
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
}
},
// 基础路径
base: isProduction ? './' : '/',
// 实验性功能:优化缓存
experimental: {
renderBuiltUrl: (filename, { hostType }) => {
if (hostType === 'js') {
return { js: `./${filename}` }
}
return { css: `./${filename}` }
}
},
//生成环境移除console和debugger
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
}
}
})