深入理解Vite核心原理

2022-12-05 0 836

1 Vite如是说

1.1 Vite是什么?

Vite是第三代的后端构筑辅助工具,在尤雨溪合作开发Vue3.0的这时候问世。近似于Webpack+ Webpack-dev-server。其主要借助应用程序ESM优点引入组织标识符,在伺服器端按需校对回到,完全埃唐佩县了装箱这个概念,伺服器随起进料。生产中借助Rollup作为装箱辅助工具,袖珍第三代的后端构筑辅助工具。

Vite有如下表所示特点:

快速的UAC: No Bundle + esbuild 预构筑即刻的组件热预览: 如前所述ESM的HMR,与此同时借助应用程序内存思路提高速率或者说的按需读取: 借助应用程序ESM支持,实现或者说的按需读取

1.2 Vite和传统装箱形式的对照

深入理解Vite核心原理

1.2.1 VS Webapck

Webpack是近几年采用量最大,与此同时街道社区最健全的后端装箱构筑辅助工具,Chavanges的5.x版对构筑技术细节展开了强化,在部分情景下装箱速率提高明显。Webpack在开启时,会先构筑工程项目组件的倚赖图,如果在工程项目中的某一地方更动了标识符,Webpack则会对相关的倚赖重新装箱,随着工程项目的减小,其装箱速率也会下降。

Vite相比于Webpack来说,没装箱的操作过程,而是间接开启了一个合作开发伺服器devServer。Vite挟持应用程序的HTTP允诺,在后端展开适当的处理将工程项目中采用的文档透过简单的降解与资源整合,然后再回到给应用程序(整座操作过程没对文档展开装箱校对)。所以校对速率很快。

1.2.2 VS SnowPack

Snowpack 首次提出借助应用程序原生植物ESM能力的装箱辅助工具,其经营理念就是减少或防止整座bundle的装箱。预设在 dev 和 production 环境都采用 unbundle 的形式来布署应用。但是它的构筑时却是交予采用者自己选择,整体的装箱新体验变得有点千疮百孔。

而 Vite 间接资源整合了 Rollup,为采用者提供了健全、照相狸尾豆的软件系统,并且由于这些软件系统,也方便快捷扩充更多的高阶功能。

二者较大的差别是在需要bundle装箱的这时候Vite 采用 Rollup 内建实用性,而 Snowpack 透过其他应用程序将其委派给 Parcel/“webpack。

2 后置科学知识

2.1 ESM

在了解Vite之前,需要先了解下ESM

ESM是JavaScript提出的官方标准化组件系统,不同于之前的CJS,AMD,CMD等等,ESM提供了更原生植物以及更动态的组件读取方案,最重要的就是它是应用程序原生植物支持的,也就是说我们可以间接在应用程序中去执行import,动态引入我们需要的组件,而不是把所有组件装箱在一起。

目前ESM组件化已经支持92%以上的应用程序,而且且作为 ECMA 标准,未来会有更多应用程序支持ECMA规范

深入理解Vite核心原理

当我们在采用组件合作开发时,其实就是在构筑一张组件倚赖关系图,当组件读取时,就会从入口文档开始,最终生成完整的组件实例图。

ESM的执行可以分为三个步骤:

构筑: 确定从哪里下载该组件文档、下载并将所有的文件解析为组件记录实例化: 将组件记录转换为一个组件实例,为所有的组件分配内存空间,依照导出、引入语句把组件指向对应的内存地址。运行:运行标识符,将内存空间填充

从上面实例化的操作过程可以看出,ESM采用实时绑定的模式,导出和引入的组件都指向相同的内存地址,也就是值引用。而CJS采用的是值拷贝,即所有导出值都是拷贝值。

2.2 Esbuild

Vite底层采用Esbuild实现对.“ts、jsx、.“js代码文档的转化,所以先看下什么是es-build。

Esbuild是一个JavaScript“ Bundler 装箱和压缩辅助工具,它提供了与Webpack、Rollup等辅助工具相似的资源装箱能力。可以将JavaScript 和TypeScript标识符装箱分发在网页上运行。但其装箱速率却是其他辅助工具的10~100倍。

目前他支持以下的功能:

读取器压缩装箱Tree shakingSource map生成

esbuild总共提供了四个函数:transform、build、buildSync、Service。有兴趣的可以移步官方文档了解。

2.3 Rollup

在生产环境下,Vite采用Rollup来展开装箱

Rollup是如前所述ESM的JavaScript装箱辅助工具。相比于其他装箱辅助工具如Webpack,他总是能打出更小、更快的包。因为 Rollup 如前所述 ESM 组件,比 Webpack 和 Browserify 使用的 CommonJS组件机制更高效。Rollup的亮点在于同一个地方,一次性读取。能针对源码展开 Tree Shaking(去除那些已被定义但没被采用的标识符),以及 Scope Hoisting 以减小输出文档大小提高运行性能。

Rollup分为build(构筑)阶段和output generate(输出生成)阶段。主要操作过程如下表所示:

语法树展开倚赖解析生成最终标识符写入目标文档

如果你的工程项目(特别是类库)只有JavaScript,而没其他的静态资源文档,采用Webpack就有点大才小用了。因为Webpack 装箱的文档的体积略大,运行略慢,可读性略低。这这时候Rollup也不失为一个好选择。

这里想对Rollp展开更深入细致的学习可以看看官网的如是说

3 核心理念基本原理

详细阐述下:

当声明一个 script标签类型为 module 时,如
<script type=”module” src=”/src/main.js”></script>
当应用程序解析资源时,会往当前域名发起一个GET允诺main.js文档
// main.js import { createApp } from vue import App from ./App.vue createApp(App).mount(#app)
允诺到

Vite其核心理念基本原理是借助应用程序现在已经支持ES6的import,碰见import就会发送一个HTTP允诺去读取文档,Vite开启一个 koa 伺服器拦截这些允诺,并在后端展开适当的处理将工程项目中采用的文档透过简单的降解与资源整合,然后再以ESM格式回到回到给浏览器。Vite整座操作过程中没对文档展开装箱校对,做到了或者说的按需读取,所以其运行速率比原始的webpack合作开发校对速率快出许多!

3.1 如前所述ESM的Dev server

在Vite出来之前,传统的装箱工具如Webpack是先解析倚赖、装箱构筑再开启合作开发伺服器,Dev Server 必须等待所有组件构筑完成,当我们修改了 bundle组件中的一个子组件, 整座 bundle 文档都会重新装箱然后输出。工程项目应用越大,开启时间越长。

深入理解Vite核心原理

而Vite借助应用程序对ESM的支持,当 import 组件时,应用程序就会下载被引入的组件。先开启合作开发伺服器,当标识符执行到组件读取时再允诺对应组件的文档,本质上实现了动态读取。灰色部分是暂时没用到的路由,所有这部分不会参与构筑操作过程。随着工程项目里的应用越来越多,增加route,也不会影响其构筑速率。

深入理解Vite核心原理

3.2 如前所述ESM 的 HMR 热预览

目前所有的装箱辅助工具实现热预览的思路都大同小异:主要是透过WebSocket创建应用程序和伺服器的通信监听文档的改变,当文档被修改时,服务端发送消息通知客户端修改适当的标识符,客户端对应不同的文档展开不同的操作的预览。

3.2.1 VS Webpack

Webpack: 重新校对,允诺变更后组件的标识符,客户端重新读取

Vite: 允诺变更的组件,再重新读取

Vite 透过 chokidar 来监听文档系统的变更,只用对发生变更的组件重新读取, 只需要精确的使相关组件与其临近的 HMR边界连接失效即可,这样HMR 预览速率就不会因为应用体积的增加而变慢而 Webpack 还要经历一次装箱构筑。所以 HMR 情景下,Vite 表现也要好于 Webpack。

3.2.2 核心理念流程

Vite整座热预览操作过程可以分成四步

创建一个websocket服务端和client文档,开启服务透过chokidar监听文档变更当标识符变更后,服务端展开判断并推送到客户端客户端根据推送的信息执行不同操作的预览

整体流程图:

深入理解Vite核心原理

3.2.2.1 开启热预览:createWebSocketServer

在 Vite“ dev server 开启之前,Vite 会为 HMR 做一些准备工作:比如创建websocket服务,借助chokidar创建一个监听对象 watcher 用于对文档修改展开监听等等,具体核心理念标识符:

源码位置:packages/vite/src/node/server/index.ts
export async function createServer( inlineConfig: InlineConfig = {} ): Promise<ViteDevServer> { …. const ws = createWebSocketServer(httpServer, config, httpsOptions) const { ignored = [], …watchOptions } = serverConfig.watch || {} const watcher = chokidar.watch(path.resolve(root), { ignored: [ **/node_modules/**, **/.git/**, …(Array.isArray(ignored) ? ignored : [ignored]) ], ignoreInitial: true, ignorePermissionErrors: true, disableGlobbing: true, …watchOptions }) as FSWatcher …. watcher.on(change, async (file) => { }) watcher.on(add, (file) => { }) watcher.on(unlink, (file) => { }) … return server }

createWebSocketServer这个方法主是创建WebSocket服务并对错误展开一些处理,最后回到封装好的on、off、 send 和 close 方法,用于后续服务端推送消息和关闭服务。

源码位置:packages/vite/src/node/server/ws.ts
export function createWebSocketServer( server: Server | null, config: ResolvedConfig, httpsOptions?: HttpsServerOptions ): WebSocketServer { let wss: WebSocket let httpsServer: Server | undefined = undefined // 热预览实用性 const hmr = isObject(config.server.hmr) && config.server.hmr const wsServer = (hmr && hmr.server) || server // 普通模式 if (wsServer) { wss = new WebSocket({ noServer: true }) wsServer.on(upgrade, (req, socket, head) => { // 监听透过vite客户端发送的websocket消息,透过HMR_HEADER区分 if (req.headers[sec-websocket-protocol] === HMR_HEADER) { wss.handleUpgrade(req, socket as Socket, head, (ws) => { wss.emit(connection, ws, req) }) } }) } else { // 中间件模式 // vite dev server in middleware mode wss = new WebSocket(websocketServerOptions) } wss.on(connection, (socket) => { … }) // 错误处理 wss.on(error, (e: Error & { code: string }) => { … }) // 回到 return { on: wss.on.bind(wss), off: wss.off.bind(wss), send(payload: HMRPayload) { … }, close() { … } } }

3.2.2.2 执行热预览:moduleGraph+handleHMRUpdate组件

接收到文档更动执行的回调,这里主要两个操作:moduleGraph.onFileChange修改文档的内存和handleHMRUpdate执行热预览

源码位置:packages/vite/src/node/server/index.ts
watcher.on(change, async (file) => { file = normalizePath(file) if (file.endsWith(/package.json)) { return invalidatePackageData(packageCache, file) } // invalidate module graph cache on file change moduleGraph.onFileChange(file) if (serverConfig.hmr !== false) { try { await handleHMRUpdate(file, server) } catch (err) { ws.send({ type: error, err: prepareError(err) }) } } })

3.2.2.2.1 moduleGraph

moduleGraph 是Vite定义的用来记录整座应用的组件倚赖图的类,除此之外还有moduleNode。

源码位置:packages/vite/src/node/server/moduleGraph.ts
深入理解Vite核心原理
深入理解Vite核心原理

moduleGraph是由一系列 map 组成,而这些map分别是url、id、file等与ModuleNode的映射,而ModuleNode 是 Vite中定义的最小组件单位。透过这两个类可以构筑下面的组件倚赖图:

深入理解Vite核心原理

可以看看moduleGraph.“onFileChange这个函数:主要是用来清空被修改文档对应的ModuleNode对象的 transformResult 属性,使之前的组件已有的转换内存失效。这块也就是Vite在热预览里的内存机制。可以看看官网的如是说

源码位置:packages/vite/src/node/server/moduleGraph.ts
onFileChange(file: string): void { const mods = this.getModulesByFile(file) if (mods) { const seen = new Set<ModuleNode>() mods.forEach((mod) => { this.invalidateModule(mod, seen) }) } } invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void { mod.info = undefined mod.transformResult = null mod.ssrTransformResult = null invalidateSSRModule(mod, seen) }

3.2.2.2.2 handleHMRUpdate

handleHMRUpdate 组件主要是监听文档的更改,进行处理和判断透过WebSocket给客户端发送消息通知客户端去允诺新的组件标识符。

源码位置:packages/vite/packages/vite/src/node/server/hmr.ts
深入理解Vite核心原理

3.2.2.3 客户端:websocket通信和预览处理

客户端:当我们实用性了热预览且不是ssr的这时候,Vite底层在处理html的这时候会把HMR相关的客户端标识符写入在我们的标识符中,如下表所示:

深入理解Vite核心原理

当接收到服务端推送的消息,透过不同的消息类型做适当的处理,如(connected、update、custom…),在实际合作开发热预览中采用最频繁的是update(动态读取热预览组件)和full-reload(刷新整座页面)事件。

源码位置:packages/vite/packages/vite/src/client/client.ts

核心理念标识符实现

深入理解Vite核心原理

3.2.2.4 强化:应用程序的内存思路提高响应速率

与此同时,Vite 还借助HTTP加速整座页面的重新读取。设置响应头使得倚赖组件(dependency module)展开强内存,而源码文档透过设置 304 Not Modified 而变成可依据条件而展开预览。

若需要对倚赖标识符模块做更动可手动操作使内存失效:

vite –force

或者手动删除 node_modules/.“vite 中的内存文档。

3.3 如前所述esbuild的倚赖预校对强化

3.3.1 为什么需要预构筑?

支持commonJS倚赖上面提到Vite是如前所述应用程序原生植物支持ESM的能力实现的,但要求采用者的标识符组件必须是ESM组件,因此必须将commonJs的文档提前处理,转化成 ESM 组件并内存入 node_modules/.vite减少组件和允诺数量

除此之外,我们常用的lodash辅助工具库,里面有很多包透过单独的文档相互引入,而 lodash-es这种包会有几百个子组件,当标识符中出现 import { debounce } from lodash-es 会发出几百个 HTTP 允诺,这些允诺会造成网络堵塞,影响页面的读取。

Vite 将有许多内部组件的 ESM 倚赖关系转换为单个组件,以提高后续页面读取性能。

透过预构筑 lodash-es 成为一个组件,也就只需要一个 HTTP 允诺了!

3.3.2 为什么采用Esbuild?

深入理解Vite核心原理

引用尤大的一句话:“快”就一个字

这是Esbuild首页的图。第三代的装箱辅助工具,提供了与Webpack、Rollup、Parcel 等辅助工具相似的资源装箱能力,但在时速上达到10~100倍的差距,耗时是Webpack2%~3%

校对运行 VS 解释运行大多数后端装箱辅助工具都是如前所述 JavaScript 实现的,大家都知道JavaScript是解释型语言,边运行边解释。而 Esbuild 则选择采用 Go 语言编写,该语言可以校对为原生植物标识符,在校对的这时候都将语言转为机器语言,在开启的时候间接执行即可,在 CPU 密集情景下,Go 更具性能优势。多线程 VS 单线程JavaScript 本质上是一门单线程语言,直到引入 WebWorker 之后才有可能在应用程序、Node 中实现多线程操作。就我对Webpack的源码认知,其源码也并未采用 WebWorker 提供的多线程能力。而GO天生的多线程优势。对构筑流程展开了强化,充分借助 CPU 资源

3.3.3 实现基本原理?

Vite预校对之后,将文档内存在node_modules/.vite/文档夹下。根据以下地方来决定是否需要重新执行预构筑。

package.json中:dependencies发生变化包管理器的lockfile

如果想强制让Vite重新预构筑倚赖,可以采用–force开启合作开发伺服器,或者间接删掉node_modules/.vite/文档夹。

3.3.3.1 核心理念标识符实现

透过createServer创建server对象后,当伺服器开启会执行httpServer.listen方法在执行createServer时,Vite底层会重写server.listen方法:首先调用应用程序的buildStart再执行runOptimize()方法runOptimize()调用optimizeDeps()和createMissingImporterRegisterFn()方法
const runOptimize = async () => { if (config.cacheDir) { server._isRunningOptimizer = true try { server._optimizeDepsMetadata = await optimizeDeps( config, config.server.force || server._forceOptimizeOnRestart ) } finally { server._isRunningOptimizer = false } server._registerMissingImport = createMissingImporterRegisterFn(server) } } if (!middlewareMode && httpServer) { let isOptimized = false // overwrite listen to run optimizer before server start const listen = httpServer.listen.bind(httpServer) httpServer.listen = (async (port: number, …args: any[]) => { if (!isOptimized) { try { await container.buildStart({}) await runOptimize() isOptimized = true } catch (e) { httpServer.emit(error, e) return } } return listen(port, …args) }) as any } else { await container.buildStart({}) await runOptimize() }
存放在_metadata.json文档)。如果不是强预构筑就对照_metadata.json文档的hash和新生成的hash:一致就回到_metadata.json文档的内容,否则清空内存文档调用Esbuild构筑组件再次存入_metadata.json文档

3.3.3.2 整体的流程图

核心理念标识符都在packages/“vite“/“src“/“node“/optimizer/index.ts里面

自动搜寻倚赖主要组件:esbuildScanPlugin预构筑校对主要组件:esbuildDepPlugin
深入理解Vite核心原理

3.4 如前所述Rollup的 Plugins

Vite 从 preact 的 WMR 中得到了启发,将Vite Plugins继承Rollup“ Plugins “ “API,在其基础上展开了一些扩充(如Vite特有的钩子等),与此同时Vite也如前所述Rollup plugins机制提供了强大的应用程序API。目前和 Vite 兼容或者内建的应用程序,可以查看vite-rollup-plugins

3.4.1 Vite应用程序是什么

采用Vite应用程序可以扩充Vite能力,透过暴露一些构筑装箱操作过程的一些时机配合辅助工具函数,让采用者可以自定义地写一些实用性标识符,执行在装箱操作过程中。比如解析采用者自定义的文档输入,在装箱标识符前转译标识符,或者查找。

在实际的实现中,Vite 仅仅需要如前所述Rollup设计的接口展开扩展,在保证兼容 Rollup应用程序的与此同时再加入一些Vite特有的钩子和属性来展开扩充。

3.4.2 Vite独有钩子

对于各个钩子的具体采用可以移步这里

config:可以在Vite被解析之前修改Vite的相关实用性。钩子接收原始采用者实用性config和一个描述实用性环境的变量envconfigResolved:解析Vite实用性后调用,实用性确认configureserver:主要用来实用性合作开发伺服器,为dev-server添加自定义的中间件transformindexhtml:主要用来转换index.html,钩子接收当前的 HTML 字符串和转换上下文handlehotupdate:执行自定义HMR预览,可以透过ws往客户端发送自定义的事件

3.4.3 通用钩子

这里举一些常用的通用钩子,其余的通用钩子可以移步这里

服务开启时调用一次

每个传入组件允诺时被调用

resolveId: 创建自定义确认函数,可以用来定位第三方倚赖load:可以自定义读取器,可用来回到自定义的内容transform:在每个传入组件允诺时被调用,主要是用来转换单个组件服务关闭时调用一次

buildend:在伺服器关闭时被调用closeBundle

3.4.4 钩子的调用顺序

引用@young村长的一个图:

深入理解Vite核心原理

Vite的官网可以看出:Vite应用程序可以用一个 enforce 属性(近似于 Webpack读取器)来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的应用程序将按照以下顺序排列:

Aliasenforce:pre的自定义应用程序Vite核心理念插件没enforce的自定义应用程序Vite构筑用的应用程序enforce:post的自定义应用程序Vite后置构筑应用程序

3.4.5 自定义应用程序

编写应用程序标识符
export default function myVitePlugin () { // 定义vite应用程序唯一id const virtualFileId = @my-vite-plugin // 回到的整座插件对象 return { // 必须的,将会显示在 warning 和 error 中 name: vite-plugin, // 钩子 // config config: (config, env) => ({ console.log(config,config) return {} }), // 确认config configResolved: config => ({}), options: options => ({}), buildStart: options => ({}), transformIndexHtml: (html, ctx) => ({ return html }), //确认 resolveId: (source, importer) => ({}), // 转换 transform: (code, id) => ({}) } }
引入应用程序:vite.config.js/ts 中引用
// vite.config.js/ts import myVitePlugin from … export default defineConfig{ plugins:[vue(),myVitePlugin()] }

4 总结

最后总结下Vite相关的优缺点:

优点:

快速的UAC: 采用No Bundle和esbuild预构筑,速率远快于Webpack高效的热更新:如前所述ESM实现,与此同时借助HTTP头来加速整座页面的重新读取,增加内存思路或者说的按需读取: 如前所述应用程序ESM的支持,实现或者说的按需读取缺点

生态:目前Vite的生态不如Webapck,不过我觉得生态也只是时间上的问题。生产环境由于esbuild对css和标识符分割不友好采用Rollup展开装箱

Vite.js虽然才在构筑装箱情景兴起,但在很多情景下基本都会优于现有的软件系统。如果有生态、想要丰富的loader、plugins的要求可以考虑成熟的Webpack。在其余情况下,Vite.js不失为一个装箱构筑辅助工具的好选择。

5 附录

官方文档Esbuild官网Vite 应用程序 APIRollup官方文档Rollup – Next-generation ES6 module bundler – Interview with Rich HarrisVite源码Vite2应用程序合作开发指南

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务