Vite简介

1.1、什么是Vite

   Vite是一种新型的前端构建工具,它能显著改善前端开发体验。
Vite由两个主要部分组成:

dev server:利用浏览器的ESM能力来提供源文件,具有丰富的内置功能并具有高效的HMR
生产构建:生产环境利用Rollup来构建代码,提供指令用来优化构建过程
Vite作为一个基于浏览器原生ESM的构建工具,它省略了开发环境的打包过程,利用浏览器去解析imports,在服务端按需编译返回。同时,在开发环境拥有速度快到惊人的模块热更新,且热更新的速度不会随着模块增多而变慢。因此,使用Vite进行开发,至少会比Webpack快10倍左右。

1.2 、Vite的主要特性

Instant Server Start —— 即时服务启动
Lightning Fast HMR —— 闪电般快速的热更新
Rich Features —— 丰富的功能
Optimized Build —— 经过优化的构建
Universal Plugin Interface —— 通用的Plugin接口
Fully Typed APIs —— 类型齐全的API
开发环境速度的提升
相比Webpack需要对entry、loader、plugin等进行诸多配置,Vite的使用可谓是相当简单了。只需执行初始化命令,就可以得到一个预设好的开发环境,开箱即获得一堆功能,包括:CSS预处理、html预处理、异步加载、分包、压缩、HMR等。他使用复杂度介于Parcel和Webpack的中间,只是暴露了极少数的配置项和plugin接口,既不会像Parcel一样配置不灵活,又不会像Webpack一样需要了解庞大的loader、plugin生态,灵活适中、复杂度适中。适合前端新手。

1.3Vite 开发环境 VS 生产环境

开发环境不需要对所有资源打包,只是使用esbuild对依赖进行预构建,将CommonJS和UMD发布的依赖转换为浏览器支持的ESM,同时提高了后续页面的加载性能(lodash的请求)。Vite会将于构建的依赖缓存到node_modules/.vite目录下,它会根据几个源来决定是否需要重新运行预构建,包括 packages.json中的dependencies列表、包管理器的lockfile、可能在vite.config.js相关字段中配置过的。只要三者之一发生改变,才会重新预构建。

同时,开发环境使用了浏览器缓存技术,解析后的依赖请求以http头的max-age=31536000,immutable强缓存,以提高页面性能。

在生产环境,由于嵌套导入会导致发送大量的网络请求,即使使用HTTP2.x(多路复用、首部压缩),在生产环境中发布未打包的ESM仍然性能低下。因此,对比在开发环境Vite使用esbuild来构建依赖,生产环境Vite则使用了更加成熟的Rollup来完成整个打包过程。因为esbuild虽然快,但针对应用级别的代码分割、CSS处理仍然不够稳定,同时也未能兼容一些未提供ESM的SDK。

为了在生产环境中获得最佳的加载性能,仍然需要对代码进行tree-shaking、懒加载以及chunk分割(以获得更好的缓存)。

二、Vite原理

2.1 、ESM&esbuild

在ES6没有出现之前,随着js代码日益膨胀,往往会对资源模块化来提效,这也就出现了多个模块化方案。如CommonJS常用于服务端,AMD、CMD规范常用在客户端。ES6出现后,紧接着出现了ESM。ESM是浏览器支持的一种模块化方案,允许在浏览器实现模块化。

CommonJS:模块同步,如Browserify会对代码进行解析,整理出代码中的所有模块依赖关系,然后把nodejs的模块编译成浏览器可用的模块,相关的模块代码都打包在一起,形成一个完整的JS文件,这个文件中不会存在 require 这类的模块化语法,变成可以在浏览器中运行的普通JS,运行时加载
AMD:模块异步,依赖前置,是requireJS在推广过程中对模块定义的规范化产出,加载完依赖后立即执行依赖模块,依赖加载成功后执行回调
CMD:模块异步,延迟执行,是seaJS在推广过程中对模块定义的规范化产出,就近依赖,先加载所有依赖模块,运行时才执行require内容,按顺序执行
与CommonJS、AMD不同,ESM的对外接口只是一种静态定义,为编译时加载,遇到模块加载命令import,就会生成一个只读引用。等脚本真正执行时,再根据这个只读引用,到被加载的那个模块内取值。由于ESM编译时就能确定模块的依赖关系,因此能够只包含要运行的代码,可以显著减少文件体积,降低浏览器压力。

由于ESM是一个比较新的模块化方案,目前其浏览器能力支持如下:

在这里插入图片描述

可以看到,除了IE、Opera等,新一代浏览器中绝大部分都已支持。
接下来以Vite创建的模板为例,看一下ESM的解析过程:

Vue logo

当浏览器解析 import HelloWorld from ‘./components/HelloWorld.vue’ 时,会向当前域名发送一个请求获取对应的资源(ESM支持解析相对路径)。
在这里插入图片描述

浏览器下载对应的文件,然后解析成模块记录。接下来会进行实例化,为模块分配内存,然后按照导入、导出语句建立模块和内存的映射关系。最后,运行上述代码,把内存空间填充为真实的值。

esbuild

Vite 对 js/ts 的处理没有使用如 glup, rollup 等传统打包工具,而是使用了 esbuild。esbuild 是一个全新的js打包工具,底层使用了go,大量使用了并行操作,可以充分利用CPU资源。esbuild支持如babel, 压缩等的功能。

对比各打包工具性能,可以看到esbuild比rollup等工具快十几倍。
在这里插入图片描述

2.2、请求拦截

Vite 的基本实现原理,就是启动一个 koa 服务器拦截由浏览器请求 ESM的请求。通过请求的路径找到目录下对应的文件做一定的处理最终以 ESM的格式返回给客户端。
在这里插入图片描述

2.2.1、依赖处理

Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。

依赖解析
以 Vite 官方 demo 为例,当我们请求 localhost:3000 时,Vite 默认返回 localhost:3000/index.html 的代码。而后发送请求 src/main.js。

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 模块。

平时开发中,webpack & rollup(rollup有对应插件) 等打包工具会帮我们找到模块的路径,但浏览器只能通过相对路径去寻找,而如果是直接使用模块名比如:import vue from ‘vue’,浏览器就会报错,这个时候就需要一个三方包进行处理。Vite 对ESM形式的 js 文件模块使用了 ES Module Lexer 处理。Lexer 会找到代码中以 import 语法导入的模块并以数组形式返回。Vite 通过该数组的值获取判断是否为一个 node_modules 模块。若是则进行对应改写成 @modules/:id 的写法。

重写完路径后,浏览器会发送 path 为 /@modules/:id 的对应请求,接下来会被 Vite 客户端做一层拦截来解析模块的真实位置。

首先正则匹配请求路径,如果是/@modules开头就进行后续处理,否则就跳过。若是,会设置响应类型为js,读取真实模块路径内容,返回给客户端。

客户端注入本质上是创建一个script标签(type=‘module’),然后将其插入到head中,这样客户端在解析html是就可以执行代码了

export const moduleRE = /^/@modules//
// plugin for resolving /@modules/:id requests.
app.use(async (ctx, next) => {
if (!moduleRE.test(ctx.path)) {
return next()
}
// path maybe contain encode chars
const id = decodeURIComponent(ctx.path.replace(moduleRE, ‘’))
ctx.type = ‘js’
const serve = async (id: string, file: string, type: string) => {
// 在代码中做一个缓存,下次访问相同路径直接从 map 中获取 304 返回
moduleIdToFileMap.set(id, file)
moduleFileToIdMap.set(file, ctx.path)
debug((${type}) ${id} -> ${getDebugPath(root, file)})
await ctx.read(file)
return next()
}
}
// 兼容 alias 情况
const importerFilePath = importer ? resolver.requestToFile(importer) : root
const nodeModulePath = resolveNodeModuleFile(importerFilePath, id)
// 如果是个 node_modules 的模块,读取文件。
if (nodeModulePath) {
return serve(id, nodeModulePath, ‘node_modules’)
}
})
依赖预构建
依赖预构建主要有两个目的:

CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
Vite使用esbuild在初次启动开发服务器前把检测到的依赖进行预构建。Vite 基于ESM,在使用某些模块时,由于模块依赖了另一些模块,依赖的模块又基于另一些模块。会出现页面初始化时一次发送数百个模块请求的情况。

以 lodash-es 为例,代码中以 import { debounce } from ‘lodash’ 导入一个命名函数时候,并不是只下载包含这个函数的文件,而是有一个依赖图。
在这里插入图片描述

可以看到一共发送了651个请求。一共花费1.53s。

Vite 为了优化这个情况,利用esbuild在启动的时候预先把debounce用到的所有内部模块全部打包成一个bundle,这样就浏览器在请求debounce时,便只需要发送一次请求了
在这里插入图片描述

可以看到预构建后,只发送了14个请求。

2.2.2、静态资源加载

当请求的路径符合 imageRE, mediaRE, fontsRE 或 JSON 格式,会被认为是一个静态资源。静态资源将处理成ESM模块返回。

// src/node/utils/pathUtils.ts
const imageRE = /.(png|jpe?g|gif|svg|ico|webp)(?.)?KaTeX parse error: Undefined control sequence: \? at position 54: …|wav|flac|aac)(\̲?̲.*)?/
const fontsRE = /.(woff2?|eot|ttf|otf)(?.
)?$/i
export const isStaticAsset = (file: string) => {
return imageRE.test(file) || mediaRE.test(file) || fontsRE.test(file)
}

// src/node/server/serverPluginAssets.ts
app.use(async (ctx, next) => {
if (isStaticAsset(ctx.path) && isImportRequest(ctx)) {
ctx.type = ‘js’
ctx.body = export default ${JSON.stringify(ctx.path)} // 输出是path
return
}
return next()
})

export const jsonPlugin: ServerPlugin = ({ app }) => {
app.use(async (ctx, next) => {
await next()
// handle .json imports
// note ctx.body could be null if upstream set status to 304
if (ctx.path.endsWith(‘.json’) && isImportRequest(ctx) && ctx.body) {
ctx.type = ‘js’
ctx.body = dataToEsm(JSON.parse((await readBody(ctx.body))!), {
namedExports: true,
preferConst: true
})
}
})
}
2.2.3、vue文件缓存

当 Vite 遇到一个 .vue 后缀的文件时。由于 .vue 模板文件的特殊性,它被拆分成 template, css, script 模块三个模块进行分别处理。最后会对 script, template, css 发送多个请求获取
在这里插入图片描述

如上图中请求 App.vue 获取script 代码 , App.vue?type=template 获取 template, App.vue?type=style。这些代码都被插入在 App.vue 返回的代码中。

2.2.4、 js/ts处理
在这里插入图片描述

Vite使用esbuild将ts转译到js,约是tsc速度的20~30倍,同时HMR更新反应到浏览器的时间会小于50ms。但是,由于esbuild转换ts到js对于类型操作仅仅是擦除,所以完全保证不了类型正确,因此需要额外校验类型,比如使用tsc --noEmit。

将ts转换成js后,浏览器便可以利用ESM直接拿到js资源。

2.3、 热更新原理

Vite 的热加载原理,其实就是在客户端与服务端建立了一个 websocket 连接,当代码被修改时,服务端发送消息通知客户端去请求修改模块的代码,完成热更新。

服务端:服务端做的就是监听代码文件的改变,在合适的时机向客户端发送 websocket 信息通知客户端去请求新的模块代码。
客户端:Vite 中客户端的 websocket 相关代码在处理 html 中时被写入代码中。可以看到在处理 html 时,vite/client 的相关代码已经被插入。
export const clientPublicPath = /vite/client
const devInjectionCode = \n<script type="module">import "${clientPublicPath}"</script>\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()
}
}
}

三、问题

1、构建工具和打包工具的区别?

构建过程应该包括 预编译、语法检查、词法检查、依赖处理、文件合并、文件压缩、单元测试、版本管理等 。打包工具更注重打包这一过程,主要包括依赖管理和版本管理。
2、Vite有什么缺点?

目前 Vite 还是使用的 es module 模块不能直接使用生产环境(兼容性问题)。默认情况下,无论是 dev 还是 build 都会直接打出 ESM 版本的代码包,这就要求客户浏览器需要有一个比较新的版本,这放在现在的国情下还是有点难度的。不过 Vite 同时提供了一些弥补的方法,使用 build.polyfillDynamicImport 配置项配合 @vitejs/plugin-legacy 打包出一个看起来兼容性比较好的版本。
生产环境使用 rollup 打包会造成开发环境与生产环境的不一致。
很多 第三方 sdk 没有产出 ems 格式的的代码,这个需要自己去做一些兼容。
3、Vite生产环境用了Rollup,那能在生产环境中直接使用 esm 吗?

其实目前的主要问题可能还是兼容性问题。
如果你的项目不需要兼容 IE11 等低版本的浏览器,自然是可以使用的。
但是更通用的方案可能还是类似 ployfill.io 的原理实现, 提前构建好 bundle.js 与 es module 两个版本的代码,根据浏览器的实际兼容性去动态选择导入哪个模块。
4、对于一些 没有产出 commonjs 的模块,如何去兼容呢?

首先业界是有一些如 lebab 的方法可以将 commjs 代码快速转化为 esm 的,但是对于一些格式不规范的代码,可能还是需要单独处理。

5、如果组件嵌套层级比较深,会影响速度吗?

可以看到请求 lodash 时 651 个请求只耗时 1.53s。这个耗时是完全可以接受的。
Vite 是完全按需加载的,在页面初始化时只会请求初始化页面的一些组件,也就是说即使层级深,但如果未展示可以不加载。
缓存可以降低耗时

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值