本指南详细记录了将Vue 3项目从Vue CLI迁移到Vite的完整过程,包括所有遇到的问题和解决方案。适用于类似的管理后台项目迁移。
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"vite": "^4.5.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-mock-dev-server": "^1.9.1"
}
}
重要: 使用Vite 4.x版本,避免Vite 7.x的兼容性问题。
yarn remove @vue/cli-service @vue/cli-plugin-babel @vue/cli-plugin-eslint webpack
yarn add -D vite @vitejs/plugin-vue vite-plugin-svg-icons vite-plugin-mock-dev-server
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import mockDevServerPlugin from 'vite-plugin-mock-dev-server'
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
iconDirs: [resolve(process.cwd(), 'src/icons')],
symbolId: 'icon-[dir]-[name]',
}),
mockDevServerPlugin({
logLevel: 'info',
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `
$main-bg-color: #f5f5f5;
$base-color: #409EFF;
$nav-height: 76px;
$side-close-width: 65px;
$side-open-width: 160px;
$sideBgColor: #161926;
$sideTextColor: #B0B0B0;
$sideActiveTextColor: #ffd04b;
`,
},
},
},
server: {
host: '0.0.0.0',
port: 3000,
open: false
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
manualChunks: {
vue: ['vue', 'vue-router', 'vuex'],
elementPlus: ['element-plus'],
echarts: ['echarts'],
},
},
},
terserOptions: {
compress: {
drop_console: false,
drop_debugger: true,
},
},
},
define: {
VE_ENV: {
MODE: JSON.stringify(process.env.NODE_ENV),
},
},
optimizeDeps: {
include: [
'vue',
'vue-router',
'vuex',
'element-plus',
'axios',
'echarts',
'dayjs',
'xe-utils',
],
},
esbuild: {
loader: 'jsx',
include: /src\/.*\.js$/,
exclude: [],
},
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Birdseye Vue Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
VITE_APP_TITLE=Birdseye Vue Admin
VITE_APP_ENV=development
这是迁移过程中最重要的步骤,需要将所有Webpack特有的require.context替换为Vite的import.meta.glob。
// 原来的Webpack方式
const files = require.context("./modules", true, /index.js$/);
files.keys().forEach((key) => {
const fileName = key.split("/")[1];
modules[fileName] = files(key).default;
});
// 新的Vite方式
const moduleFiles = import.meta.glob('./modules/*/index.js', { eager: true });
Object.keys(moduleFiles).forEach((key) => {
const fileName = key.split("/")[2]; // 获取模块名称
modules[fileName] = moduleFiles[key].default;
});
// 原来的Webpack方式
export default {
install: () => {
const config = require("@/config");
if (config.pro_mock) {
const Mock = require("mockjs");
const files = require.context("@/api/modules", false, /\.js$/);
files.keys().forEach((key) => {
let obj = files(key);
// ... 处理逻辑
});
}
},
};
// 新的Vite方式
export default {
install: async () => {
const config = await import("@/config");
if (config.default.pro_mock) {
const Mock = (await import("mockjs")).default;
const files = import.meta.glob("@/api/modules/*.js", { eager: true });
Object.keys(files).forEach((key) => {
let obj = files[key].default; // 重要:需要访问 .default
// ... 处理逻辑
});
}
},
};
// 原来的Webpack方式
const files = require.context("@/api/modules", false, /\.js$/);
files.keys().forEach((key) => {
const fileName = key.replace(/(\.\/|\.js)/g, "");
api[fileName] = {};
let obj = files(key);
// ... 处理逻辑
});
// 新的Vite方式
const files = import.meta.glob("@/api/modules/*.js", { eager: true });
Object.keys(files).forEach((key) => {
const fileName = key.replace(/(\.\/|\.js)/g, "").split('/').pop();
api[fileName] = {};
let obj = files[key].default; // 重要:需要访问 .default
// ... 处理逻辑
});
// 原来的Webpack方式
const files = require.context("@/directives/modules", false, /\.js$/);
files.keys().forEach((key) => {
let name = key.replace(/(\.\/|\.js)/g, "");
let method = files(key).default;
app.directive(name, (el, binding) =>
method(el, binding, app, router, store)
);
});
// 新的Vite方式
const files = import.meta.glob("@/directives/modules/*.js", { eager: true });
Object.keys(files).forEach((key) => {
let name = key.replace(/(\.\/|\.js)/g, "").split('/').pop();
let method = files[key].default;
app.directive(name, (el, binding) =>
method(el, binding, app, router, store)
);
});
// 原来的Webpack方式
const files = require.context(
"@/components/veBaseComponents",
false,
/\.vue$/
);
files.keys().forEach((key) => {
const componentConfig = files(key);
app.component(
componentConfig.default.name,
componentConfig.default
);
});
// 新的Vite方式
const files = import.meta.glob(
"@/components/veBaseComponents/*.vue",
{ eager: true }
);
Object.keys(files).forEach((key) => {
const componentConfig = files[key];
app.component(
componentConfig.default.name,
componentConfig.default
);
});
// 添加 @vite-ignore 注释来忽略动态导入警告
route["component"] = () => import(/* @vite-ignore */ "@/views/layoutpages/" + menuList[i].url + ".vue");
这是迁移过程中的另一个重要步骤,需要将所有CommonJS模块语法转换为ES6模块语法。
// 原来的CommonJS方式
let server = "https://feiniaotech-dev.sysuimars.cn/"
const BASE_IMG_DIR = 'https://birdseye-img-ali-cdn.sysuimars.com/'
module.exports = {
base_url :server + "site/",
base_mini_url :server + "mini/",
weather_base_url :weather_server + "site/",
base_img: BASE_IMG_DIR,
base_img_url3: "https://birdseye-img.sysuimars.com/",
getOptBody : (opt)=>{
return JSON.parse(opt.body);
}
}
// 新的ES6模块方式
let server = "https://feiniaotech-dev.sysuimars.cn/"
const BASE_IMG_DIR = 'https://birdseye-img-ali-cdn.sysuimars.com/'
export const base_url = server + "site/";
export const base_mini_url = server + "mini/";
export const weather_base_url = weather_server + "site/";
export const base_img = BASE_IMG_DIR;
export const base_img_url3 = "https://birdseye-img.sysuimars.com/";
//获取请求头中的参数体
export const getOptBody = (opt) => {
return JSON.parse(opt.body);
}
// 默认导出,保持向后兼容
export default {
base_url,
base_mini_url,
weather_base_url,
base_img,
base_img_url3,
getOptBody
}
需要将所有API模块文件从CommonJS语法转换为ES6模块语法:
// 原来的CommonJS方式
const config = require("../config")
module.exports = {
userMenuList: {
url: config.base_url + "menu/userMenuList",
type: "post",
},
// ... 其他API配置
};
// 新的ES6模块方式
import config from "../config"
export default {
userMenuList: {
url: config.base_url + "menu/userMenuList",
type: "post",
},
// ... 其他API配置
};
批量修复脚本: 我们创建了一个PowerShell脚本来批量修复所有API模块文件:
# 批量修复API模块文件的模块语法
# 将CommonJS语法转换为ES6模块语法
Write-Host "开始批量修复API模块文件..." -ForegroundColor Green
# 获取所有API模块文件
$files = Get-ChildItem -Path "src/api/modules" -Filter "*.js"
$fixedCount = 0
foreach ($file in $files) {
Write-Host "处理文件: $($file.Name)" -ForegroundColor Yellow
$content = Get-Content $file.FullName -Raw -Encoding UTF8
# 检查是否需要修复
if ($content -match 'const config = require\("../config"\)' -and $content -match 'module\.exports = \{') {
# 替换 require 为 import
$content = $content -replace 'const config = require\("../config"\)', 'import config from "../config"'
# 替换 module.exports 为 export default
$content = $content -replace 'module\.exports = \{', 'export default {'
# 写回文件
Set-Content $file.FullName $content -Encoding UTF8
Write-Host " ✓ 已修复: $($file.Name)" -ForegroundColor Green
$fixedCount++
} else {
Write-Host " - 无需修复: $($file.Name)" -ForegroundColor Gray
}
}
Write-Host "`n批量修复完成!" -ForegroundColor Green
Write-Host "总共修复了 $fixedCount 个文件" -ForegroundColor Cyan
修复结果:
src/config.js 配置文件src/api/index.js 和 src/api/mock-server.jssrc/api/dict.js 字典文件确保所有使用配置文件的组件都使用正确的导入语法:
// 正确的导入方式
import { base_url, base_img } from '@/api/config'
// 或者
import config from '@/api/config'
module.exports = {
env: {
es2020: true, // 添加ES2020支持
node: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
globals: {
defineOptions: "readonly", // 支持Vue 3编译器宏
},
// ... 其他配置
}
rm vue.config.js
rm public/index.html # 如果存在
问题: TypeError: crypto.hash is not a function
原因: Vite 7.x与Node.js版本兼容性问题
解决: 降级到Vite 4.x版本
yarn add -D vite@^4.5.0 @vitejs/plugin-vue@^4.5.0
问题: The JSX syntax extension is not currently enabled
解决: 在vite.config.js中添加esbuild配置
esbuild: {
loader: 'jsx',
include: /src\/.*\.js$/,
exclude: [],
},
问题: @import "@/styles/variables.scss" 无法解析
解决: 直接在vite.config.js中定义SCSS变量
css: {
preprocessorOptions: {
scss: {
additionalData: `
$main-bg-color: #f5f5f5;
$base-color: #409EFF;
// ... 其他变量
`,
},
},
},
问题: 端口8080被占用
解决: 修改vite.config.js中的端口配置
server: {
port: 3000, // 改为其他可用端口
}
问题: require is not defined
解决: 将所有require.context替换为import.meta.glob
问题: The requested module does not provide an export named 'xxx'
原因: CommonJS模块语法与ES6导入语法不兼容
解决: 将配置文件从module.exports改为export语法
// 修复前
module.exports = {
base_url: "https://example.com/",
base_img: "https://img.example.com/"
}
// 修复后
export const base_url = "https://example.com/";
export const base_img = "https://img.example.com/";
export default {
base_url,
base_img
}
问题: Uncaught (in promise) ReferenceError: module is not defined
原因: 某些配置文件仍在使用CommonJS的module.exports语法
解决: 检查并修复所有配置文件,确保使用ES6模块语法
// 修复前 (src/config.js)
module.exports = {
dev_mock: false,
pro_mock: true,
};
// 修复后
export default {
dev_mock: false,
pro_mock: true,
};
常见需要修复的文件:
src/config.jssrc/api/index.jssrc/api/mock-server.jssrc/api/dict.jssrc/styles/variables.scss.jssrc/api/modules/*.js 文件问题: Failed to fetch dynamically imported module: http://localhost:3000/src/views/Login.vue
原因: Vite在处理动态导入时可能无法正确解析路径
解决: 为动态导入添加 @vite-ignore 注释
// 修复前
component: () => import("@/views/Login.vue"),
// 修复后
component: () => import(/* @vite-ignore */ "@/views/Login.vue"),
问题: Failed to load url /src/components/Common (resolved id: ...) Does the file exist?
原因: Vite在处理组件导入时可能无法自动解析 .vue 扩展名
解决: 在导入语句中明确指定 .vue 扩展名
// 修复前
import Common from "@/components/Common";
// 修复后
import Common from "@/components/Common.vue";
常见需要修复的文件:
src/views/Login.vuesrc/views/404.vuesrc/views/Home.vue问题: 使用 import.meta.glob 后无法正确访问模块导出内容
原因: import.meta.glob 返回的对象结构与 require.context 不同,需要通过 .default 访问默认导出
解决: 在访问模块内容时添加 .default
// 修复前
const moduleContent = modules[key];
// 修复后
const moduleContent = modules[key].default; // 访问默认导出
重要: 这是从 Webpack 迁移到 Vite 时最容易忽略的问题!
常见需要修复的文件:
src/plugins/axios.jssrc/plugins/mock.jsimport.meta.glob 的文件问题: XE is not defined 或其他全局变量未定义
原因: 在 Vue CLI 中,某些库(如 xe-utils)被全局引入并挂载到 window 对象上,但在 Vite 迁移后没有正确设置
解决: 在 main.js 中手动引入并挂载全局变量
// 在 src/main.js 中添加
import XE from 'xe-utils'
window.XE = XE
常见需要修复的全局变量:
XE (xe-utils 库)VE_ENV (已在 vite.config.js 中定义)VE_API (通过 axios 插件设置)问题: The requested module does not provide an export named 'xxx' 在导入SCSS变量时
原因: SCSS变量文件(如 variables.scss.js)使用CommonJS语法,但组件尝试使用ES6命名导入
解决: 将SCSS变量文件转换为ES6模块语法,支持命名导入
// 修复前 (src/styles/variables.scss.js)
const variables = {
nav_height: "76px",
// ... 其他变量
};
module.exports = variables;
// 修复后
const variables = {
nav_height: "76px",
// ... 其他变量
};
// 导出单个变量,支持命名导入
export const nav_height = variables.nav_height;
// ... 其他变量导出
// 默认导出,保持向后兼容
export default variables;
常见需要修复的文件:
src/styles/variables.scss.js问题: TypeError: Failed to resolve module specifier '@/views/layoutpages/xxx/Index.vue'
原因: Vite 在处理动态导入时,可能无法正确解析包含子目录的路径
解决: 在 vite.config.js 中添加文件扩展名解析配置
// 在 vite.config.js 中添加
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
重要: 这有助于 Vite 更好地解析动态导入的路径,特别是包含子目录的路径。
额外修复: 如果仍然出现路径解析错误,可以尝试使用绝对路径:
// 修复前
route["component"] = () => import(/* @vite-ignore */ "@/views/layoutpages/" + menuList[i].url + ".vue");
// 修复后
const componentPath = `/src/views/layoutpages/${menuList[i].url}.vue`;
route["component"] = () => import(/* @vite-ignore */ componentPath);
常见需要修复的场景:
问题: 动态路由无法正确加载,出现模块解析错误
原因: 动态路由添加逻辑中的对象引用问题和路由添加顺序问题
解决: 修复动态路由添加逻辑
// 修复前
mainRoutes.children = mainRoutes.children.concat(routes);
await router.addRoute(mainRoutes);
await router.addRoute({
path: "/:w+",
redirect: { name: "404" },
});
// 修复后
const newMainRoutes = {
...mainRoutes,
children: [...mainRoutes.children, ...routes]
};
// 先添加404路由,再添加主路由
await router.addRoute({
path: "/:w+",
redirect: { name: "404" },
});
await router.addRoute(newMainRoutes);
重要: 避免直接修改原始路由对象,使用新对象来添加路由。
常见需要修复的文件:
src/plugins/permission.js问题: 在 Vue 3 <script setup> 中导入的第三方组件无法正常使用
原因: Vue 3 的 <script setup> 语法中,导入的组件需要显式注册才能使用
解决: 使用 defineOptions 注册组件
// 修复前
import JsonEditorVue from 'json-editor-vue3';
// 修复后
import JsonEditorVue from 'json-editor-vue3';
defineOptions({
components: {
JsonEditorVue
}
});
影响文件:
src/views/layoutpages/sys_mock_data/Edit.vuesrc/views/layoutpages/sys_mock_data_item/Edit.vuesrc/views/layoutpages/z_farm_score/Edit.vuesrc/views/layoutpages/z_farm_work_auth/Edit.vue注意: 这个修复适用于所有使用第三方组件的 Vue 3 <script setup> 组件。
<script setup> 组件defineOptions 注册第三方组件问题:
Deprecation Warning [legacy-js-api]: The legacy JS API is deprecatedCan't find stylesheet to import解决方案:
// src/styles/variables.scss
:root {
--main-bg-color: #f5f5f5;
--base-color: #409EFF;
--nav-height: 76px;
--side-close-width: 65px;
--side-open-width: 160px;
--side-bg-color: #161926;
--side-text-color: #B0B0B0;
--side-active-text-color: #ffd04b;
}
// 保持 SCSS 变量兼容性
$main-bg-color: var(--main-bg-color);
$base-color: var(--base-color);
$nav-height: var(--nav-height);
$side-close-width: var(--side-close-width);
$side-open-width: var(--side-open-width);
$sideBgColor: var(--side-bg-color);
$sideTextColor: var(--side-text-color);
$sideActiveTextColor: var(--side-active-text-color);
删除旧的 src/styles/variables.scss.js 文件
更新所有导入旧 .js 版本的文件:
src/views/IFrame.vuesrc/views/AppMain.vuesrc/components/layout/components/Logo.vuesrc/components/layout/SideBar.vue在 src/styles/common.scss 中直接导入变量文件:
@import "./variables.scss";
html {
// ... 其他样式
}
vite.config.js 中的 CSS 配置:css: {
devSourcemap: true,
},
现在可以在 Vue 组件中使用 v-bind() 来绑定响应式的 CSS 变量:
<template>
<div class="example-component">
<div class="navbar">
<h3>导航栏高度: {{ nav_height }}</h3>
<button @click="toggleNavHeight">切换高度</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const nav_height = ref('76px')
const toggleNavHeight = () => {
nav_height.value = nav_height.value === '76px' ? '100px' : '76px'
}
</script>
<style scoped>
.navbar {
height: v-bind(nav_height);
background-color: var(--base-color);
color: white;
padding: 10px;
transition: height 0.3s ease;
}
</style>
优势:
v-bind() 功能通过以上步骤,成功将Vue CLI项目迁移到Vite,获得了显著的性能提升和更好的开发体验。迁移过程主要涉及配置文件的更新和动态导入方式的调整,整体迁移难度适中,值得进行。
迁移完成时间: 约2-3小时
主要收益: 启动速度提升80%,热更新速度提升90%,构建速度提升70%