淘宝vite(vue vite)

发布时间:

本篇文章给大家谈谈vite,以及vue vite的知识点,希望对各位有所帮助,不要忘了收藏本站喔。

文章详情介绍:

Vite 开发环境为何这么快?

提到 Vite,第一个想到的字就是 ,到底快在哪里呢?为什么可以这么快? 本文从以下几个地方来讲

快速的冷启动: No Bundle + esbuild 预构建

模块热更新:利用浏览器缓存策略

按需加载:利用浏览器 ESM 支持

Vite 本质上是一个本地资源服务器,还有一套构建指令组成。

本地资源服务器,基于 ESM 提供很多内建功能,HMR 速度很快

使用 Rollup 打包你的代码,预配件了优化的过配置,输出高度优化的静态资源

快递的冷启动No-bundle

在冷启动开发者服务器时,基于 Webpack 这类 bundle based 打包工具,启动时必须要通过 依赖收集、模块解析、生成 chunk、生成模块依赖关系图,最后构建整个应用输出产物,才能提供服务。

这意味着不管代码实际是否用到,都是需要被扫描和解析。

而 Vite 的思路是,利用浏览器原生支持 ESM 的原理,让浏览器来负责打包程序的工作。而 Vite 只需要在浏览器请求源码时进行转换并按需提供源码即可。

这种方式就像我们编写 ES5 代码一样,不需要经过构建工具打包成产物再给浏览器解析,浏览器自己就能够解析。

与现有的打包构建工具 Webpack 等不同,Vite 的开发服务器启动过程仅包括加载配置和中间件,然后立即启动服务器,整个服务启动流程就此结束。

Vite 利用了现代浏览器支持的 ESM 特性,在开发阶段实现了 no-bundle 模式,不生成所有可能用到的产物,而是在遇到 import 语句时发起资源文件请求。

当 Vite 服务器接收到请求时,才对资源进行实时编译并将其转换为 ESM,然后返回给浏览器,从而实现按需加载项目资源。而现有的打包构建工具在启动服务器时需要进行项目代码扫描、依赖收集、模块解析、生成 chunk 等操作,最后才启动服务器并输出生成的打包产物。

正是因为 Vite 采用了 no-bundle 的开发模式,使用 Vite 的项目不会随着项目迭代变得庞大和复杂而导致启动速度变慢,始终能实现毫秒级的启动。

esbuild 预构建

当然这里的毫秒级是有前提的,需要是非首次构建,并且没有安装新的依赖,项目代码中也没有引入新的依赖。

这是因为 Vite 的 Dev 环境会进行预构建优化。 在第一次运行项目之后,直接启动服务,大大提高冷启动速度,只要没有依赖发生变化就会直接出发热更新,速度也能够达到毫秒级。

这里进行预构建主要是因为 Vite 是基于浏览器原生**支持 **ESM 的能力实现的,但要求用户的代码模块必须是ESM模块,因此必须将 commonJS 和 UMD 规范的文件提前处理,转化成 ESM 模块并缓存入 node_modules/.vite。

在转换 commonJS 依赖时,Vite 会进行智能导入分析,即使模块导出时动态分配的,具名导出也能正常工作。

ts复制代码// 符合预期 import React, { useState } from 'react'

另一方面是为了性能优化

为了提高后续页面加载的性能,Vite 将那些具有许多内部模块的 ESM 依赖转为单个模块。

比如我们常用的 lodash 工具库,里面有很多包通过单独的文件相互导入,而 lodash-es这种 ESM 包会有几百个子模块,当代码中出现 import { debounce } from 'lodash-es' 会发出几百个 HTTP 请求,这些请求会造成网络堵塞,影响页面的加载。

通过将 lodash-es 预构建成一个单独模块,只需要一个 HTTP 请求。

那么如果是首次构建呢?Vite 还能这么快吗?

在首次运行项目时,Vite 会对代码进行扫描,对使用到的依赖进行预构建,但是如果使用 rollup、webpack 进行构建同样会拖累项目构建速度,而 Vite 选择了 esbuild 进行构建。

btw,预构建只会在开发环境生效,并使用 esbuild 进行 esm 转换,在生产环境仍然会使用 rollup 进行打包。

生产环境使用 rollup 主要是为了更好的兼容性和 tree-shaking 以及代码压缩优化等,以减小代码包体积

为什么选择 esbuild?

esbuild 的构建速度非常快,比 Webpack 快非常多,esbuild 是用 Go 编写的,语言层面的压制,运行性能更好

核心原因就是 esbuild 足够快,可以在 esbuild 官网看到这个对比图,基本上是 上百倍的差距。

前端的打包工具大多数是基于 JavaScript 实现的,由于语言特性 JavaScript 边运行边解释,而 esbuild 使用 Go 语言开发,直接编译成机器语言,启动时直接运行即可。

更多关于 Go 和 JavaScript 的语言特性差异,可以检索一下。

不久前,字节开源了 Rspack 构建工具,它是基于 Rust 编写的,同样构建速度很快

Rust 编译生成的 Native Code 通常比 JavaScript 性能更为高效,也意味着 rspack 在打包和构建中会有更高的性能。

同时 Rust 支持多线程,意味着可以充分利用多核 CPU 的性能进行编译。而 Webpack 受限于 JavaScript 对多线程支持较弱,导致很难进行并行计算。

不过,Rspack 的插件系统还不完善,同时由于插件支持 JS 和 rust 编写,如果采用 JS 编写估计会损失部分性能,而使用 rust 开发,对于开发者可能需要一定的上手成本

同时发现 Vite 4 已经开始增加对 SWC 的支持,这是一个基于 Rust 的打包器,可以替代 Babel,以获取更高的编译性能。

**Rust 会是 JavaScript 基建的未来吗?**推荐阅读:
zhuanlan.zhihu.com/p/433300816

模块热更新

主要是通过 WebSocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。

Webpack 和 Vite 在热更新上有什么不同呢?

Webpack: 重新编译,请求变更后模块的代码,客户端重新加载

Vite 通过监听文件系统的变更,只对发生变更的模块重新加载,只需要让相关模块的 boundary 失效即可,这样 HMR 更新速度不会因为应用体积增加而变慢,但 Webpack 需要经历一次打包构建流程,所以 HMR Vite 表现会好于 Webpack。

核心流程

Vite 热更新流程可以分为以下:

    创建一个 websocket 服务端和client文件,启动服务

    监听文件变更

    当代码变更后,服务端进行判断并推送到客户端

    客户端根据推送的信息执行不同操作的更新

创建 WebSocket 服务

在 dev server 启动之前,Vite 会创建websocket服务,利用chokidar创建一个监听对象 watcher 用于对文件修改进行监听等等,具体核心代码在 node/server/index 下

createWebSocketServer 就是创建 websocket 服务,并封装内置的 close、on、send 等方法,用于服务端推送信息和关闭服务

源码地址:
packages/vite/src/node/server/ws.ts

执行热更新

当接受到文件变更时,会执行 change 回调

scss复制代码watcher.on('change', async (file) => { file = normalizePath(file) // invalidate module graph cache on file change moduleGraph.onFileChange(file) await onHMRUpdate(file, false) })

当文件发生更改时,这个回调函数会被触发。file 参数表示发生更改的文件路径。

首先会通过 normalizePath 将文件路径标准化,确保文件路径在不同操作系统和环境中保持一致。

然后会触发 moduleGraph 实例上的 onFailChange 方法,用来清空被修改文件对应的 ModuleNode 对象的 transformResult 属性,**使之前的模块已有的转换缓存失效。**这块在下一部分会讲到。

ModuleNode 是 Vite 最小模块单元

moduleGraph 是整个应用的模块依赖关系图

源码地址:
packages/vite/src/node/server/moduleGraph.ts

ts复制代码onFileChange(file: string): void { const mods = this.getModulesByFile(file) if (mods) { const seen = new Set() mods.forEach((mod) => { this.invalidateModule(mod, seen) }) } } invalidateModule( mod: ModuleNode, seen: Set = new Set(), timestamp: number = Date.now(), isHmr: boolean = false, hmrBoundaries: ModuleNode[] = [], ): void { ... // 删除平行编译结果 mod.transformResult = null mod.ssrTransformResult = null mod.ssrModule = null mod.ssrError = null ... mod.importers.forEach((importer) => { if (!importer.acceptedHmrDeps.has(mod)) { this.invalidateModule(importer, seen, timestamp, isHmr) } }) }

可能会有疑惑,Vite 在开发阶段不是不会打包整个项目吗?怎么生成模块依赖关系图

确实是这样,Vite 不会打包整个项目,但是仍然需要构建模块依赖关系图,当浏览器请求一个模块时

Vite 首先会将请求的模块转换成原生 ES 模块

分析模块依赖关系,也就是 import 语句的解析

将模块及依赖关系添加到 moduleGraph 中

返回编译后的模块给浏览器

因此 Vite 的 Dev 阶段时动态构建和更新模块依赖关系图的,无需打包整个项目,这也实现了真正的按需加载。

handleHMRUpdate

在 chokidar change 的回调中,还执行了 onHMRUpdate 方法,这个方法会调用执行 handleHMRUpdate 方法

在 handleHMRUpdate 中主要会分析文件更改,确定哪些模块需要更新,然后将更新发送给浏览器。

浏览器端的 HMR 运行时会接收到更新,并在不刷新页面的情况下替换已更新的模块。

源码地址:
packages/vite/src/node/server/hmr.ts

ts复制代码export async function handleHMRUpdate( file: string, server: ViteDevServer, configOnly: boolean, ): Promise { const { ws, config, moduleGraph } = server // 获取相对路径 const shortFile = getShortName(file, config.root) const fileName = path.basename(file) // 是否配置文件修改 const isConfig = file === config.configFile // 是否自定义插件 const isConfigDependency = config.configFileDependencies.some( (name) => file === name, ) // 环境变量文件 const isEnv = config.inlineConfig.envFile !== false && (fileName === '.env' || fileName.startsWith('.env.')) if (isConfig || isConfigDependency || isEnv) { // auto restart server ... try { await server.restart() } catch (e) { config.logger.error(colors.red(e)) } return } ... // 如果是 Vite 客户端代码发生更改,强刷 if (file.startsWith(normalizedClientDir)) { // ws full-reload return } // 获取到文件对应的 ModuleNode const mods = moduleGraph.getModulesByFile(file) ... // 调用所有定义了 handleHotUpdate hook 的插件 for (const hook of config.getSortedPluginHooks('handleHotUpdate')) { const filteredModules = await hook(hmrContext) ... } // 如果是 html 文件变更,重新加载页面 if (!hmrContext.modules.length) { // html file cannot be hot updated if (file.endsWith('.html')) { // full-reload } return } updateModules(shortFile, hmrContext.modules, timestamp, server) }

配置文件更新、.env更新、自定义插件更新都会重新启动服务 reload server

Vite 客户端代码更新、index.html 更新,重新加载页面

调用所有 plugin 定义的 handleHotUpdate 钩子函数

过滤和缩小受影响的模块列表,使 HMR 更准确。

返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理

插件处理更新 hmrContext 上的 modules

如果是其他情况更新,调用 updateModules 函数

流程图如下

在 updateModules 中主要是对模块进行处理,生成 updates 更新列表,ws.send 发送 updates 给客户端

ws 客户端响应

客户端在收到服务端发送的 ws.send 信息后,会进行相应的响应

当接收到服务端推送的消息,通过不同的消息类型做相应的处理,比如 update、connect、full-reload 等,使用最频繁的是 update(动态加载热更新模块)和 full-reload (刷新整个页面)事件。

源码地址:
packages/vite/src/client/client.ts

在 update 的流程里,会使用 Promise.all 来异步加载模块,如果是 js-update,及 js 模块的更新,会使用 fetchUpdate 来加载

ts复制代码if (update.type === 'js-update') { return queueUpdate(fetchUpdate(update)) }

fetchUpdate 会通过动态 import 语法进行模块引入

浏览器缓存优化

Vite 还利用 HTTP 加速整个页面的重新加载。 对预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存,以提高开发期间页面重新加载的性能。一旦被缓存,这些请求将永远不会再次访问开发服务器。

这部分的实现在 transformMiddleware 函数中,通过中间件的方式注入到 Koa dev server 中。

源码地址:
packages/vite/src/node/server/middlewares/transform.ts

若需要对依赖代码模块做改动可手动操作使缓存失效:

ts复制代码vite --force

或者手动删除 node_modules/.vite 中的缓存文件。

总结

Vite 采用 No Bundle 和 esbuild 预构建,速度远快于 Webpack,实现快速的冷启动,在 dev 模式基于 ES module,实现按需加载,动态 import,动态构建 Module Graph。

在 HMR 上,Vite 利用 HTTP 头 cacheControl 设置 max-age 应用强缓存,加速整个页面的加载。

当然 Vite 还有很多的不足,比如对 splitChunks 的支持、构建生态 loader、plugins 等都弱于 Webpack。不过 Vite 仍然是一个非常好的构建工具选择。在不少应用中,会使用 Vite 来进行开发环境的构建,采用 Webpack5 或者其他 bundle base 的工具构建生产环境。

原文链接:
https://juejin.cn/post/72567825