项目背景

所谓工欲善其事必先利其器。近期公司有一个大数据处理平台的项目,由于项目启动时间实在太长,体验极差,且生产模式下还使用了Gulp工具打包,所以组长让我重新选择构建工具改造。

打包工具权衡

组长给了我两个选择:

  1. Vue Cli
  2. Vite

说实话,直接使用Vue-Cli应该是改造时间最少的,但是Vite实在是太吸引人了,组内也开始准备将各个项目的构建工具换成Vite,加上自己又想学习一下,就选择使用了Vite(有点私心,哈哈哈哈)。

实现思路

实现之前还是进行了一点预研,避免浪费时间,以下是我设计本次改造的实现思路:

  1. 环境搭建: 首先搭建一个基于Vue2 + Vite的最小化环境,之后引入葱姜蒜等配套设施,Vue-RouterVuexAxioslodash等。
  2. 代码迁移:在确保搭建的环境能够正常运行之后,将需要的项目源码迁移过来,当然这一步可以细分,避免一次性迁移出太多问题。
  3. 运行排坑:迁移代码之后,需要对代码是否能正常运行进行验证,这一步很重要,也是我花时间最多的一步。
  4. 项目优化:由于老项目里面本身有挺多问题,加上Vite不做优化的情况还是有一些问题的,故需做一些优化

具体实现

万物之始:环境搭建

首先运行yarn create vite创建一个Vite项目,这里不使用--template vue的原因是用了之后会直接创建一个Vue3的项目,emmm。。咱这个项目用的Vue2,所以不这么干。

随后我们来添加Vue的插件(开始脑阔疼),这里着重说一下,由于老项目采用的是固定版本Vue@2.5.2,本来应该使用vite-plugin-vue2这个创建,但是实践之后有个问题:
vue@2.5.2+vue-template-compiler这种方式会导致Viterun build之后不退出,没有Done x.xx s .这种输出,就卡在这儿了(我人傻了呀)。

而我们公司采用的是Jenkins自动化打包流程,这一步卡住就会导致后续的步骤都无法进行下去。经过测试之后2.5.2的这个版本有点问题(具体什么问题我至今也不是特别清楚),升级到2.6.0之后就不会出现这个问题。

在权衡之后,为了项目的扩展性和可维护性(想用Composition Api),并且查看更新文档之后,决定直接升级成Vue 2.7

这一系列操作之后,最终Vue的插件使用了只支持Vue 2.7@vitejs/plugin-vue2,并且也不用再安装template-compiler。之后就是引入项目中的各种依赖,葱姜蒜三件套Vue-RouterVuexAxios等。

这里贴一下简单的配置:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue2';
import { fileURLToPath, URL } from 'url';
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    host: 'localhost',
    port: 8080,
    open: true,
    proxy: {
        /** 这里随意 */
    }
  },
})

无话可说:代码迁移

这一步没什么好说的,直接复制粘贴,完事儿,只是项目中有一些gulp的任务被我直接干掉了,实际上我看了代码也没做什么事情,还是调用webpack的生产打包,额外生成了一些版本号的东西。

险些中道崩殂:运行排坑

这一步是我花时间比较多的,甚至差点就不想用Vite了。主要描述下我碰到的各种问题:

  1. 路径里面不能带 #,可能是配置问题;
  2. Commonjs 的方式必须修改为ES Module,包括require.context()这种写法需要修改为import.meta.globEager()
  3. import会被Vite预先处理,动态变量导入的时候变量只能是单层级的目录,多层级的需自行处理;
  4. img url 不能直接用函数处理之后的字符串,应该使用new Url(),参考cn.vitejs.dev/guide/asset…
  5. Vite vue<style>部分不支持 /deep/或者 >>> 的写法,正确写法为 :deep(.child)
  6. Vite vue中使用render写法的时候,必须做如下操作:
  • 使用@vitejs/plugin-vue2-jsx插件,如果使用vite-plugin-vue2的话,可以直接传入jsx: true来代替。
  • 指定.vue文件中<script>上添加lang="jsx"
  • vite.config.js中添加
 {
     esbuild: {
         jsxFactory: 'h',
         jsxFragment: 'Fragment'
     }
 }

还有一些项目中的问题就不在这儿一一叙述了,可能大家碰不到,大概都是跟导入有关的。

迎来曙光:项目优化

这一步主要是根据项目的情况做了一些优化,参考自三元大佬的小册深入浅出 Vite – 神三元 – 小册 (),非常的细嗷:

请求优化:

首先大家都知道,Vite的性能瓶颈之一就是加载大量的script module,传统的HTTP1.1存在队头阻塞的情况,同一个 TCP 管道中同一时刻只能处理一个 HTTP 请求,也就是说如果当前请求没有处理完,其它的请求都处于阻塞状态,另外浏览器对于同一域名下的并发请求数量都有限制,比如 Chrome 中只允许并发6个请求。

Vite 中,我们可以通过vite-plugin-mkcert在本地 Dev Server 上开启 HTTP2: 由于 HTTP2 依赖 TLS 握手,插件会自动生成 TLS 证书,然后支持通过 HTTPS 的方式启动,而 Vite 会自动把 HTTPS 服务升级为 HTTP2。(此处引用神三元大佬的小册《深入浅出Vite》第19节性能优化)

压缩:

由于Vite自带压缩代码功能,也可以自己配置,我这儿就没有额外配置。另外有很多同学都用了压缩图片的工具,但是这玩意儿我装不上(尬住),我就没用,我看有同学说这个插件用了图片压缩出来还有更大的情况(啊这。。)。

代码分割:

这里Vite本身支持在build.rollupOptions.output.manualChunks自定义分包行为,但是我太懒,直接采用vite-plugin-chunk-split插件进行自动分包。

打包预览

打包预览能够让我们清晰地知道打包之后的体积,从而能更精确地去定位打包问题中可能发生的问题。这里采用的是Rollup的插件rollup-plugin-visualizer

依赖预购建

预购建也是Vite开发过程中很重要的一步,很早之前Vite采用Rollup进行这一步,新的Vite也改成了Es Build来实现。在某些动态 import 的场景下,由于 Vite 天然按需加载的特性,经常会导致某些依赖只能在运行时被识别出来,可能会出现new depenencies found的情况,直接进行二次依赖预构建,代价很大,会重新请求所有模块,而且可能不止出现一次(如果你在开发过程中,发现点开某个模块,页面突然白了,多半可能是二次预构建了)。

所以通过optimizeDeps字段可以对预购建进行自定义配置,以下是我这部分的配置:

   {
       optimizeDeps: {
         include: [
          '@antv/g6',
          'vue-codemirror',
          'x2js',
          'insert-css',
          'vue',
          'moment',
          ...codeMirrorDeps
        ]
      }
   }

这里着重说一下我解构的codeMirrorDeps这个变量,在我实践的过程中,我发现Vite并不能很好地处理这种导入方式:

import { CodeMirror } from 'vue-codemirror';
// language
import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/python/python.js';
import 'codemirror/mode/sql/sql.js';
// theme css
import 'codemirror/theme/base16-light.css';
import 'codemirror/theme/monokai.css';
import 'codemirror/theme/solarized.css';
// hint
import 'codemirror/addon/hint/javascript-hint.js';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/show-hint.js';
import 'codemirror/addon/selection/active-line.js';
// highlightSelectionMatches
import 'codemirror/addon/scroll/annotatescrollbar.js';
import 'codemirror/addon/search/match-highlighter.js';
import 'codemirror/addon/search/matchesonscrollbar.js';
import 'codemirror/addon/search/searchcursor.js';
// require active-line.js
import 'codemirror/addon/selection/active-line.js';
// closebrackets
import 'codemirror/addon/edit/closebrackets.js';
// keyMap
import 'codemirror/addon/comment/comment.js';
import 'codemirror/addon/dialog/dialog.css';
import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/edit/matchbrackets.js';
import 'codemirror/addon/search/search.js';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/keymap/emacs.js';
import 'codemirror/keymap/sublime.js';
import 'codemirror/mode/clike/clike.js';

这种方式导致的就是,我在加载这个组件的时候,每次都会二次预购建,故在这儿我将这些路径抽离成变量codeMirrorDeps,写在预构建中。

最终配置文件

展示下最终的vite.config.js的文件:

import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'url';
// import viteEslint from 'vite-plugin-eslint';
import dns from 'dns';
import { visualizer } from 'rollup-plugin-visualizer';
import { chunkSplitPlugin } from 'vite-plugin-chunk-split';
import mkcert from 'vite-plugin-mkcert';
import { codeMirrorDeps } from './config/codeMirrorImports.js';
import vue from '@vitejs/plugin-vue2';
import vueJsx from '@vitejs/plugin-vue2-jsx';
/**
 * Node.js 在 v17 以下版本中默认会对 DNS 解析地址的结果进行重新排序。
 * 访问 localhost 时,浏览器使用 DNS 来解析地址,这个地址可能与 Vite 正在监听的地址不同
 * 使用 setDefaultResultOrder('verbatim') 禁用这个排序
 */
dns.setDefaultResultOrder('verbatim');
// https://vitejs.dev/config/
export default defineConfig({
  esbuild: {
    jsxFactory: 'h',
    jsxFragment: 'Fragment'
  },
  plugins: [
    vue(),
    vueJsx(),
    visualizer(),
    chunkSplitPlugin({
      customSplitting: {
        // g6: ['@antv/g6']
      }
    }),
    // 由于 HTTP2 依赖 TLS 握手,插件会自动生成 TLS 证书,然后支持通过 HTTPS 的方式启动,而 Vite 会自动把 HTTPS 服务升级为 HTTP2
    mkcert()
  ],
  optimizeDeps: {
    include: [
      '@antv/g6',
      'vue-codemirror',
      'x2js',
      'insert-css',
      'vue',
      'moment',
      ...codeMirrorDeps
    ]
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  // 开发时运行在localhost: 8080 / 127.0.0.1: 8080
  server: {
    https: true,
    host: 'localhost',
    port: 8080,
    open: true,
    proxy: {
      /** 省略.. */
    }
  },
  publicDir: 'static',
  build: {
    // 8 KB
    assetsInlineLimit: 8 * 1024
  }
});

总结:最后一些话

Webpack切换成Vite其实过程蛮复杂的,需要处理很多东西,这个过程中也有很多坑(其实大部分都跟导入有关,我碰到的是这样)。如果公司给的时间不够充裕的情况下,不建议使用Vite

本篇引用及致谢深入浅出 Vite – 神三元 – 课程 (),想深入学习Vite的同学可以看看。