vue2.x 项目 webpack升级vite避坑总结

前言

目前一个用webpack构建的vue2.X 项目由于业务扩展越来越大,导致项目在本地编译时热更新很慢,页面更新需要10几秒以上。为提高团体开发速度和效率,最近打算把底部打包构建的环境由webpack迁移为vite。

兼容性注意
Vite 需要 Node.js 版本 >= 12.0.0

为什么 Vite 启动这么快

  • 底层实现上, Vite 是基于 esbuild 预构建依赖

  • esbuild 使用 go 编写,并且比以 js 编写的打包器预构建依赖, 快 10 - 100 倍。 因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒

  • 两者的启动方式也有所差异

webpack启动方式
Webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。
vue2.x 项目 webpack升级vite避坑总结_第1张图片

vite启动方式
vue2.x 项目 webpack升级vite避坑总结_第2张图片
Vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。

webpack如何升级为vite

vue3.x中webpack迁移vite,主要借助wp2vite脚手架,改造过程中也会遇到很多问题,可参考这篇博文 项目 Webpack 转 Vite 实战

vue2.x中webpack转vite, 主要借助 vite-plugin-vue2插件,具体可参考 Vue2老项目使用vite2升级

改造流程

  1. 安装相关包
    npm install vite -D
    npm install @vue/compiler-sfc -D
    npm install vite-plugin-vue2 -D
  2. 新建vite.config.js, 文件内容见附录1
  3. 给index.html增加main.js入口
<script type="module" src="/src/main.js"></script>

项目改造中遇到的坑

除上述博文提及的坑之外,还遇到了一些其它的问题,总结如下:

  1. 文件后缀省略导致页面报错404(例:vue文件引入时,webpack只需要文件名),在vite.config.js配置resolve.extensions中添加对应后缀,vite默认有[’.mjs’, ‘.js’, ‘.ts’, ‘.jsx’, ‘.tsx’, ‘.json’]

  2. 入口文件不是index.html,或者不在根目录,导致资源报错404;需将 /public/index.html 挪出到根目录路径下, 或者改变vite.config.js 默认的入口路径地址。

  3. @别名的使用,修改相对路径地址

  4. sass语言的扩展,默认生成的vite配置文件,只支持less语言,如项目中涉及sass语言,需额外扩展引入

  5. webpack中使用require引入文件, 但vite中需要改成 important 引入;
    因为vite的底层有使用到Rollup组件打包,但Rollup不支持common.js语法风格,所以需要改成esModule语法才能正确编译;
    vite.config.js 中 optimizeDeps 也是此原理;目前社区中大部分模块都没有设置默认导出 esm,而是导出了 cjs 的包,目前在 vite 项目里直接使用 lodash 之类的包会报错;
    在这里插入图片描述optimize 命令专门为解决模块引用的坑而开发;将原来common.js语法转译为esm风格的语法供vite使用

...
    optimizeDeps: {
      // @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
      include: [
        '@iconify/iconify',
        'ant-design-vue/es/locale/zh_CN',
        'moment/dist/locale/zh-cn',
        'ant-design-vue/es/locale/en_US',
        'moment/dist/locale/eu',
      ],
      exclude: ['vue-demi'],
    },
...   
  1. svg字体图标的批量导入变更:
    webpack环境下,利用require.context批量导出
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

vite环境下,transformIndexHtml钩子函数读取到html源文件,并将svg标签插入到html的body中
核心代码如下:

import { readFileSync, readdirSync } from 'fs'

// id 前缀
let idPerfix = ''

// 识别svg标签的属性
const svgTitle = /+].*?)>/

// 有一些svg文件的属性会定义height和width,要把它清除掉
const clearHeightWidth = /(width|height)="([^>+].*?)"/g

// 没有viewBox的话就利用height和width来新建一个viewBox
const hasViewBox = /(viewBox="[^>+].*?")/g

// 清除换行符
const clearReturn = /(\r)|(\n)/g

/**
 * @param dir 路径
*/
function findSvgFile(dir: string): string[] {
  const svgRes: string[] = []
  const dirents = readdirSync(dir, {
    withFileTypes: true
  })
  for (const dirent of dirents) {
    const path = dir + dirent.name
    if (dirent.isDirectory()) {
      svgRes.push(...findSvgFile(path + '/'))
    } else {
      const svg = readFileSync(path)
        .toString()
        .replace(clearReturn, '')
        .replace(svgTitle, ($1, $2) => {
          let width = 0
          let height = 0
          let content = $2.replace(
            clearHeightWidth,
            (s1, s2, s3) => {
              s3 = s3.replace('px', '')
              if (s2 === 'width') {
                width = s3
              } else if (s2 === 'height') {
                height = s3
              }
              return ''
            }
          )
          if (!hasViewBox.test($2)) {
            content += `viewBox="0 0 ${width} ${height}"`
          }
          return `${idPerfix}-${dirent.name.replace(
            '.svg',
            ''
          )}" ${content}>`
        })
        .replace('', '')
      svgRes.push(svg)
    }
  }
  return svgRes
}
  1. vite默认只支持template模板渲染,不支持render(h, context) {}渲染;
    template----html的方式做渲染
    render----js的方式做渲染
    解决方式: (1) 将render渲染写法改为template渲染写法
    (2)通过扩展vite插件Plugin 转译

  2. build编译后,vendor.js入口文件打包过大;
    可通过cdn方式引入业务所需组件,如:Vue、ElementUI、moment、ECharts、tinymce、axios 等
    然后通过 viteExternalsPlugin 插件排除以上组件的打包,以减小构建生成包的体积;
    界面上则通过 window 对象获取各组件实例

    除viteExternalsPlugin组件+ index.html中script引入外, 还可以通过扩展vite-plugin-cdn插件 + 修改vite.config.js文件中 rollup配置来实现排除打包。

  3. css 深度作用选择器 >>> 写法改为 ::v-deep, vite中不支持>>>书写形式,不生效,且开发环境build构建时也会报警告

  4. build编译后,有几处css文件 warning: “@charset” must be the first rule in the file, 意思是“@charset”必须是文件中的第一条规则, 初步怀疑是有其它css代码先于"@charset"编译了;但是目前还没排查出具体问题,暂时只是警告,不影响项目正常运行,若有知道解决方式的童鞋,望给出解决建议!

  5. 通过vite-plugin-html插件,搭配 .env 文件,可以在开发或构建项目时,对 index.html 注入动态数据,例如替换网站标题、区分正式、开发文件引入等。

  6. 项目打包时报@charset警告
    解决方式: postcss.config.js 中去除@charset警告

module.exports = {
  plugins: [
    //require('autoprefixer'),
    // 移除打包element时的@charset警告
    {
      postcssPlugin: 'internal:charset-removal',
      AtRule: {
        charset: (atRule) => {
          if (atRule.name === 'charset') {
            atRule.remove()
          }
        }
      }
    }
  ]
}

还有些细枝末节的问题,未记录。 至此,项目在本地和线上都能正常编译运行,经测试无大碍。

vite 低版本兼容处理

import legacyPlugin from '@vitejs/plugin-legacy'

plugins: [
	...
    legacyPlugin({
        targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
	}),
    ...
]

兼容原理查看此文章

总结

使用感受:vite环境下热更新确实比webpack 快很多,从根本上解决了开发环境编译很慢的问题,从一定程度上大力的节省了开发人员的时间成本。但vite毕竟是后起之秀,生态环境、社区都尚不成熟,有遇到无法解决之坑的风险,所以谨慎迁移!!

建议: 页面较少的小项目,无需迁移;项目较大,有迁移需求的,多看几遍官方文档说明!! 新项目可直接使用vite的脚手架搭建。有需要的还可以自己封装vite-plugin组件使用。

vite官方api
Vite 官方中文文档

附件1(vite.config.js源码):

/* eslint-disable */
import legacyPlugin from '@vitejs/plugin-legacy';
import { svgBuilder } from './src/icons/builder.js';
import { viteExternalsPlugin } from 'vite-plugin-externals'

import {
  viteMockServe
} from 'vite-plugin-mock';
import * as path from 'path';
import {
  createVuePlugin
} from 'vite-plugin-vue2';
// @see https://cn.vitejs.dev/config/
export default ({
  command,
  mode
}) => {
  let rollupOptions = {};

  let optimizeDeps = {};

  let proxy = {};

  let define = {
    'process.env.NODE_ENV': '"development"',
  }

  let esbuild = {}

  return {
    base: './', // index.html文件所在位置
    root: './', // js导入的资源路径,src
    resolve: {
       extensions: ['.vue', '.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.node', '.scss'],
       alias: {
          '@': path.resolve(__dirname, 'src'),
          '~@': path.resolve(__dirname, 'src'),
          'vue$': 'vue/dist/vue.runtime.esm.js',
       }
    },
    define: define,
    server: {
      host: '127.0.0.1',
      port: '8085',
      strictPort: true,
      //open: '/#/dashboard',
      // 代理
      proxy,
      fs: {
        //strict: false,
        // Allow serving files from one level up to the project root
        //allow: ['..']
        allow: ['.','..'],
      }
    },
    build: {
      target: 'es2015',
      minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
      manifest: false, // 是否产出maifest.json
      sourcemap: false, // 是否产出soucemap.json
      outDir: 'dist', // 产出目录
      rollupOptions,
    },
    esbuild,
    optimizeDeps,
    plugins: [
      // legacyPlugin({
      //   targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
      // }), 
      viteMockServe({
        mockPath: 'mock',
        localEnabled: command === 'serve',
      }), 
      createVuePlugin(),
      svgBuilder('./src/icons/svg/'),
      viteExternalsPlugin({
        vue: 'Vue',
        'element-ui': 'ElementUI',
        moment: 'moment',
        echarts: 'echarts',
        //ECharts: 'ECharts',
        axios: 'axios',
        tinymce: 'tinymce',
      }),
    ],
    css: {
      preprocessorOptions: {
        less: {
          // 支持内联 JavaScript
          javascriptEnabled: true,
        },
        sass: {
          //additionalData: '@import "./src/styles/index.scss";', //global css
          javascriptEnabled: true,
        },
      }
    },
  }
}

你可能感兴趣的:(vite,node.js,vue.js,javascript)