Vite
是 vue
的作者尤雨溪在开发 vue3.0
的时候开发的一个 web
开发构建工具。由于其原生 ES
模块导入方式,可以实现闪电般的冷服务器启动。Vite
是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
ES
模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。Rollup
打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。Vite
意在提供开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并有完整的类型支持。
这里的背景介绍会从与 Vite
紧密相关的两个概念的发展史说起
JavaScript
的模块化标准共存的模块化标准
为什么 JavaScript
会有多种共存的模块化标准?因为 js
在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从 CommonJS
开始,到 ES6
正式推出 ES Modules
规范结束,所有争论,终成历史,ES Modules
也成为前端重要的基础设施。
CommonJS
:现主要用于 Node.js
(Node@13.2.0开始支持直接使用ES Module)AMD
:require.js 依赖前置,市场存量不建议使用CMD
:sea.js 就近执行,市场存量不建议使用ES
Module:ES语言规范,标准,趋势,未来而 Vite
的核心正是依靠浏览器对 ES Module
规范的实现。
发展中的构建工具
近些年前端工程化发展迅速,各种构建工具层出不穷,目前 Webpack
仍然占据统治地位,npm
每周下载量达到两千多万次。下面是开发者比较熟知的一些构建工具
痛点
现在常用的构建工具如 Webpack
,主要是通过 抓取-编译-构建 整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给 dev server
(开发服务器)。
Webpack
等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server
(开发服务器)性能遇到瓶颈:
dev server
启动时间达到几十秒甚至几分钟。HMR
热更新: 即使采用了 HMR
模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下 Vite
应运而生。
项目创建(以 vue 项目为例)
shnpm init vite@latest
vue
模版就创建完成了vite
和 vue-cli
对比Vite
在开发环境下速度快,体验好,但是只针对 ES6
浏览器。vue-cli
创建的项目开发速度慢,体验差,可以针对旧浏览器;Vite
不支持 vue2
,所以不能选择版本,是直接构建 vue3
。vue-cli
创建项目可以选择版本;Vite
创建的项目没有集成 vue-router
、vuex
等插件,还需要手动安装;vue-cli
作为老牌构建工具,使用者众多,更加经得住历史的考验,并且得益于使用者众多,所以在生态环境和插件数量方面更好。Vite
可以使用插件进行扩展,这得益于 Rollup
优秀的插件接口设计和一部分 Vite
独有的额外选项。这意味着 Vite
用户可以利用 Rollup
插件的强大生态系统,同时根据需要也能够扩展开发服务器和 SSR
功能。
@vitejs/plugin-legacy: 为打包后的文件提供传统浏览器兼容性支持。
shnpm i -D @vitejs/plugin-legacy
vite.config.ts
引入模块并添加配置tsimport legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
legacy({
targets: ['defaults', 'not IE 11']
})
]
})
rollup-plugin-visualizer: 构建分析,打包之后,会在根目录默认生成一个 stats.html 文件
shnpm install rollup-plugin-visualizer
vite.config.ts
中引入模块并添加配置tsimport { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import legacy from "@vitejs/plugin-legacy";
import { visualizer } from "rollup-plugin-visualizer";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ["defaults", "not IE 11"],
}),
visualizer()
],
});
stats.html
文件为了与某些 Rollup
插件兼容,可能需要强制执行插件的顺序,或者只在构建时使用。这应该是 Vite
插件的实现细节。可以使用 enforce
修饰符来强制插件的位置:
pre
:在 Vite 核心插件之前调用该插件默认
:在 Vite 核心插件之后调用该插件post
:在 Vite 构建插件之后调用该插件一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的插件将按照以下顺序排列:
- Alias
- 带有 enforce: ‘pre’ 的用户插件
- Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有 enforce: ‘post’ 的用户插件
- Vite 后置构建插件(最小化,manifest,报告)
示例:
tsimport { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
{
...legacy({
targets: ['defaults', 'not IE 11']
}),
enforce: 'pre'
},
vue()
]
})
默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用。如果插件在服务或构建期间按需使用,请使用 apply 属性指明它们仅在 ‘build’ 或 ‘serve’ 模式时调用
示例:
tsexport default defineConfig({
plugins: [
{
...legacy({
targets: ['defaults', 'not IE 11']
}),
enforce: 'pre',
apply: 'build'
},
]
})
配置约定
如果插件不使用 Vite
特有的钩子,可以作为 兼容 Rollup
的插件 来实现,推荐使用 Rollup
插件名称约定。
-Rollup
插件应该有一个带 rollup-plugin-
前缀、语义清晰的名称。
对于 Vite
专属的插件:
如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:
vite
添加使用自定义插件
my-plugin
执行初始化index.ts
添加测试内容tsimport type { ResolvedConfig } from "vite";
const examplePlugin = () => {
let config: any;
return {
name: "read-config",
configResolved(resolvedConfig: ResolvedConfig) {
console.log("resolvedConfig=>", resolvedConfig);
// 存储最终解析的配置
config = resolvedConfig;
},
// 在其他钩子中使用存储的配置
transform(code: string, id: string) {
// console.log(code,id)
if (config.command === "serve") {
// dev: 由开发服务器调用的插件
} else {
// build: 由 Rollup 调用的插件
}
},
};
};
export default examplePlugin;
vite.config.ts
中进行配置使用tsimport examplePlugin from "./src/my-plugin/index";
export default defineConfig({
plugins: [
vue(),
exmaplePlugin()
]
})
字段 | 说明 | 所属 |
---|---|---|
name | 插件名称 | vite 和 rollup 共享 |
handleHotUpdate | 执行自定义 HMR(模块热替换)更新处理 | vite 独享 |
config | 在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并 | vite 独享 |
configResolved | 在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作 | vite 独享 |
configureServer | 是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件。 | vite 独享 |
transformIndexHtml | 转换 index.html 的专用钩子。 | vite 独享 |
options | 在收集 rollup 配置前,vite (本地)服务启动时调用,可以和 rollup 配置进行合并 | vite 和 rollup 共享 |
buildStart | 在 rollup 构建中,vite (本地)服务启动时调用,在这个函数中可以访问 rollup 的配置 | vite 和 rollup 共享 |
resolveId | 在解析模块时调用,可以返回一个特殊的 resolveId 来指定某个 import 语句加载特定的模块 | vite 和 rollup 共享 |
load | 在解析模块时调用,可以返回代码块来指定某个 import 语句加载特定的模块 | vite 和 rollup 共享 |
transform | 在解析模块时调用,将源代码进行转换,输出转换后的结果,类似于 webpack 的 loader | vite 和 rollup 共享 |
buildEnd | 在 vite 本地服务关闭前,rollup 输出文件到目录前调用 | vite 和 rollup 共享 |
closeBundle | 在 vite 本地服务关闭前,rollup 输出文件到目录前调用 | vite 和 rollup 共享 |
使用 Vite
构建前端项目时,Vite
会在一开始将项目中的代码分为依赖和源码两大类。
依赖即开发项目时使用的第三方库,一个稍大的项目会有成百上千个依赖,处理这些依赖库的性能代价是比较高的,而且不同依赖库使用的模块化规则不同(如 UMD、ESM 或 CommonJS),所以还需要将其转成相关的模块化规则。
对于依赖的处理,我们通常称为依赖预构建,Vite 使用 esbuild 来实现依赖预构建,将 CommonJS 和 UMD 的依赖库转为 ESM 形式,此外因为依赖库大多数时候不会变化,Vite 会将构建好的依赖存到 node_modules/.vite 目录中,如果依赖变化(package.json 等文件中依赖变化了)则会重新构建。
Vite 使用 esbuild 这个全新 js 打包工具的原因是该工具使用 Go 开发,相比于 gulp、rollup 等使用 JS 开发的传统打包工具,esbuild 会快上十几倍,因为打包其实是 CPU 密集型操作,编译型语言会比动态语言快很多(目前前端的一个趋势便是使用编译型语言开发的工具去替代已有的一些工具)。 esbuild 目前还比较新,虽然重要的功能都有了,但还是有部分功能缺失,如对 CSS 处理还不太好,所以 Vite 目前只是要 esbuild 实现开发期间的构建操作,项目开发完要正式构建时使用的是 rollup,rollup 非常成熟,虽然慢一点,但正式构建在开发过程中不会太频繁。
源码,通常就是我们开发项目时自己写的 JS 代码,这些代码时常被修改,但并不是每次修改都需要重新加载所有源码的,Vite 利用浏览器 ES Modules(ESM),当浏览器发起相应模块的请求时,Vite 内置的基于 Koa 构建的 web 服务器会拦截 ES Module 请求,并通过 path 找到想要目录的文件,通过简单的处理再返回给浏览器。
Vite 跳过了打包的动作,Webpack 每次需要将代码打包成一个完整的 JS,当项目变大时,打包就会变慢,这是根本原因,而 Vite 在开发阶段构建项目时会将其构建成 ESM 的形式,这让浏览器来决定什么使用要加载什么模块,然后 Vite 拦截并处理浏览器对模型加载的请求,从而实现真正的按需加载,不再需要打包。
通过 Vite 官网中的两张图,可以更清晰的理解其中的差异:
webpack是先打包再启动开发服务器,vite是直接启动开发服务器,然后按需编译依赖文件,vite基本原理是无需打包,依赖浏览器原生支持esm 去解析模块.
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!