告别 Webpack 时代的龟速!我的 Vue3 项目 Vite 升级实战复盘

166 阅读6分钟

告别 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 的核心套件。

  1. 卸载旧依赖

    Bash

    npm uninstall @vue/cli-plugin-babel @vue/cli-plugin-eslint @vue/cli-plugin-router @vue/cli-plugin-vuex @vue/cli-service
    
  2. 安装新依赖

    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 -> base
  • devServer -> server
  • css.loaderOptions -> css.preprocessorOptions
  • configureWebpack.optimization.splitChunks -> build.rollupOptions.output.manualChunks
  • BundleAnalyzerPlugin -> rollup-plugin-visualizer (需要安装 rollup-plugin-visualizer)

第三步:index.html 的改造

Vite 将 index.html 视为应用的入口。

  1. 移动文件:将 public/index.html 移动到项目根目录。

  2. 修改内容

    • 移除所有 Webpack 的 EJS 模板语法(如 <%= BASE_URL %>)。
    • <body> 底部添加入口脚本:<script type="module" src="/src/main.js"></script>

第四步:环境变量的适配

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。

  • 解决方案

    1. 静态资源:使用 import 导入。

      JavaScript

      // before
      const img = require('@/assets/logo.png');
      // after
      import img from '@/assets/logo.png';
      
    2. 动态资源(如在变量或循环中):使用 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

根据自身依赖调整 manualChunksplugins 等配置。

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
      }
    }
  }
})