Vite是一种新型的前端构建工具,它能显著改善前端开发体验。
Vite由两个主要部分组成:
一个开发服务器,它基于 原生 ES 模块(ESM
),提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR
)。
ESM
:ESM是浏览器支持的一种模块化方案,允许在浏览器实现模块化。ES6之前,JS一直没有自己的模块体系。但是ES6引入了ESM,到此,JS终于有了自己的模块体系。
HMR
:全称 Hot Module Replacement,模块热更新,它能够在保持页面状态的情况下动态替换资源模块,提供丝滑顺畅的 Web 页面开发体验。
一套构建指令,它使用 Rollup
打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源(打包后的体积更小)。
Rollup
:基于ES6的模块打包工具,打包文件小且干净,执行效率更高,更专注于JS打包,同时支持 Tree-shaking
。
Tree-shaking
:前端中的 tree-shaking
可以理解为通过工具"摇"我们的 JS 文件,将其中用不到的代码"摇"掉,是一个性能优化
的范畴。具体来说,在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking
,将没有使用的模块 code
摇掉,这样来达到删除无用代码的目的。
Vite作为一个基于浏览器原生ESM的构建工具,它省略了开发环境的打包过程,利用浏览器去解析imports,在服务端按需编译返回。同时,在开发环境拥有速度快到惊人的模块热更新,且热更新的速度不会随着模块增多而变慢。因此,使用Vite进行开发,至少会比Webpack快10倍
左右。
Webpack
。那么Vite
是如何解决如Webpack
这样的构建工具一样,在复杂、多模块项目开发中启动慢、HMR慢的问题呢?Vite
和Webpack
,发现主要有如下不同:Webpack | Vite |
---|---|
先打包生成bundle(经过Webpack打包后的静态资源文件),再启动开发服务器 | 先启动开发服务器,利用新一代浏览器的ESM能力,无需打包,直接请求所需模块并实时编译 |
HMR时需要把改动模块及相关依赖全部编译 | HMR时只需让浏览器重新请求该模块,同时利用浏览器的缓存(源码模块协商缓存,依赖模块强缓存)来优化请求 |
因此,针对开发环境中的启动慢问题,Vite
开发环境冷启动无需打包,无需分析模块之间的依赖,同时也无需在启动开发服务器前进行编译,启动时还会使用 esbuild预构建依赖
来进行预构建。
而Webpack
启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,这可满满都是 CPU、IO 操作啊,在 Node 运行时下性能必然是有问题。
针对HMR慢,即使只有很小的改动,Webpack
依然需要构建完整的模块依赖图,并根据依赖图来进行转换。而Vite
利用了ESM
和浏览器缓存
技术,更新速度与项目复杂度无关。可以看到,Vite构建工具,在开发环境启动时只需要启动两个Server,一个用于页面加载,一个用于HMR的Websocket。当浏览器发出原生的ESM请求,Server收到请求只需要编译当前文件后返回给浏览器,不需要管理依赖。
Webpack
需要对entry、loader、plugin等进行诸多配置,Vite
的使用可谓是相当简单了。只需执行初始化命令,就可以得到一个预设好的开发环境,开箱即获得一堆功能,包括:CSS预处理、html预处理、异步加载、分包、压缩、HMR等。他只是暴露了极少数的配置项和plugin接口,不会像Webpack一样需要了解庞大的loader、plugin生态,灵活适中、复杂度适中。适合前端新手。Vite 的基本实现原理,就是启动一个 koa 服务器拦截由浏览器请求 ESM的请求。通过请求的路径找到目录下对应的文件做一定的处理最终以 ESM的格式返回给客户端。
Vite 通过在一开始将应用中的模块区分为 依赖
和 源码
两类,改进了开发服务器启动时间。依赖 大多为在开发时不会变动的纯 JavaScript。
localhost:3000
时,Vite 默认返回 localhost:3000/index.html
的代码。而后发送请求 src/main.js
。import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
可以观察到浏览器请求 vue.js 时, 请求路径是 @modules/vue.js
。在 Vite 中约定若 path 的请求路径满足 /^\/@modules\//
格式时,被认为是一个 node_modules
模块。
首先正则匹配请求路径,如果是/@modules
开头就进行后续处理,否则就跳过。若是,会设置响应类型为js,读取真实模块路径内容,返回给客户端(即浏览器)。
客户端注入本质上是创建一个script标签(type=‘module’),然后将其插入到head中,这样客户端在解析html是就可以执行代码了。
依赖预构建
Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
Vite使用esbuild在初次启动开发服务器前把检测到的依赖进行预构建。Vite 基于ESM,在使用某些模块时,由于模块依赖了另一些模块,依赖的模块又基于另一些模块。会出现页面初始化时一次发送数百个模块请求的情况。
以 lodash-es
为例,代码中以 import { debounce } from 'lodash'
导入一个命名函数时候,并不是只下载包含这个函数的文件,而是有一个依赖图。
可以看到一共发送了651个请求。一共花费1.53s。
Vite 为了优化这个情况,利用esbuild在启动的时候预先把debounce用到的所有内部模块全部打包成一个bundle,这样就浏览器在请求debounce时,便只需要发送一次请求了。
可以看到预构建后,lodash-es
只发送了1个请求。
当请求的路径符合 imageRE, mediaRE, fontsRE 或 JSON 格式,会被认为是一个静态资源。静态资源将处理成ESM模块返回。
const imageRE = /\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/
const mediaRE = /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/
const fontsRE = /\.(woff2?|eot|ttf|otf)(\?.*)?$/i
当 Vite 遇到一个 .vue 后缀的文件时。由于 .vue 模板文件的特殊性,它被拆分成 template, css, script
模块三个模块进行分别处理。最后会对 script, template, css
发送多个请求获取。
如上图中请求 App.vue 获取script 代码 , App.vue?type=template 获取 template, App.vue?type=style。这些代码都被插入在 App.vue 返回的代码中。
Vite使用esbuild将ts转译到js,约是tsc速度的20~30倍,同时HMR更新反应到浏览器的时间会小于50ms。但是,由于esbuild转换ts到js对于类型操作仅仅是擦除,所以完全保证不了类型正确,因此需要额外校验类型,比如使用tsc --noEmit
。
将ts转换成js后,浏览器便可以利用ESM直接拿到js资源。
Vite 的热更新原理,其实就是在客户端与服务端建立了一个 websocket 连接,当代码被修改时,服务端发送消息通知客户端去请求修改模块的代码,完成热更新。
export const clientPublicPath = `/vite/client`
const devInjectionCode = `\n\n`
async function rewriteHtml(importer: string, html: string) {
return injectScriptToHtml(html, devInjectionCode)
}
当request.path 路径是 /vite/client 时,请求获取已经提前写好的关于 websocket 的代码。因此在客户端中我们创建了一个 websocket 服务并与服务端建立了连接。
Vite 会接受到来自客户端的消息。通过不同的消息触发一些事件。做到浏览器端的即时热模块更换(热更新)。包括 connect、vue-reload、vue-rerender 等事件,分别触发组件vue 的重新加载,render等。
// Listen for messages
socket.addEventListener('message', async ({ data }) => {
const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
if (payload.type === 'multi') {
payload.updates.forEach(handleMessage)
} else {
handleMessage(payload)
}
})
async function handleMessage(payload: HMRPayload) {
const { path, changeSrcPath, timestamp } = payload as UpdatePayload
console.log(path)
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
break
case 'vue-reload':
queueUpdate(
import(`${path}?t=${timestamp}`)
.catch((err) => warnFailedFetch(err, path))
.then((m) => () => {
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite] ${path} reloaded.`)
})
)
break
case 'vue-rerender':
const templatePath = `${path}?type=template`
import(`${templatePath}&t=${timestamp}`).then((m) => {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite] ${path} template updated.`)
})
break
case 'style-update':
// check if this is referenced in html via
const el = document.querySelector(`link[href*='${path}']`)
if (el) {
el.setAttribute(
'href',
`${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
)
break
}
const importQuery = path.includes('?') ? '&import' : '?import'
await import(`${path}${importQuery}&t=${timestamp}`)
console.log(`[vite] ${path} updated.`)
break
case 'js-update':
queueUpdate(updateModule(path, changeSrcPath, timestamp))
break
case 'custom':
const cbs = customUpdateMap.get(payload.id)
if (cbs) {
cbs.forEach((cb) => cb(payload.customData))
}
break
case 'full-reload':
if (path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
// currently on that page.
const pagePath = location.pathname
if (
pagePath === path ||
(pagePath.endsWith('/') && pagePath + 'index.html' === path)
) {
location.reload()
}
return
} else {
location.reload()
}
}
}
兼容性注意
Vite 需要 Node.js 版本 >= 12.0.0。
$ npm init vite@latest
# npm 7+, 需要额外的双横线:
npm init vite@latest my-vue-app -- --template vue
# vite+ts
npm init vite@latest my-vue-app -- --template vue-ts
Vite 官方对 Vue 的支持只针对于Vue3.x 版本,而对于 Vue2.x 是不支持的。使 Vite 支持 Vue2.x,是需要安装插件来实现。
https://www.vitejs.net