一文搞懂 vue2 webpack to vite

911 阅读7分钟

一、背景

1、背景:

一个开发了6年的老项目,项目启动时间和热更新时间越来越慢,在日常维护5、6个项目时,几个项目切来切去启动浪费时间,另外一点一旦同时开了2个项目后保存编译时间也越来越慢,影响开发效率。

技术架构为vue2+webpack(3)+node(12)。  

2、技术应用场景

适合所有vue2webpack老项目,大幅度的优化启动时间和编译时间。

3、整体思路

第一步:node由12升级到16,相关依赖插件升级。这一步是因为其他项目node为16,统一node版本。
第二部:webpack改为vite,处理相关不兼容问题。

二、解决过程

2.1优化前的准备工作

1、优化前数据保存

启动时间30s:

image.png

保存后编译时间1.5s:

image.png

2、因构建包升级,重新拉取一个git,在本地起一个分支,以免影响正在开发的任务。

3、最终效果希望减少项目启动时间在几秒内。

2.2进入优化阶段

1、node12升级为node16相关依赖升级

这一步主要是相关依赖升级,核心插件:

image.png plugin-proposal-optional-chaining这个去除了,升级后默认支持,去除babel相关配置。

 

2、为什么选择修改构建工具为vite

一开始并没有想升级为vite,当node升级为16版本、相关依赖插件升级后,node的启动时长反而从30s加长到40多s,在优化后效果并不明显,达不到想要的效果,此时在进一步优化有2条路走:
a、检查工具链,分析时间加长的原因,进而解决
b、之前做了个vue3+vite项目,启动在3s内,那是不是vue2也可以兼容vite了
从可以看的到的效果上看,b方案已经是可以看得到的,而检查和分析工具链有相当的复杂度和耗时,并且结果并不确定,综上分析尝试使用b方案。

3、当确定将构建工具webpack改为vite后,官网无解决方法,社区有一些零散的方案,因此选择这个方案注定要碰一些壁。

a、首先对相关包升级,核心工具如下:

image.png

b、异常问题解决

(1)环境和启动修改

// 环境启动变量vue改为vite
// env
// webpack
VUE_APP_VERSION
VUE_APP_BUILD
VUE_APP_API_BASE_UR
VUE_APP_UPGRADE_URL
VUE_APP_UPLOAD_LOG
VUE_APP_AUDITION_AUDIO_FILE
// 环境变量值获取
process.env.VUE_APP_MODE

// vite
VITE_APP_VERSION
VITE_APP_BUILD
VITE_BASE_URL
VITE_APP_UPGRADE_URL
VITE_APP_UPLOAD_LOG
VITE_APP_AUDITION_AUDIO_FILE
// 环境变量值获取
import.meta.env.VITE_APP_MODE

(2)删除babel.config.js文件

(3).vue组件引入修改

// webpack
import MainHeader from '@/components/include/mainHeader'

// vite
import HomeMenu from '@/components/include/homeMenu.vue'

(4)~@图片引入修改

// webpack
background-image: url(~@/assets/images/icon-test-ok.png)

// vite
background-image: url(@/assets/images/icon-test-ok.png)

(5)scssdeep样式穿透修改

// webpack
/deep/ .elst-switch-item {}

// vite
::v-deep .elst-switch-item {}

(6)element-ui生产环境下图标、字体显示失败,console有黄色警告

// webpack
@import "~element-ui/packages/theme-chalk/src/index"

// vite
@import "element-ui/packages/theme-chalk/src/index.scss"

(7)require引入修改

// require图片引入问题几种处理方式

// 全局mixin
Vue.mixin({
  methods: {
    getImageUrl(name) {
      return new URL(name, import.meta.url).href
    }
  }
})

// 动态图片引入
popupImg() {
     //  webpack
      return require(`@/assets/images/history/integral-value-${this.profit}.png`)
     // vite      
     return new URL(`../../../assets/images/history/integral-value-${this.profit}.png`, import.meta.url).href
}

// 静态图片、媒体资源引入
import blockImg from '@/assets/images/empty-block.png'
import audioMatchingSrc from '@/assets/media/arena/matching.mp3'

// 文件引入
//webpack
const configJson = require('@/config/config.json')
// vite
import configJson from '@/config/config.json'

(7)全局组件注册修改

// webpack
const componentsFiles = require.context('./', true, /Result[A-Z]\w+.(vue|js)$/)
let components = {}

componentsFiles.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = componentsFiles(fileName)
  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/.\w+$/, '')
    )
  )
  components[componentName] = componentConfig.default || componentConfig
})
export default Vue => {
  for (let key in components) {
    Vue.component(key, components[key])
  }
}

// vite
async function loadComponents() {
  const componentsList = import.meta.glob('./Result*.vue')
  const moduleList = []
  for (const moduleEle in componentsList) {
    const module = componentsList[moduleEle]().then(mod => mod.default)
    moduleList.push(module)
  }
  const componentsFiles = await Promise.all(moduleList)
  let components = {}
  componentsFiles.forEach(fileName => {
    const componentName = upperFirst(
      camelCase(
        fileName.name.split('/').pop().replace(/.\w+$/, '')
      )
    )
    components[componentName] = fileName
  })
  return components
}

// 导出一个函数,该函数调用 loadComponents 并等待其完成
export default Vue => {
  loadComponents().then(components => {
    for (let key in components) {
      Vue.component(key, components[key])
    }
  })
}

(8)main.js文件修改,不支持main.js使用jsx写法,引入插件并修改main.js为main.jsx文件,index引入文件也一并修改,这里index文件位置可能也要修改。

// index.html


// vite.config.js,这种方式埋下一个伏笔后面会讲到
import { createVuePlugin } from 'vite-plugin-vue2'
  ---
createVuePlugin({
    jsx: true,
  }),

(9)路由router修改

// 去除用于打包的分类的webpackChunkName
/* webpackChunkName: "group-aistudy" */

/*路由注册修改 这里有个坑 当我在routes.js使用await顶级方式收集所有路由后,项目可在开发环境正常运行,
但是在打包上线后出现白屏路由无法正常加载等情况*/

// 第一种解决方式采用router.addRoute 动态注册
import router from './router'
async function getRoutes() {
  const modules = import.meta.glob('./modules/*.js')
  const moduleList = [] // 使用具体类型代替 any 类型
  for (const path in modules) {
    moduleList.push(modules[path]().then(mod => mod.default))
  }
  const modulesRouteList = await Promise.all(moduleList)
  const routeList = modulesRouteList.reduce((prev, curr) => prev.concat(curr), [])
  return routeList
}
async function init() {
  const modulesRoutes = await getRoutes()
  for (let route of modulesRoutes) {
    router.addRoute(route)
  }
}
init()
export default routes

//  第二种
const modules = import.meta.glob('./modules/*.js',  { eager: true })
function getModuleRoutes() {
  const routerList = []
  Object.keys(modules).forEach((key) => {
    const mod = modules[key].default || {}
    const modList = Array.isArray(mod) ? [...mod] : [mod]
    routerList.push(...modList)
  })
  return routerList
}

const modulesRoutes = getModuleRoutes()
const allRoutes = routes.concat(modulesRoutes)
export default allRoutes

(10)api以前老项目有将路由注册到windows全局调用情况,在这种情况下需要将接口导出,否则无法正常引用

// 不确定是否被注册到全局引用,在路由文件加一个导出
export const getDashboard = () => http.get(ApiPath.dashboard, {})
export const getModuleList = () => http.get(ApiPath.skillup_module_list)
export const getBanner = () => http.get(ApiPath.banner, {})
// 下面添加
export default {
  getDashboard,
  getModuleList,
  getBanner,
}

(11)*as写法修改

// webpack
import * as resourceFeedback from './resourceFeedback'
// vite
import resourceFeedback from './resourceFeedback'

(12)vue.config.js文件改为vite.config.js文件

发现在不做构建优化的情况下,vite首屏加载速度要明显比webpack慢。

原因在于有些文件打包后比较大,vite首屏一次性加载所有文件,加快了启动速度,牺牲了首屏加载速度,所以在这种情况下,中大型项目需评估是否使用。

本项目优化后首屏加载速度基本与webpack持平。

export default () => {
  return defineConfig({
    plugins: [
      createVuePlugin({
        jsx: true,
      }),
      createSvgIconsPlugin({
        // 指定图标文件夹,绝对路径(NODE代码)
        iconDirs: [path.resolve(process.cwd(), 'src/assets/svg')],
        symbolId: 'icon-[name]',
        inject: 'body-last',
        customDomId: '__svg__icons__dom__'
      }),
      compression({
        algorithm: 'gzip', // 压缩算法
        ext: '.gz', // 压缩文件后缀名
      }),
    ],
    resolve: {
      extensions: [".vue", ".js", ".json"],
      /** 添加alias规则 */
      alias: [
        {
          find: '@/',
          replacement: '/src/'
        }
      ],
      
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `
          @use "@/assets/scss/_variables.scss" as *;
          @use "@/assets/scss/_mixin.scss" as *;
          `,
        },
      },
    },
    build: {
      target: 'esnext',
      // 启用 Tree Shaking
      treeShaking: true,
      // 启用代码分割
      codeSplit: true,
      // 压缩输出文件大小限制,单位 KB
      maxAssetSize: 500,
      // 压缩输出文件数量限制
      maxFileCount: 100,
      terserOptions: {  
        compress: {
          //生产环境时移除console
          drop_console: true,
          drop_debugger: true,
        }
      },
      brotliSize: false,
      outDir: 'dist',
      rollupOptions: {
        minify: 'terser',
        output: {
          chunkFileNames: 'js/[name]-[hash].js',
          entryFileNames: 'js/[name]-[hash].js',
          assetFileNames: '[ext]/[name]-[hash].[ext]',
          manualChunks(id) {
            if(id.includes('node_modules')){
              return id.toString().split('node_modules/')[1].split('/')[0].toString()
            }
          }
        },
      },
    },
  })
}

(13)vite-plugin-vue2无法兼容渲染element-uitable组件,渲染空白,改为@vitejs/plugin-vue2、@vitejs/plugin-vue2-jsx

import vue from '@vitejs/plugin-vue2'
import vueJsx from '@vitejs/plugin-vue2-jsx'

export default () => {
  return defineConfig({
    plugins: [
      vue(),
      vueJsx(),
---

(14)ios低版本兼容性问题

// vite.config.js
// ios版本兼容15以上
 build: {
      target: 'esnext'
      ...
}
// 亲测ios版本兼容12以上,12以下没测过
 build: {
      target: 'es2015'
      ...
}
// 更低版本兼容 安装@vitejs/plugin-legacy
import { defineConfig } from 'vite';
import legacyPlugin from '@vitejs/plugin-legacy';
export default defineConfig({
  plugins: [
    legacyPlugin({
      targets: ['defaults', 'not IE 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
      renderLegacyChunks: true,
      polyfills: [
        'es.symbol',
        'es.promise',
        'es.promise.finally',
        'es/map',
        'es/set',
        'es.array.filter',
        'es.array.for-each',
        'es.array.flat-map',
        'es.object.define-properties',
        'es.object.define-property',
        'es.object.get-own-property-descriptor',
        'es.object.get-own-property-descriptors',
        'es.object.keys',
        'es.object.to-string',
        'web.dom-collections.for-each',
        'esnext.global-this',
        'esnext.string.match-all'
      ]
    })
  ],
  build: {
    target: 'es2015',
  }
});

三、总结

3.1升级效果:

a、启动时间约5s内,二次启动时间3s内:

image.png

b、热更新时间:vite最大优势,保存即编译,无感编译时间。

3.2技术经验:

本次vue2webpack构建转vite,通用于vue2webpack项目,适合中小型项目,可大幅度提升项目启动时间,编译时间,提升开发效率。

3.3有待提升:

1、vite生成构建包上,配置不如webpack灵活,生成上线包,拆包拆的过碎,首次加载大量文件,约700个文件,后期可分析进行文件合并。

2、项目上因为文件不是很多,采用直接修改的方式,如果文件较多可采用开发脚本方式进行修改。

3、本项目是直接修改webpack改成vite,目前最好是可以开发环境用vite生产环境仍然用webpack。

4、随着vite更新迭代,社区逐渐完善vite在不久的将来在打包上有不下于webpack优势。