Vite笔记
Vite组成、原理、配置、插件、性能优化
Vite 读 /vit/
资源
- [官方文档](https ://Vitejs.dev/)
- Vite源码
- Awesome Vite
Vite发展&现状
- 2021.2 v2版本
- 2022.7 v3版本
- 2022.12-至今 v4版本
- vue-cli官方推荐使用
create-vue
来创建基于 Vite 的新项目
Vite基本概念
Vite和webpack一样都是构建工具。
使用webpack打包大型的前端应用,启动开发服务器时webpack必须优先抓取并分析 整个应用的依赖 ,由于大型项目包含大量模块,可能需要很长时间(甚至是几分钟)才能启动dev server,文件修改后的HMR也可能长达几时秒到一分钟,极大影响开发者的开发效率。
Vite就是为了解决这个问题而诞生。
目前Vite主要由两部分组成:
- 一个开发服务器,服务于开发环境,它基于 原生 ES 模块 和ESBuild,提供了 丰富的内建功能:如速度很快的 模块热更新(HMR)。其原理是利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。
- 一套构建指令,用于生产环境打包,使用 Rollup 打包代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
上手
使用npm create vite@latest
命令初始化一个Vite项目
有六种框架可选:
选择vue框架,初始化后的目录结构是这样:
相较于过去基于webpack的vue项目,有2个不同之处。
1是index.html从/public移出到了根目录,这是因为对于Vite项目而言,index.html就是一切依赖的入口。
2是Vite.config.ts 替代了 vue.config.js。
Vite其他命令如下:
Vite
启动开发服务器Vite build
打包Vite optimize
预构建依赖Vite preview
本地预览打包产物
本地启动项目后,可以看到index.html中以原生ES模块的方式,引入了main.js和其他依赖:
对于浏览器对.vue文件的HTTP请求,通过设置Content-Type: application/javascript
,被处理成了浏览器可识别的js逻辑,并且js中用的也是原生ES模块的导入导出语法:
本地打包后,启动preview服务器查看效果:
学Vite的几个前置条件
1 了解ES Module
Vite基于原生ESM实现。
现在js程序越来越复杂,因此近年来Nodejs都支持将 JavaScript 程序拆分为可按需导入的单独模块的机制,例如,CommonJS 和基于 AMD 的其他模块系统 如 RequireJS、 Webpack 、 Babel。
但是这类模块系统服务器启动缓慢,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,流程如下图:
最新的浏览器开始原生支持模块功能、浏览器支持es语法中的import和export语法
可以通过 <script type="module" src="xx"> 引入模块
。
浏览器通过HTTP请求的方式,按需加载每一个被引入的模块,在启动开发服务器时,可以达到免打包、随开随用的效果,流程如下:
2 了解ESBuild
那么Vite为什么不直接使用浏览器原生esm,还要用ESBuild呢?
因为浏览器原生ESM功能有限,例如:只支持用完整的相对路径/绝对路径引入模块,不支持引入裸模块(以模块名的方式从node_modules下引入模块),会报错,举个例子引入lodash的报错:
为什么浏览器不支持自动从node_modules下寻找并引入模块?如果某个模块依赖项很多,而每个模块又会产生一次HTTP请求,会导致严重的HTTP请求并发问题。
而 ESBuild 会对依赖进行 预构建,对原生的ESM功能进行了扩展。
另外,ESBuild 使用 Go 编写,比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。(文章 - ESBuild 为什么那么快)
3 了解Rollup
对于生产环境,Vite会使用rollup进行生产环境的构建和打包。
ESBuild 也支持打包,为何不用 ESBuild 打包?
目前Vite团队认为 Rollup 提供了更好的性能与灵活性方面的权衡:Vite 目前的插件 API 与使用 ESBuild
作为打包器并不兼容。尽管 ESBuild
速度更快,但 Vite 采用了 Rollup 灵活的插件 API 和基础建设,这对 Vite 在生态中的成功起到了重要作用。
(即便如此,ESBuild
在过去几年有了很大进展,Vite不排除在未来使用 ESBuild
进行生产构建的可能性。)
近年来对于构建工具的选择,主流的经验是:
关于Webpack和Rollup的区别,推荐阅读一篇2017年的Medium文章:Webpack and Rollup: the same but different。
相较于Rollup,Webpack有着丰富庞大的生态系统、大量的contributors和sponsors,rollup的生态并没有webpack工具强大。
但Rollup是React, Vue, Ember, Preact, D3, Three.js, Moment等众多知名开源库选择的构建工具,rollup相对webpack更轻量,其构建的代码并不会像webpack一样被注入大量的webpack内部结构,而是尽量的精简保持代码原有的状态。它同样支持tree-shaking、依赖解析等能力。
Dev Server 功能 & 原理
我在本地进行了实际对比:
使用vue cli创建一个webpack+vue的空项目,启动devserver时间在10s左右;
使用Vite创建一个Vite+vue的空项目,启动dev server的时间在1s-2s左右。
Vite本地启动比webpack快很多,那么Vite启动本地服务器的原理是什么?
Vite
会将应用代码分为 依赖 和 源码 两类,对不同的模块进行按需编译。
1 依赖预构建
- 对于依赖:当首次启动
dev server
时,Vite 会在本地加载站点之前预构建依赖。 - 默认情况下,预构建是自动且透明地完成的,可以在Vite.config.js配置中禁用。
- Vite 底层使用
ESBuild
预构建依赖(Esbuild
使用 Go 编写,比以 JavaScript 编写的打包器预构建依赖快 10-100 倍)。
兼容导出格式
为了兼容不同的第三方依赖包的不同的导出格式(例如浏览器不支持的commonjs),ESBuild会将所有依赖包转换为浏览器支持的esm格式,然后放到当前目录 node_modules/.Vite/deps
下。
多包传输优化
为了解决浏览器网络多包传输的性能问题,ESBuild会分析依赖链,将多模块的import集成到一个模块中去,减少 http 请求数。例如引入lodash-es包的合并前后对比:
补齐import路径
浏览器只支持完整相对路径/绝对路径形式引入模块,对于裸模块、省略了文件后缀名的模块,预构建阶段会对路径进行补齐和重写。例如引入lodash包:
import _ from "lodash";
强缓存
依赖包的HTTP请求会被Vite设置为 Cache-Control: max-age=31536000,immutable
进行强缓存。
2 源码转换
对于源码:JSX、CSS 或者 Vue SFC等,Vite会对其进行转换,以 原生 ESM 方式提供源码。
Vite对vue文件的处理
对于.vue文件:Vite会使用vue-compiler将其转为js,通过设置Content-Type:text/javascript,让浏览器将.vue文件识别为js文件并解析。
Vite对css的处理
对于css文件:Vite会将浏览器.css文件的HTTP请求设置Content-Type:text/javascript,然后把css文件的内容替换为一段js代码,这段js代码的逻辑就是将对应的css代码作为 <style>
标签插入到html文件中,并且支持css的热更新功能。
CSS代码分割:Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件(而不是将所有的 CSS 抽取到一个文件中),这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个 <link>
标签载入,该异步 chunk 会保证只在 CSS 加载完毕后再执行,避免发生 FOUC 。
Vite对css模块化的处理
将所有.module.css后缀的文件视为模块化css,例子:
Foo.module.css文件:
.footer{
width: 100px;
height: 100px;
background-color: rosybrown;
}
Bar.module.css文件:
.footer{
width: 100px;
height: 100px;
background-color: rosybrown;
}
对其css代码中的所有类名进行 哈希转换,创建映射对象:
<script setup>
import css from "@/styles/modules/Bar.module.css";
console.log('Bar.module.css映射对象', css);
</script>
然后将原始css文件中的代码抹除,替换成一段JS脚本,这段脚本会将上述的映射对象 默认导出:
这段脚本的逻辑是将哈希转换后的css代码,作为 <style>
标签插入html中:
Vite对less和scss的处理
Vite 也同时提供了对 .scss
, .sass
, .less
, .styl
和 .stylus
文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖:
# .scss and .sass
npm add -D sass
# .less
npm add -D less
# .styl and .stylus
npm add -D stylus
Vite调用各个css预处理器将.scss .less转换为css代码后,对css代码的处理同上述【Vite对css的处理】
协商缓存
- 源码的HTTP请求会被Vite设置为
304 Not Modified
进行协商缓存。
3 静态资源处理
尽量遵循 ESM 方式提供静态资源
Vite 尽量避免直接处理静态资源,而是选择遵循 ESM 方式提供服务,例如引入静态资源图片 import img from 'xxx.png'
,执行后会返回解析后的 URL,供浏览器直接请求:
- 常见的图像、媒体和字体文件类型会被自动检测为资源,可以通过
assetsInclude
选项配置文件后缀名。 - 代码中引入的静态资源,会返回解析后的公共路径:
import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl
-
Vue SFC
<template>
中的资源引用,会被自动转换为import语句。 -
资源体积小于
assetsInlineLimit
选项值 (默认4KB)会被转换为内联的 base64 data URL。 -
在 CSS 中引用的
url()
,也是同样的处理方式。 -
如果需要显式引入一个资源的url,可以加
?url
后缀:import workletURL from 'extra-scalloped-border/worklet.js?url' CSS.paintWorklet.addModule(workletURL)
-
如果需要将资源引入为字符串,可以使用
?raw
后缀:import shaderString from './shader.glsl?raw'
-
public目录:对于不会被源码引入、保持原文件名不加hash的文件,可以放在项目根目录下public目录中,开发时必须直接通过“/”根路径访问,打包时也会被完整复制到根目录下。
-
支持JSON的具名导入(解构导入)
使用 new URL(url, import.meta.url)
:
import.meta.url 是一个 现代浏览器支持的 ESM 的原生功能,会暴露当前模块的 URL。将它与原生的 URL 构造器 组合使用,在一个 JavaScript 模块中,通过相对路径我们就能得到一个被完整解析的静态资源 URL:
const imgUrl = new URL('./img.png', import.meta.url).href
document.getElementById('hero-img').src = imgUrl
在dev server使用上述方法,可以借助浏览器的原生能力,而不需要Vite做处理。
对于生产构建时,Vite 会进行必要的转换保证 URL 在打包和资源哈希后仍指向正确的地址。
Vite打包功能
打包入口
默认情况下,以 <root>/index.html
作为打包入口,并生成能够静态部署的应用程序包。
MPA多页应用:在 build.rollupOptions.input
数组中,定义多个html入口即可。
hash
打包后的资源文件名有hash,与文件内容一一对应(文件内容更新才会更新hash值,便于控制浏览器缓存)。
浏览器兼容性
Vite不支持低版本浏览器:
- 默认情况下 Vite 只处理语法转译,且 默认不包含任何 polyfill 。
- 默认情况下,Vite 的目标是 支持原生 ESM、支持原生 ESM 动态导入 和 import.meta 的浏览器:Chrome >=87,Firefox >=78,Safari >=14,Edge >=88
- 可以通过 build.target 配置项指定构建目标,最低支持 es2015。
- 可以通过插件 @Vitejs/plugin-legacy 支持传统浏览器,最低支持到IE 11
rollup配置
Vite基于Rollup工具打包,可以通过 build.rollupOptions
直接调整底层的 Rollup 选项。
base路径
如果需要 在嵌套的服务器路径下部署项目,可以指定 base
配置项。
build watch
可以使用 Vite build --watch
来启用 rollup 的监听器,文件变化时重新构建。
预加载优化
Vite 会为入口 chunk 和它们在打包出的 HTML 中的直接引入自动生成 <link rel="modulepreload">
指令,实现预加载优化。
Vite配置文件
配置文件的语法提示
Vite的配置文件是 Vite.config.js
,可以直接写成:
export default {}
但是,可以利用 defineConfig
获得语法提示和类型约束:
import { defineConfig } from "Vite";
export default defineConfig({})
也可以利用IDE和jsdoc支持,来获得语法提示
/** @type {import('vite').UserConfig} */
export default {
// ...
}
不同环境配置
类似于webpack.dev.config、webpack.prod.config
Vite在配置文件提供环境区分:
export default defineConfig(({ command, mode, ssrBuild }) => {
if (command === 'serve') {
return {
// dev 独有配置
}
} else {
// command === 'build'
return {
// build 独有配置
}
}
})
可以采用更具有扩展性的方式:
import { defineConfig } from "Vite";
import ViteBaseConfig from "./Vite.base.config";
import ViteDevConfig from "./Vite.dev.config";
import ViteProdConfig from "./Vite.prod.config";
// 策略模式
const envResolver = {
serve: ()=>Object.assign({}, ViteBaseConfig, ViteDevConfig),
build: ()=>Object.assign({}, ViteBaseConfig, ViteProdConfig)
}
export default defineConfig(({ command, mode, ssrBuild }) => {
return envResolver[command]();
});
环境变量
Vite内置了dotenv库:支持读取 .env
文件。
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
Vite会解析.env
文件中的所有环境变量注入到node端 process.env
对象中;
解析其中Vite_
前缀的环境变量,注入到客户端的 import.meta.env
对象中。
但是 Vite.config.js执行时,默认是不加载 .env
文件的,因为这些文件需要在执行完 Vite 配置后才能确定加载哪一个,举个例子:Vite.config.js中的 envDir
选项会影响到.env的加载。
如果需要提前在Vite.config.js中获取环境变量,可以使用 Vite 的 loadEnv 函数来加载指定的 .env
文件:
export default defineConfig(({ command, mode, ssrBuild }) => {
// 根据mode加载.env文件,默认是.env.development和.env.production,可以在Vite命令后通过--mode配置
// process.cwd()指定env文件目录是当前执行node脚本所在的目录
// 第三个参数是变量前缀,设置为''会加载所有环境变量,而不管是否有 `Vite_` 前缀。
const env = loadEnv(mode, process.cwd(), '')
console.log(env);
});
路径别名
resolve:{
alias: {
'@': path.resolve(__dirname,'../', 'src')
}
}
当使用文件系统路径的别名时,要始终使用绝对路径。相对路径的别名值会原封不动地被使用,因此无法被正常解析。
css配置
配置项 css.modules 定义了CSS modules 的行为,会被传递给 postcss-modules。
css.modules.localsConvention
:配置了对css模块类名转换后,映射对象中key的写法,camelCase驼峰,dashes中划线
配置项目 css.preprocessorOptions 指定了传递给 CSS 预处理器的选项。文件扩展名用作选项的键。每个预处理器支持的选项可以在它们各自的文档中找到:
配置项 css.postcss 与postcss.config.js文件的起同等作用。
proxy代理配置
通过server.proxy
为开发服务器配置自定义代理规则。
相关配置内容继承自 http-proxy。
Vite插件
Vite插件作用是什么
Vite在不同的生命周期中取调用不同的插件,以达到不同的目的。
使用插件
可选的插件:官方插件、社区插件、部分兼容Vite的rollup插件
例子:引入并使用插件 Vite-aliases (自动根据项目目录生成 路径别名配置)
npm i Vite-aliases -D
// Vite.config.js
import { ViteAliases } from 'Vite-aliases'
export default {
plugins: [
ViteAliases()
]
};
假如项目目录是:
src
assets
components
pages
store
utils
会自动生成如下配置:
[
{
find: '@',
replacement: '${your_project_path}/src'
},
{
find: '@assets',
replacement: '${your_project_path}/src/assets'
},
{
find: '@components',
replacement: '${your_project_path}/src/components'
},
{
find: '@pages',
replacement: '${your_project_path}/src/pages'
},
{
find: '@store',
replacement: '${your_project_path}/src/store'
},
{
find: '@utils',
replacement: '${your_project_path}/src/utils'
},
]
插件原理 & 插件开发
插件开发文档: https://cn.Vitejs.dev/guide/api-plugin.html
1 插件代码结构
Vite插件应该是一个“返回实际插件对象的工厂函数”:
export default function myPlugin(config) {
return {
name: 'xx',
transform(src, id) {
// 此处transform是一个rollup和Vite通用的钩子
},
}
}
2 插件钩子
开发插件最核心的逻辑就是搞清楚:我的插件应该在哪些Vite生命周期做哪些事情。
因此要先了解Vite提供了哪些生命周期钩子:
-
通用钩子:在开发中,Vite 开发服务器会创建一个插件容器来调用 Rollup 构建钩子,与 Rollup 如出一辙:
- 在dev server启动时被调用的钩子:
options
:(options: InputOptions) =>InputOptions | null
。构建阶段第一个钩子,替换或操作传递给rollup
的选项对象,在 Rollup 配置完成之前运行。buildStart
:(options: InputOptions) =>void
。在options后被调用,可以读取到最终options值,被 rollup.rollup 方法调用,如果仅需要读取options推荐使用这个钩子。
- 在每个模块请求时被调用的钩子:
- 在dev server启动时被调用的钩子:
-
Vite 独有钩子:服务于特定的 Vite 目标。这些钩子会被 Rollup 忽略。
- config:在解析 Vite 配置前调用。接收原始用户配置,可以返回一个将被deepMerge到现有配置中的部分配置对象。
- configResolved:在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。
- configureServer:用于配置dev server的钩子,最常见的用例是给dev server添加自定义中间件,会在内部中间件被安装前调用。中间件API
server.middleware.use((req,res,next)=>{})
。 - configurePreviewServer:用于预览服务器。用上一个用法类似。
- transformIndexHtml:转换
index.html
的专用钩子。钩子接收当前的 HTML 字符串和转换上下文ctx
。ctx
在开发期间暴露ViteDevServer
实例,在构建期间暴露 Rollup 输出的包。 - handleHotUpdate:自定义 HMR 行为。
3 插件执行顺序
可以指定插件的enforce属性,来调整插件的执行顺序,enforce=pre/post。
所有插件的执行顺序如下:
- Alias
- 带有
enforce: 'pre'
的用户插件 - Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有
enforce: 'post'
的用户插件 - Vite 后置构建插件(最小化,manifest,报告)
4 区分mode执行插件
默认情况下插件在开发(serve)和构建(build)模式中都会调用。
如果插件只需要在预览或构建期间有条件地应用,使用 apply
属性指明它们仅在 'build'
或 'serve'
模式时调用
function myPlugin() {
return {
name: 'build-only',
apply: 'build' // 或 'serve'
}
}
Vite 对 TS的支持
- Vite 天然支持引入
.ts
文件。 - Vite 使用 ESBuild 将 TypeScript 转译到 JavaScript,约是
tsc
速度的 20~30 倍。 - Vite只负责转译ts代码,不负责类型检查。(Vite默认类型检查已经被你的 IDE 或构建过程处理了。)(原因是类型检查会影响Vite的速度:转译在每个文件的基础上进行,与 Vite 的按需编译模式完全吻合。相比之下,类型检查需要了解整个模块图。把类型检查塞进 Vite 的转换管道,将不可避免地损害 Vite 的速度优势。)
Vite不负责类型检查,怎么获得更多类型检查提示?
- 开发时,在项目中使用ts,并添加tsconfig.json配置,就可以依赖IDE自带的ts检查。
- 开发时,可以在一个单独的进程中运行
tsc --noEmit --watch
,可以获得更多IDE提示。 - 打包时,可以这样运行 Vite 构建命令:
tsc --noEmit && Vite build
,如果检查到类型错误,会报错并停止打包。 - 也可以使用Vite-plugin-checker插件,会将ts类型错误抛到浏览器中。
Vite性能优化
分包策略
把一些不会经常更新的依赖,单独打包:通过配置 build.rollupOptions.output.manualChunks
来自定义 chunk 分割策略:
({
manualChunks: (id) => {
if (id.includes("node_modules")) {
return "vendor";
}
},
});
或者直接使用Vite4中内置的 splitVendorChunkPlugin
插件,会自动拆分vender chunk:
// Vite.config.js
import { splitVendorChunkPlugin } from "vite";
export default defineConfig({
plugins: [splitVendorChunkPlugin()],
});
gzip压缩
使用 Vite-plugin-compression 插件,打包后会生成静态资源的压缩文件,后缀是.gz,直接提供给浏览器去解压缩。由于消耗浏览器资源,只针对大的文件进行压缩;
这种方式与nginx的gzip压缩区别在哪?nginx上的资源是以原始文件形式存在的,当返回给浏览器的时候再进行压缩,浏览器收到压缩文件再解压缩。是动态压缩,会消耗服务器资源。如果提前压缩好.gz文件,可以节省服务器压力。
动态导入
动态导入也是es原生支持的特性。
例如使用动态导入引入一个异步组件4:
import { defineAsyncComponent } from "vue";
const Bar = defineAsyncComponent(() => import("@cmp/Bar/index.vue"));
打包后,Bar组件的代码会被单独打包为一个chunk,在使用到Bar组件时浏览器会按需加载这个chunk。
Glob导入
Vite 支持使用特殊的 import.meta.glob
函数从文件系统导入多个模块:const modules =import.meta.glob('./dir/*.js')
。
匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的 chunk。
避免单屏拥有过度数目的依赖
举个例子:lodash-es是lodash为了支持构建工具tree-shaking而诞生的包,包中导出了600多个模块,如果我们在vite项目中不小心写了以下代码:
import * as lodash from 'lodash-es';
这将会导致dev server模式下,页面出现严重卡顿,因为浏览器会将这600多个子模块全部作为HTTP请求,进而导致了严重的HTTP请求并发问题。
如上动态演示,这些请求都会阻塞DOM的渲染,降低开发效率。