Vue-CLI-to-Vite-Migration-Guide.md 24 KB

Vue CLI 到 Vite 迁移完整指南

概述

本指南详细记录了将Vue 3项目从Vue CLI迁移到Vite的完整过程,包括所有遇到的问题和解决方案。适用于类似的管理后台项目迁移。

迁移前准备

1. 项目信息

  • 原构建工具: Vue CLI (Webpack)
  • 目标构建工具: Vite
  • Vue版本: Vue 3
  • UI框架: Element Plus
  • 包管理器: Yarn

2. 迁移原因

  • Vue CLI启动速度慢
  • npm依赖管理问题
  • 需要更现代的构建工具

迁移步骤

第一步:更新依赖配置

1.1 修改 package.json

{
  "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的兼容性问题。

1.2 删除Vue CLI相关依赖

yarn remove @vue/cli-service @vue/cli-plugin-babel @vue/cli-plugin-eslint webpack

1.3 安装Vite依赖

yarn add -D vite @vitejs/plugin-vue vite-plugin-svg-icons vite-plugin-mock-dev-server

第二步:创建Vite配置文件

2.1 创建 vite.config.js

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: [],
  },
})

2.2 创建入口文件 index.html

<!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>

2.3 创建环境变量文件 env

VITE_APP_TITLE=Birdseye Vue Admin
VITE_APP_ENV=development

第三步:修复 require.context 问题

这是迁移过程中最重要的步骤,需要将所有Webpack特有的require.context替换为Vite的import.meta.glob

3.1 修复 Vuex Store (src/store/index.js)

// 原来的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;
});

3.2 修复 Mock 插件 (src/plugins/mock.js)

// 原来的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
                // ... 处理逻辑
            });
        }
    },
};

3.3 修复 Axios 插件 (src/plugins/axios.js)

// 原来的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
    // ... 处理逻辑
});

3.4 修复指令注册 (src/directives/index.js)

// 原来的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)
    );
});

3.5 修复组件自动注册 (src/components/veBaseComponents/index.js)

// 原来的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
    );
});

第四步:修复动态导入问题

4.1 修复路由动态导入 (src/plugins/permission.js)

// 添加 @vite-ignore 注释来忽略动态导入警告
route["component"] = () => import(/* @vite-ignore */ "@/views/layoutpages/" + menuList[i].url + ".vue");

第五步:修复模块语法兼容性问题

这是迁移过程中的另一个重要步骤,需要将所有CommonJS模块语法转换为ES6模块语法。

5.1 修复配置文件 (src/api/config.js)

// 原来的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
}

5.2 修复API模块文件 (src/api/modules/*.js)

需要将所有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

修复结果

  • ✅ 成功修复了 87个API模块文件
  • ✅ 修复了 src/config.js 配置文件
  • ✅ 修复了 src/api/index.jssrc/api/mock-server.js
  • ✅ 修复了 src/api/dict.js 字典文件

5.3 检查导入兼容性

确保所有使用配置文件的组件都使用正确的导入语法:

// 正确的导入方式
import { base_url, base_img } from '@/api/config'
// 或者
import config from '@/api/config'

第六步:更新ESLint配置

6.1 修改 .eslintrc.js

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  # 如果存在

常见问题及解决方案

1. crypto.hash 错误

问题: 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

2. JSX语法错误

问题: The JSX syntax extension is not currently enabled

解决: 在vite.config.js中添加esbuild配置

esbuild: {
  loader: 'jsx',
  include: /src\/.*\.js$/,
  exclude: [],
},

3. SCSS变量导入错误

问题: @import "@/styles/variables.scss" 无法解析

解决: 直接在vite.config.js中定义SCSS变量

css: {
  preprocessorOptions: {
    scss: {
      additionalData: `
        $main-bg-color: #f5f5f5;
        $base-color: #409EFF;
        // ... 其他变量
      `,
    },
  },
},

4. 端口冲突

问题: 端口8080被占用

解决: 修改vite.config.js中的端口配置

server: {
  port: 3000, // 改为其他可用端口
}

5. 模块解析错误

问题: require is not defined

解决: 将所有require.context替换为import.meta.glob

6. 导出错误

问题: 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
}

7. module未定义错误

问题: 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.js
  • src/api/index.js
  • src/api/mock-server.js
  • src/api/dict.js
  • src/styles/variables.scss.js
  • 所有 src/api/modules/*.js 文件

8. 动态导入模块错误

问题: 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"),

9. 组件导入路径错误

问题: 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.vue
  • src/views/404.vue
  • src/views/Home.vue
  • 其他使用组件导入的文件

10. import.meta.glob 导出访问错误

问题: 使用 import.meta.glob 后无法正确访问模块导出内容

原因: import.meta.glob 返回的对象结构与 require.context 不同,需要通过 .default 访问默认导出

解决: 在访问模块内容时添加 .default

// 修复前
const moduleContent = modules[key];

// 修复后
const moduleContent = modules[key].default; // 访问默认导出

重要: 这是从 Webpack 迁移到 Vite 时最容易忽略的问题!

常见需要修复的文件

  • src/plugins/axios.js
  • src/plugins/mock.js
  • 其他使用 import.meta.glob 的文件

11. 全局变量未定义错误

问题: 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 插件设置)

12. SCSS变量文件导出错误

问题: 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

13. 动态导入路径解析失败

问题: 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);

常见需要修复的场景

  • 权限插件中的动态路由导入
  • 其他使用动态导入的组件

14. 动态路由添加失败

问题: 动态路由无法正确加载,出现模块解析错误

原因: 动态路由添加逻辑中的对象引用问题和路由添加顺序问题

解决: 修复动态路由添加逻辑

// 修复前
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

15. 第三方组件注册问题

问题: 在 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.vue
  • src/views/layoutpages/sys_mock_data_item/Edit.vue
  • src/views/layoutpages/z_farm_score/Edit.vue
  • src/views/layoutpages/z_farm_work_auth/Edit.vue

注意: 这个修复适用于所有使用第三方组件的 Vue 3 <script setup> 组件。

迁移检查清单

  • 更新package.json脚本和依赖
  • 创建vite.config.js配置文件
  • 创建index.html入口文件
  • 修复所有require.context使用
    • 替换为import.meta.glob
    • 确保正确访问.default导出
  • 修复动态导入警告
  • 修复路由动态导入问题
    • 为路由组件导入添加 @vite-ignore 注释
    • 配置 Vite 路径解析扩展名
    • 修复动态路由添加逻辑
  • 修复组件导入路径问题
    • 为组件导入添加 .vue 扩展名
  • 修复全局变量问题
    • 检查并设置必要的全局变量(如 XE)
  • 修复模块语法兼容性问题
    • 转换配置文件为ES6模块语法
    • 转换API模块文件为ES6模块语法
    • 检查所有导入语句兼容性
  • 修复第三方组件注册问题
    • 检查所有使用第三方组件的 Vue 3 <script setup> 组件
    • 使用 defineOptions 注册第三方组件
  • 更新ESLint配置
  • 删除旧配置文件
  • 测试开发服务器启动
  • 测试构建过程
  • 验证所有功能正常

性能对比

迁移前 (Vue CLI)

  • 启动时间: ~30-60秒
  • 热更新: ~2-5秒
  • 构建时间: ~2-5分钟

迁移后 (Vite)

  • 启动时间: ~3-8秒 ⚡
  • 热更新: ~100-500ms ⚡
  • 构建时间: ~30-60秒 ⚡

注意事项

  1. 版本兼容性: 建议使用Vite 4.x版本,避免Vite 7.x的兼容性问题
  2. 依赖管理: 推荐使用yarn而不是npm,避免依赖冲突
  3. 路径解析: 确保所有别名路径(@/)正确配置
  4. 环境变量: Vite使用VITE_前缀的环境变量
  5. 插件兼容性: 某些Vue CLI插件可能需要Vite替代方案
  6. 模块语法: 确保所有文件使用ES6模块语法,避免CommonJS与ES6混用
  7. 导入导出: 检查所有导入导出语句的兼容性

16. SCSS 导入和 v-bind() 使用问题

问题:

  1. Sass 弃用警告:Deprecation Warning [legacy-js-api]: The legacy JS API is deprecated
  2. SCSS 导入错误:Can't find stylesheet to import
  3. v-bind() 函数未定义错误

解决方案:

16.1 修复 SCSS 变量导入

  1. 将 SCSS 变量转换为 CSS 自定义属性:
// 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);
  1. 删除旧的 src/styles/variables.scss.js 文件

  2. 更新所有导入旧 .js 版本的文件:

    • src/views/IFrame.vue
    • src/views/AppMain.vue
    • src/components/layout/components/Logo.vue
    • src/components/layout/SideBar.vue
  3. src/styles/common.scss 中直接导入变量文件:

@import "./variables.scss";

html {
    // ... 其他样式
}
  1. 简化 vite.config.js 中的 CSS 配置:
css: {
  devSourcemap: true,
},

16.2 使用 v-bind() 绑定响应式 CSS 变量

现在可以在 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>

优势:

  • 解决了 Sass 弃用警告
  • 支持 CSS 自定义属性
  • 可以使用 Vue 3 的 v-bind() 功能
  • 更好的性能和兼容性

总结

通过以上步骤,成功将Vue CLI项目迁移到Vite,获得了显著的性能提升和更好的开发体验。迁移过程主要涉及配置文件的更新和动态导入方式的调整,整体迁移难度适中,值得进行。


迁移完成时间: 约2-3小时
主要收益: 启动速度提升80%,热更新速度提升90%,构建速度提升70%