前言
在前端的发展过程中,我们见证了诸如 webpack
、Rollup
和 Parcel
等工具的变迁,它们极大地改善了前端开发者的开发体验。我们知道前端构建工具的当下依旧是webpack
,那么Vite
会是未来吗?
1. 前端工程化痛点
学习前端构建工具的的前提是了解工程化的痛点。⽆论⼯具层⾯如何更新,它们解决的核心问题,即前端⼯程的痛点是不变的。因此,想要知道哪个⼯具更好⽤,就要看它解决前端⼯程痛点的效果。那么,前端⼯程都有哪些痛点呢?
- 前端的模块化需求。我们知道,业界的模块标准非常多,包括 ESM(vite)、CommonJS(nodejs webpack)、AMD 和 CMD 等等。前端⼯程需要落实这些模块规范,同时要兼容不同的模块规范,以适应不同的执⾏环境。
- 兼容浏览器,编译⾼级语法。由于浏览器的实现规范所限,只要⾼级语⾔/语法(TypeScript、JSX 等)想要在浏览器中正常运行,就必须被转化为浏览器可以理解的语法。这都需要⼯具链层⾯的⽀持,⽽且这个需求会⼀直存在。
- 线上代码的质量问题。和开发阶段的考虑侧重点不同,⽣产环境中,我们不仅要考虑代码的安全性、兼容性问题,保证线上代码的正常运⾏,也需要考虑代码运⾏时的性能问题。由于浏览器的版本众多,代码兼容性和安全策略各不相同,线上代码的质量问题也将是前端⼯程中⻓期存在的⼀个痛点。
- 开发效率。项⽬的冷启动启动时间、热更新时间都可能严重影响开发效率,尤其是当项⽬越来越庞⼤的时候。因此,提⾼项⽬的启动速度和热更新速度也是前端⼯程的重要需求。
2. Vite的优势
就Webpack而言,在项目中启动花个⼏分钟都是很常⻅的事情,热更新也经常需要等待⼗秒以上。这主要是因为:
- 项⽬冷启动时必须递归打包整个项⽬的依赖树
- JavaScript 语⾔本⾝的性能限制,导致构建性能遇到瓶颈,直接影响开发效率
Vite非常高效的解决了以上两个问题:
- Vite 在开发阶段基于浏览器原⽣ ESM 的⽀持实现了 no-bundle 构建
Q: 什么是 no-bundle 构建?
A: No-Bundle不像传统构建方式那样将所有的代码打包成一个或多个文件,而是直接将各个模块作为独立的文件加载到浏览器中。这是基于浏览器原生支持的 ES6 模块加载功能(即 import 和 export)实现的。
- 借助 Esbuild 超快的编译速度来做第三⽅库构建和 TS/JSX 语法编译,从⽽能够有效提⾼开发效率。
除了开发效率,Vite在其他方面也表现不俗:
- Vite 基于浏览器原⽣ ESM 的⽀持实现模块加载,并且⽆论是开发环境还是⽣产环境,都可以将其他格式的产物(如 CommonJS)转换为 ESM。
- Vite 内置了对 TypeScript、JSX、Sass 等⾼级语法的⽀持,也能够加载各种各样的静态资源,如图⽚、Worker 等等。
- Vite 基于成熟的打包⼯具 Rollup 实现
生产环境
打包,同时可以配合 Terser 、Babel 等⼯具链,可以极⼤程度保证构建产物的质量。
3. 基于Vite搭建vue项目
js
npm create vite@latest
终端执行上述命令,按照提示操作即可 (本文项目用例是 vue3 + ts)。也可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目
js
npm create vite@latest my-vue-app -- --template vue
在一个 Vite 项目中,index.html
在项目最外层,而不是在 public
文件夹内。这是有意而为之的:在开发期间 Vite 是一个服务器,而 index.html是该 Vite 项目的入口文件
。Vite 将 index.html
视为源码和模块图的一部分。Vite 解析 <script type="module" src="...">
,这个标签指向你的 JavaScript 源码。
Q: 关于ts文件中,为什么引入vue文件会使vscode编辑器出现红线错误提示?
A: 在Vue 3中,通常使用单文件组件(SFC),这些文件拥有.vue
扩展名。TypeScript默认是不理解以.vue结尾的这类文件的,因此需要这样的声明来告诉编译器如何处理它们。通过下面的声明让你可以从TypeScript中导入.vue
文件,并将其视为DefineComponent
类型。
js
declare module "*.vue" {
import { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any>
export default component
}
4.关于css的配置
Vite 提供了对 .scss
, .sass
, .less
, .styl
和 .stylus
文件原生的内置支持,因此没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖。比如:
js
# .scss and .sass
npm add -D sass
# .less
npm add -D less
# .styl and .stylus
npm add -D stylus
由于 Vite 的目标仅为现代浏览器,因此建议使用原生 CSS 变量 或者 引入PostCSS的相关插件(例如 postcss-preset-env
: 这是一个强大的CSS转换器,能够帮助开发者们优雅地过渡到最新的CSS语法,同时确保浏览器的兼容性)。
js
npm i postcss-preset-env -D
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import postcssPresetEnv from 'postcss-preset-env'
export default defineConfig({
plugins: [vue()],
css: {
postcss: {
plugins: [postcssPresetEnv()]
}
}
})
5. 静态资源处理
导入一个静态资源会返回解析后的 URL:
js
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import vueUrl from '../assets/vue.svg';
onMounted(() => {
// 图片地址在 src/assets 文件目录
(<HTMLImageElement>document.getElementById('vue-img')).src = vueUrl;
// 图片地址在 public 文件目录
(<HTMLImageElement>document.getElementById('vite-img')).src = '/vite.svg';
})
</script>
<template>
<div>
<img src="" alt="" id="vue-img">
<img src="" alt="" id="vite-img">
</div>
</template>
6. Vite 的依赖预构建
首次启动 vite
时,Vite 在本地加载站点之前,会预构建项目依赖。依赖预构建的目的有两个:
- 将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块
- 为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。
例如,
lodash-es
有超过 600 个内置模块!当我们执行import { debounce } from 'lodash-es'
时,浏览器同时发出 600 多个 HTTP 请求!即使服务器能够轻松处理它们,但大量请求会导致浏览器端的网络拥塞,使页面加载变得明显缓慢。
可以通过vite 的配置来控制预构建,如下所示,默认情况下是会对所有的依赖进行预构建处理,可以通过 exclude
来排除某些依赖。lodash-es
按照如下配置,会发现网络请求有600多个,会阻塞页面渲染:
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: { exclude: ['lodash-es'] } // exclude 表示不要对插件 'lodash-es' 执行预构建
})
7. Vite 的双引擎架构
Vite 的双引擎架构不仅仅停留在开发阶段使⽤ Esbuild,⽣产环境⽤ Rollup 的阶段。Vite 真正的架构远没有这么简单。
7.1 Esbuild 的高性能和缺点
Esbuild 的确是 Vite ⾼性能的得⼒助⼿,在开发构建阶段让 Vite 获相当优异的性能,如果这个阶段⽤传统的打包器/编译器来完成的话,开发体验要下降⼀⼤截。一般来说,node_modules 依赖的⼤⼩动辄⼏百MB甚⾄上GB,会远超项⽬源代码。如果这些依赖直接在 Vite 中使⽤,会出现⼀系列的问题,所以需要 Esbuild 在依赖预构建阶段进行处理。
Vite 1.x 版本中使⽤ Rollup 来做这件事情,但 Esbuild 的性能实在强悍,Vite 2.x果断采⽤Esbuild来完成第三⽅依赖的预构建。
当然,Esbuild 作为打包⼯具也有⼀些缺点:
- 不⽀持降级到 ES5 的代码。这意味着在低端浏览器代码会跑不起来。
- 不⽀持
const 枚举
等语法。这意味着单独使用这些语法在 esbuild 中会直接抛错。 - 不提供操作打包产物的接⼝,像 Rollup 中灵活处理打包产物的能⼒(如 renderChunk 钩⼦)在Esbuild 当中完全没有。
- 不⽀持⾃定义 Code Splitting 策略。传统的 Webpack 和 Rollup 都提供了⾃定义拆包策略的 API,⽽ Esbuild 并未提供,从⽽降级了拆包优化的灵活性。
尽管 Esbuild 有如此多的的局限,但是不妨碍 vite 在开发阶段使用它启动项⽬并获得极致的性能提升。
⽣产环境处于稳定性考虑当然是采⽤功能更加丰富、⽣态更加成熟的 Rollup 作为依赖打包⼯具了
。
7.2 Esbuild 对 TS/JSX 的处理
Esbuild 转译 TS 或者 JSX 的能⼒通过 Vite 插件 Esbuild Transfomer
提供,这个Vite 插件在开发环境和生产环境都会执⾏。这部分能⼒恰好⽤来替换原先 Babel 或者 TSC 的功能
,因为⽆论是 Babel 还是 TSC都有性能问题,⼤家对这两个⼯具普遍的认知都是:太慢了。虽然 Esbuild Transfomer
能带来巨⼤的性能提升,但其⾃⾝也有局限性,最⼤的局限性就在于 TS 中的类型检查问题
。这是因为 Esbuild 并没有实现 TS 的类型系统,在编译 TS (或者 TSX )⽂件时仅仅抹掉了类型相关的代码,暂时没有能⼒实现类型检查。
Vite 之所以不把类型检查作为转换过程的一部分,是因为这两项工作在本质上是不同的。转译可以在每个文件的基础上进行,与 Vite 的按需编译模式完全吻合。相比之下,类型检查需要了解整个模块图。把类型检查塞进 Vite 的转换管道,将不可避免地损害 Vite 的速度优势。
7.3 Esbuild 的压缩能力
在生产环境中 Esbuild 压缩器通过插件的形式融⼊到了 Rollup 的打包流程
中。传统的⽅式都是使⽤Terser
这种 JS 开发的压缩器来实现,在 Webpack 或者 Rollup 中作为⼀个 Plugin 来完成代码打包后的压缩混淆的⼯作。但Terser压缩很慢,原因如下:
- 压缩这项⼯作涉及⼤量 AST 操作,并且在传统的构建流程中,AST 在各个⼯具之间⽆法共享,⽐如 Terser 就⽆法与 Babel 共享同⼀个 AST,造成了很多重复解析的过程。
- JS 本⾝属于解释性 + JIT(即时编译) 的语⾔,对于压缩这种 CPU 密集型的⼯作,其性能远远⽐不上 Golang 这种原⽣语⾔。
Esbuild 这种从头到尾共享 AST 以及原⽣语⾔编写的 Minifier 在性能上能够甩开传统⼯具的好⼏⼗倍
7.4 生产环境的构建基石 ------ Rollup
Rollup 在 Vite 中的重要性一点也不亚于 Esbuild,它既是 Vite ⽤作⽣产环境打包的核⼼⼯具,也直接决定了 Vite 插件机制的设计。虽然 ESM 已经得到众多浏览器的原⽣⽀持,但是为了在⽣产环境中也能取得优秀的产物性能(既能向下兼容各种浏览器,又能打包压缩减少网络性能
),Vite 默认选择在⽣产环境中利⽤ Rollup 打包,并基于 Rollup 本⾝成熟的打包能⼒进⾏扩展和优化,主要包含 2 个⽅⾯:
- CSS 代码分割。如果某个异步模块中引⼊了⼀些 CSS 代码,Vite 就会⾃动将这些 CSS 抽取出来⽣成单独的⽂件,提⾼线上产物的 缓存复⽤率 。
- ⾃动预加载。Vite 会⾃动为⼊⼝ chunk 的依赖⾃动⽣成预加载标签 ,适当的预加载会让浏览器提前下载好资源,优化页面性能。如:
html
<head>
<!-- ⼊⼝ chunk -->
<script type="module" crossorigin src="/assets/index.250e0340.js"></script>
<!-- ⾃动预加载⼊⼝ chunk 所依赖的 chunk-->
<link rel="modulepreload" href="/assets/vendor.293dca09.js">
</head>
8. Vite对低版本浏览器的兼容处理
对低版本浏览器的向下兼容,核心在于babel的配置,可以参考我的文章 在前端工程中,我是这样配置babel的
这种处理简单来说就是把JavaScript 中 es2015 / 2016 / 2017 / 2046 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。严格来说,babel 也可以转化为更低的规范。但以目前情况来说,es5 规范已经足以覆盖绝大部分浏览器,因此常规来说转到 es5 是一个安全且流行的做法。babel 作为一种常用的前端转译器来实现这一过程
在vite 的实际配置中是通过 vite.config.ts 配置实现,引入相关插件,配置如下:
js
npm i @vitejs/plugin-legacy -D
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import postcssPresetEnv from 'postcss-preset-env'
import path from 'path'
import { splitVendorChunkPlugin } from 'vite'
import legacy from '@vitejs/plugin-legacy'
// https://vitejs.dev/config/
export default defineConfig({
build: {
outDir: 'dist'
},
plugins: [
vue(),
splitVendorChunkPlugin(), // vite自动分包
legacy({
targets: ['defaults', 'not IE 11']
})
],
css:{
postcss:{ plugins:[postcssPresetEnv()] }
},
resolve: {
alias: { '@assets': path.resolve(__dirname, './src/assets/') }
}
})
打包的产物如下:
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>demo</title>
<!--不做任何兼容性处理 -->
<script type="module" crossorigin src="/assets/index-32d4285d.js"></script>
</head>
<body>
<div id="app"></div>
<!-- 进⾏兼容性处理的垫⽚ -->
<script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-491d1e34.js"></script>
<!-- 这⼀段的执⾏依赖前⾯的 polyfills -->
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-18fb56248d.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
</body>
</html>
可以分析一下上面的打包产物,产物可以分为两种类型:module类型
和 nomodule类型
。现代浏览器访问的时候,由于不需要做兼容处理,浏览器可以直接访问 module类型
的js文件,浏览器会自动忽略nomodule类型
js文件的加载;低版本浏览器访问的时候,需要依赖兼容性的垫片处理得到的 nomodule类型
的js文件,同时浏览器会自动忽略 module类型
的js文件。