Vite也许没那么快,但Vue2.7是真的香——Vite+Vue2.7使用纪实

14,216 阅读10分钟

前言

关于Vite和Vue3的讨论越来越多,看了官网的特性后,真是按捺不住想尝试一下。开发环境秒开?Composition APISFC Style CSS Variable Injection? 看起来哪个都比webpack+Vue2香呀。(尤大都向React推荐Vite了,难道你还不试一下Vite么?)
其实在去年,我们在LOFTER的哈利波特街区活动中就尝试使用了Vite2 + Vue3搭建活动主街区页面,当时对Vue3的语法还不是很适应,为啥ref要用value去取值?reactive解构后的响应性怎么没了?Vite热更新咋不太灵光?为了保证活动页在Android6以下能满足基本交互需要,还引入了es6-proxy-polyfill,友好的提示用户系统不支持。出于兼容性和稳定性的考虑,我们没有在其他项目中应用。
时间来到今年七月,随着Vue2.7的正式发布,很多Vue3的编码特性被正式迁移到了Vue2,更像是Vue3的基础试用版(Vue:再过18个月,我也懒得管Vue2了)。

The Composition API is backported using Vue 2's getter/setter-based reactivity system to ensure browser compatibility.

就是官方的这句话,让我觉得把现在的Vue2项目完全升到2.7是一件百利而无一害的事情。我们的Vue2项目随着业务迭代承载着更加复杂的业务场景,如何提升项目的开发体验也提上了日程。实际使用中,Vite还是挺多坑要踩的,但是Vue2.7更新的内容,结合社区生态,对业务实现和开发体验的提升都提供了助力

实战——检验Vite+Vue2.7

那就找个场景先预研一下吧,于是稻米节活动被拿来做了实验(但愿不会被稻米看到)。
通过对官方文档的阅读,以及查看社区的讨论,Vue2.7最次也是支持option api的(万一遇到解决不了的bug降级一下写法就好了)。既然做实验,就要有实验目的。

  1. 检验SFC setup语法糖的各种写法兼容性
  2. 探索Vite现在发展的怎么样了
  3. 检验Vite生产环境的稳定性

先贴个结论,1和2的结果都挺好,3就是坑多多了。
实验流程:

项目搭建

单纯的Vite Getting Started流程实在是过于简单,啥配置都没有,难不成每个功能都要去搜索引擎查?还好有 awesome-vue 这个项目,集结了大家的智慧(踩过的坑),参阅了各种脚手架模板后,结合业务需要,项目架子初步搭好。
简单贴一下package.json

"dependencies": {
  "@sentry/browser": "^5",
  "@sentry/integrations": "^5", // sentry用来收集异常
  "@vueuse/core": "^9.0.2",  // 基于Composition API的工具函数,同时支持Vue2, Vue3
  "axios": "^0.27.2",
  "nejsbridge": "^1.7.19",
  ……
  "vue": "^2.7.8",
  "vue-clipboard2": "^0.3.3",
  "vue-router": "^3.5.4"
},
"devDependencies": {
  "@antfu/eslint-config": "^0.25.2", // Anthony Fu是Vue和Vite团队的核心成员,有很多开源作品
  "@vitejs/plugin-legacy": "^2.0.0", // 自动生成传统版本的chunk及与其相对应ES语言特性方面的polyfill
  "@vitejs/plugin-vue2": "^1.1.2", // plugin only works with Vue@^2.7.0.
  "autoprefixer": "^10.4.8", 
  "eslint": "^7.32.0",
  "less": "^4.1.3",
  "nei-ts-helper": "^0.1.3", // 组内接口生成TS声明的工具
  "terser": "^5.4.0", // 生产环境打包代码需要
  "typescript": "^4.6.4",
  "unplugin-auto-import": "^0.10.1",  // vue函数的自动导入
  "unplugin-vue-components": "^0.21.2", // vue组件库的自动按需导入
  "vite": "^3.0.4",
  "vue-tsc": "^0.38.4"
}

重点推荐一下unplugin-auto-importunplugin-vue-components

unplugin-auto-import解决了vue3-hook、vue-router、useVue等多个插件的自动导入,也支持自定义插件的自动导入,是一个功能强大的Typescript支持工具。基于unplugin,在构建和打包的时候自动解析模块并引入。

// vite.config.js
plugins: [
  AutoImport({
    imports: [
      'vue',
      'vue-router',
      '@vueuse/core',
    ],
    // 解决eslint报错问题
    eslintrc: {
      enabled: false,
      globalsPropValue: true,
    },
    dts: 'src/auto-imports.d.ts',
  }),
  ……
]
// ==> 自动生成全局声明 auto-imports.d.ts

declare global {
  const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
  const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
  const computedAsync: typeof import('@vueuse/core')['computedAsync']
  ……
}

unplugin-vue-components支持自定义组件自动引入,同样基于unplugin

// vite.config.js
plugins: [
  Components({
    transformer: 'vue2', // vue2.7必需
    dirs: ['src/components'],
    extensions: ['vue'],
    dts: 'src/components.d.ts',
  }),
  ……
]
// ==> 自动生成全局声明 auto-imports.d.ts

declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    Container: typeof import('./components/Container.vue')['default']
    Header: typeof import('./components/Header.vue')['default']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    ……
  }
}

业务开发

  • 使用插件后代码的书写简便了很多,格式更加清爽,import理论上可以通过完善插件做到完全消除
<script setup>
  import {
    getNosThumbWebP,
  } from '@/common/utils';

  const props = defineProps(['blogNickName']);
  const name = computed(() => (props.blogNickName ? `@${props.blogNickName}` : '你的'));

  const bgUrl = `url(${getNosThumbWebP('https://lofter.lf127.net/xxx/header.png', {
    thumbnail: '1125x0',
  })})`;

</script>

<template>
  <div class="header">
    <div class="user-name">{{name}}</div>
  </div>
</template>

<style scoped lang="less">
  .header {
    width: 100%;
    height: 1333px;
    background-image: v-bind(bgUrl);
    background-size: 100% 100%;
    background-repeat: no-repeat;
    position: relative;
    .user-name {
      ……
    }
  }
</style>

注意:v-bind作用的元素如果有v-if的场景,最好不是template里的根元素,否则可能导致css变量无处挂载。

  • setup语法糖,自己用了才知道,真的好用。还需要在更复杂的业务场景中实践,结合类似vueuse的用法做逻辑封装。
  • 热更新经常无效,Vue.extend(ToastConfig)创建的组件完全不会更新,需要重启dev server才行。

打包测试

  1. 踩过坑才知道下面这部分内容有多重要
  • 构建生产版本-浏览器兼容性
  • 用于生产环境的构建包会假设目标浏览器支持现代 JavaScript 语法。默认情况下,Vite 的目标是能够 支持原生 ESM script 标签支持原生 ESM 动态导入 import.meta 的浏览器:
  • 你也可以通过 build.target 配置项 指定构建目标,最低支持 es2015。
  • 请注意,默认情况下 Vite 只处理语法转译,且 默认不包含任何 polyfill。你可以前往 Polyfill.io 查看,这是一个基于用户浏览器 User-Agent 字符串自动生成 polyfill 包的服务。
  • 传统浏览器可以通过插件 @vitejs/plugin-legacy 来支持,它将自动生成传统版本的 chunk 及与其相对应 ES 语言特性方面的 polyfill。兼容版的 chunk 只会在不支持原生 ESM 的浏览器中进行按需加载。
// 通过监测支持import.meta.url和动态引入判断是否为现代浏览器
<script type="module">try{
  import.meta.url;
  import("_").catch(()=>1);
}catch(e){}
window.__vite_is_modern_browser=true;
</script>
  <script type="module">!function(){
  if(window.__vite_is_modern_browser)return;
  console.warn("vite: loading legacy build because dynamic import or import.meta.url is unsupported, syntax error above should be ignored");
  var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)
}();</script>

线上的一个白屏问题,发现报错源自try{}catch{}这种ES10才支持的语法。
vite3修改了默认的打包配置,仅支持Chrome >=87。需要通过修改build: target提升构建产物的兼容性

  1. _VITE_ASSET legacy.js打包的css相对路径不对,全换成cdn资源解决(最新版vite已修复该问题)。

暂没有遇到其他兼容性问题。

进阶——将一个重度Webpack+Vue2工程升级为Vite+Vue2.7

LOFTER市集工程用webpack的重度体现在

  1. 一套dev+build+init命令行交互
  2. 自定义页面参数配置,结合HtmlWebpackPlugin形成动态页面配置,HtmlWebpackPlugin确实是多页应用构建的神器
  3. 有些年头的css,babel配置
  4. 私有的webpack插件,离线包插件等

这些要么移除掉,要么在Vite中找到替代方案。

开始升级。。。

  1. 安装vitevue2.7@vitejs/plugin-vue2@vitejs/plugin-legacy去掉vue-template-compiler
  2. 增加vite.config.js,配置aliasless,vite会默认按root配置的文件目录,自动查找文件目录下的index.html,页面模板中添加代码
<script type="module" src="/cardHome/index.js"></script>

然后就是疯狂报错

  • require url问题
vue.runtime.esm.js:4573 [Vue warn]: Error in created hook: "ReferenceError: require is not defined"

import.meta.url 是一个 ESM 的原生功能,会暴露当前模块的 URL。将它与原生的 URL 构造器 组合使用,在一个 JavaScript 模块中,通过相对路径我们就能得到一个被完整解析的静态资源 URL

// icon: require('@/common/images/card/upgrade/home.png') =>
icon: new URL('@/common/images/card/upgrade/home.png', import.meta.url).href
  • svg 雪碧图插件替换,svg-sprite-loader->vite-plugin-svg-icons

symbolId的组装方式不同,stroke color替换后者有问题,暂时可以通过样式覆盖解决

  • css-loader中@value的用法在vite中不支持,可以使用less变量的方式替换,不全局使用css变量一个是兼容性,还有是市集本身颜色不需要切换
/* @value --default_common_bg_tertiary: #3E3E3E;  */
@default_common_bg_tertiary: #3E3E3E;

/* background: --default_common_bg_tertiary; */
background: @default_common_bg_tertiary;
  1. 使用HTTP2,可以配置server.https来启用,像卡牌首页的代码模块很多,通过H2可以改善请求量大导致的页面刷新耗时较长的问题。但是

注意:当 server.proxy 选项 也被使用时,将会仅使用 TLS。

这会导致如果配置了接口代理的情况下项目又用不了H2了。
可以使用nginx解决该问题,最新版nginx(1.22.0)支持配置listen 443 ssl http2;结合server.httpsbase配置的路径,可以实现在开发环境下使用H2加速页面刷新。

Webpack与Vite开发环境比较

基于win10,i5-6500 CPU,16.0 GB内存

webpack
devtool: 'cheap-module-eval-source-map',
vite
便于比较,使用http/1.1
无缓存run dev全部40s
首页+背包24s
暂无全部数据
首页+背包3s+,需要更长渲染时间
有缓存run dev全部30s
首页+背包18s
暂无全部数据
首页+背包3s-
热更新首页7s左右无提示,看起来很快,有时候改了组件代码刷新也没用,只能重新run dev
背包页无缓存刷新38 requests
3.1 MB transferred
12.7 MB resources
Finish: 5.44 s
DOMContentLoaded: 1.79 s
Load: 2.52 s
191 requests
3.7 MB transferred
3.7 MB resources
Finish: 3.49 s
DOMContentLoaded: 1.81 s
Load: 3.46 s
背包页有缓存刷新34 requests
12.6 kB transferred
12.7 MB resources
Finish: 1.99 s
DOMContentLoaded: 1.20 s
Load: 1.91 s
191 requests
446 kB transferred
3.7 MB resources
Finish: 3.02 s
DOMContentLoaded: 1.48 s
Load: 2.99 s
首页无缓存刷新82 requests
20.1 MB transferred
38.0 MB resources
Finish: 5.55 s
DOMContentLoaded: 2.57 s
Load: 4.35 s
278 requests
20.8 MB transferred
24.9 MB resources
Finish: 4.84 s
DOMContentLoaded: 2.46 s
Load: 4.42 s
首页有缓存刷新81 requests
120 kB transferred
38.0 MB resources
Finish: 3.39 s
DOMContentLoaded: 1.82 s
Load: 3.09 s
278 requests
557 kB transferred
24.4 MB resources
Finish: 3.88 s
DOMContentLoaded: 1.92 s
Load: 3.15 s
  • 比较1:在使用esm的情况下,页面的渲染速度并没有显著提升,使用H2或加强模块异步加载的处理会稍微好点。

在实际项目中,Rollup 通常会生成 “共用” chunk Vite 将使用一个预加载步骤自动重写代码,来分割动态导入调用,以实现当 A 被请求时,C 也将 同时 被请求: Entry ---> (A + C) C 也可能有更深的导入,在未优化的场景中,这会导致更多的网络往返。Vite 的优化会跟踪所有的直接导入,无论导入的深度如何,都能够完全消除不必要的往返。

  • 比较2:esm会发出更多的requests,浏览器等待耗时明显,偶尔可能会阻塞导致更长的渲染时间
  • 比较3:Vite热更新不理想,需要经常刷新或者重启,重启dev虽然很快,但是页面渲染等待耗时可能较长

关于vite的热更新: Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API。 Vite 热更新问题排查 可以监听自定义 HMR 事件,在插件中处理未更新的情况

还有一些应用场景进一步实践后再和大家分享。

一些感想:

  • Vite更像是一个上层建筑,集合了esbuild,rollup,各种loader等,如果项目比较复杂,仍然需要深度定制,逃不开配置的递归使用
  • 新项目还是很推荐用Vite构建,直接使用Vite和rollup的生态
  • 老项目从webpack迁移还是缺少一些完备的插件,也是为社区贡献的机会,开发环境Vite生产环境webpack理论上是可行的,需要处理两者之间的差异,比如环境变量,插件的差异等等。开发环境和生产环境的构建方式不同总感觉会有坑,还是需要慎重