web前端培训Vite的原理源码解析

2022-12-06 0 1,000

web前端培训Vite的原理源码解析

大背景

这儿的大背景如是说会从与Vite密切有关的三个基本概念的文化史讲起,三个是JavaScript的模组化国际标准,另三个是后端构筑辅助工具。

并存的模组化国际标准

为何JavaScript会有多种不同并存的模组化国际标准?即使js在结构设计Hathras并没模组化的基本概念,随著后端销售业务维数急速提升,模组化愈来愈受开发人员的倚重,街道社区已经开始不断涌现多种不同模组化软件系统,它互相先进经验,也争辩急速,逐步形成数个政治势力,从CommonJS已经开始,到ES6正式宣布推出ES Modules规范化完结,大部份争辩,实乃发展史,ES Modules也正式成为后端关键的基础建设。

CommonJS:现主要就用作Node.js([email protected]已经开始全力支持间接采用ES Module)

AMD:require.js 倚赖后置,消费市场增量不提议采用

CMD:sea.js 设点继续执行,消费市场增量不提议采用

ES Module:ES语言规范化,国际标准,趋势,未来

发展中的构筑辅助工具

近些年后端工程化发展迅速,各种构筑辅助工具层出不穷,目前Webpack仍然占据统治地位,npm 每周下载量达到两千多万次。下面是我按 npm 发版时间线列出的开发人员比较熟知的一些构筑辅助工具。

web前端培训Vite的原理源码解析

当前工程化痛点

现在常用的构筑辅助工具如Webpack,主要就是通过抓取-编译-构筑整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构筑打包后,再把打包后的代码交给dev server(开发服务器)。

Webpack等构筑辅助工具的诞生给后端开发带来了极大的便利,但随著后端销售业务的复杂化,js代码量呈指数增长,打包构筑时间愈来愈久,dev server(开发服务器)性能遇到瓶颈:

缓慢的服务启动: 大型项目中dev server启动时间达到几十秒甚至几分钟。

缓慢的HMR热更新:即使采用了 HMR 模式,其热更新速度也会随著应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。

缓慢的开发环境,大大降低了开发人员的幸福感,在以上大背景下Vite应运而生。

什么是Vite?

基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构筑工具!

基本概念

先如是说以下文中会经常提到的一些基础基本概念:

倚赖: 指开发不会变动的部分(npm包、UI组件库),esbuild进行预构筑。

源代码:浏览器不能间接继续执行的非js代码(.jsx、.css、.vue等),vite只在浏览器请求有关源代码的时候进行转换,以提供ESM源代码。

开发环境

利用浏览器原生的ES Module编译能力,省略费时的编译环节,直给浏览器开发环境源代码,dev server只提供轻量服务。

浏览器继续执行ESM的import时,会向dev server发起该模块的ajax请求,服务器对源代码做简单处理后返回给浏览器。

Vite中HMR是在原生 ESM 上继续执行的。当编辑三个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。

采用esbuild处理项目倚赖,esbuild采用go编写,比一般node.js编写的编译器快几个数量级。

生产环境

集成Rollup打包生产环境代码,倚赖其成熟稳定的生态与更简洁的插件机制。

处理流程对比

Webpack通过先将整个应用打包,再将打包后代码提供给dev server,开发人员才能已经开始开发。

web前端培训Vite的原理源码解析

Vite间接将源代码交给浏览器,实现dev server秒开,浏览器显示页面需要有关模块时,再向dev server发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。

web前端培训Vite的原理源码解析

基本用法

创建vite项目

$ npm create vite@latest

选取模板

Vite 内置6种常用模板与对应的TS版本,可满足后端大部分开发场景,可以点击下列表格中模板间接在 StackBlitz中在线试用。

web前端培训Vite的原理源码解析

启动

{

“scripts”: {

“dev”: “vite”, // 启动开发服务器,别名:`vite dev`,`vite serve`

“build”: “vite build”, // 为生产环境构筑产物

“preview”: “vite preview” // 本地预览生产构筑产物

}

}

实现基本原理

ESbuild 编译

esbuild 采用go编写,cpu密集下更具性能优势,编译速度更快,以下摘自官网的构筑速度对比:

浏览器:“已经开始了吗?”服务器:“已经完结了。”开发人员:“好快,好喜欢!!”
web前端培训Vite的原理源码解析

倚赖预构筑

模组化兼容:如开头大背景所写,现仍并存多种不同模组化国际标准代码,Vite在预构筑阶段将倚赖中各种其他模组化规范化(CommonJS、UMD)转换 成ESM,以提供给浏览器。

性能优化:npm包中大量的ESM代码,大量的import请求,会造成网络拥塞。Vite采用esbuild,将有大量内部模块的ESM关系转换成单个模块,以减少 import模块请求次数。

按需加载

服务器只在接受import请求的时候,才会编译对应的文件,将ESM源代码返回给浏览器,实现真正的按需加载。

缓存

HTTP缓存:充分利用http缓存做优化,倚赖(不会变动的代码)部分用max-age,immutable强缓存,源代码部分用304协商缓存,提升页面打开速度。

文件系统缓存:Vite在预构筑阶段,将构筑后的倚赖缓存到node_modules/.vite ,有关配置更改时,或手动控制时才会重新构筑,以提升构筑速度。

重写模块路径

浏览器import只能引入相对/绝对路径,而开发代码经常采用npm包名间接引入node_module中的模块,需要做路径转换后交给浏览器。

es-module-lexer 扫描 import 语法

magic-string 重写模块的引入路径

// 开发代码

import { createApp } from vue

// 转换后

import { createApp } from /node_modules/vue/dist/vue.js

源代码分析

与Webpack-dev-server类似Vite同样采用WebSocket与客户端建立连接,实现热更新,源码实现基本可分为两部分,源代码位置在:

vite/packages/vite/src/client client(用作客户端)

vite/packages/vite/src/node server(用作开发服务器)

client 代码会在启动服务时注入到客户端,用作客户端对于WebSocket消息的处理(如更新页面某个模块、刷新页面);server 代码是服务端逻辑,用作处理代码的构筑与页面模块的请求。

简单看了下源代码([email protected]),核心功能主要就是以下几个方法(以下为源代码截取,部分逻辑做了删减):

命令行启动服务npm run dev后,源代码继续执行cli.ts,调用createServer方法,创建http服务,监听开发服务器端口。

// 源代码位置 vite/packages/vite/src/node/cli.ts

const { createServer } = await import(./server)

try {

const server = await createServer({

root,

base: options.base,

})

if (!server.httpServer) {

throw new Error(HTTP server not available)

}

await server.listen()

}

1.createServer方法的继续执行做了很多工作,如整合配置项、创建http服务(早期通过koa创建)、创建WebSocket服务、创建源代码的文件监听、插件继续执行、optimize优化等。下面注释中标出。

// 源代码位置 vite/packages/vite/src/node/server/index.ts

export async function createServer(

inlineConfig: InlineConfig = {}

): Promise<ViteDevServer> {

// Vite 配置整合

const config = await resolveConfig(inlineConfig, serve, development)

const root = config.root

const serverConfig = config.server

// 创建http服务

const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)

// 创建ws服务

const ws = createWebSocketServer(httpServer, config, httpsOptions)

// 创建watcher,设置代码文件监听

const watcher = chokidar.watch(path.resolve(root), {

ignored: [

**/node_modules/**,

**/.git/**,

…(Array.isArray(ignored) ? ignored : [ignored])

],

…watchOptions

}) as FSWatcher

// 创建server对象

const server: ViteDevServer = {

config,

middlewares,

httpServer,

watcher,

ws,

moduleGraph,

listen,

}

// 文件监听变动,websocket向后端通信

watcher.on(change, async (file) => {

handleHMRUpdate()

})

// 非常多的 middleware

middlewares.use(…)

// optimize

const runOptimize = async () => {…}

return server

}

2.采用chokidar监听文件变化,绑定监听事件。

// 源代码位置 vite/packages/vite/src/node/server/index.ts

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

3.通过ws来创建WebSocket服务,用作监听到文件变化时触发热更新,向客户端发送消息。

// 源代码位置 vite/packages/vite/src/node/server/ws.ts

export function createWebSocketServer(…){

let wss: WebSocket

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) => {

// 服务就绪

if (req.headers[sec-websocket-protocol] === HMR_HEADER) {

wss.handleUpgrade(req, socket as Socket, head, (ws) => {

wss.emit(connection, ws, req)

})

}

})

} else {

}

// 服务准备就绪,就能在浏览器控制台看到熟悉的打印 [vite] connected.

wss.on(connection, (socket) => {

socket.send(JSON.stringify({ type: connected }))

})

// 失败

wss.on(error, (e: Error & { code: string }) => {

})

// 返回ws对象

return {

on: wss.on.bind(wss),

off: wss.off.bind(wss),

// 向客户端发送信息

// 数个客户端同时触发

send(payload: HMRPayload) {

const stringified = JSON.stringify(payload)

wss.clients.forEach((client) => {

// readyState 1 means the connection is open

client.send(stringified)

})

}

}

}

4.在服务启动时会向浏览器注入代码,用作处理客户端接收到的WebSocket消息,如重新发起模块请求、刷新页面。

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

async function handleMessage(payload: HMRPayload) {

switch (payload.type) {

case connected:

console.log(`[vite] connected.`)

break

case update:

notifyListeners(vite:beforeUpdate, payload)

break

case custom: {

notifyListeners(payload.event as CustomEventName<any>, payload.data)

break

}

case full-reload:

notifyListeners(vite:beforeFullReload, payload)

break

case prune:

notifyListeners(vite:beforePrune, payload)

break

case error: {

notifyListeners(vite:error, payload)

break

}

default: {

const check: never = payload

return check

}

}

}

优势

快!快!非常快!!

高度集成,开箱即用。

基于ESM急速热更新,无需打包编译。

基于esbuild的倚赖预处理,比Webpack等node编写的编译器快几个数量级。

兼容Rollup庞大的插件机制,插件开发更简洁。

不与Vue绑定,全力支持React等其他框架,独立的构筑辅助工具。

内置SSR全力支持。

天然全力支持TS。

不足

Vue仍为第一优先全力支持,量身定做的编译插件,对React的全力支持不如Vue强大。

虽然已经面世2.0正式宣布版,已经可以用作正式宣布线上生产,但目前消费市场上实践少。

生产环境集成Rollup打包,与开发环境最终继续执行的代码不一致。

与 webpack 对比

由于Vite主打的是开发环境的极致体验,生产环境集成Rollup,这儿的对比主要就是Webpack-dev-server与Vite-dev-server的对比:

到目前很长时间以来Webpack在后端工程领域占统治地位,Vite面世以来备受关注,街道社区活跃,GitHub star 数量激增,目前达到37.4K

web前端培训Vite的原理源码解析

Webpack配置丰富采用极为灵活但上手成本高,Vite开箱即用配置高度集成

Webpack启动服务需打包构筑,速度慢,Vite免编译可秒开

Webpack热更新需打包构筑,速度慢,Vite毫秒响应

Webpack成熟稳定、资源丰富、大量实践案例,Vite实践较少

Vite采用esbuild编译,构筑速度比webpack快几个数量级

兼容性

默认目标浏览器是在script标签上全力支持原生 ESM 和 原生 ESM 动态导入

可采用官方插件 @vitejs/plugin-legacy,转义成传统版本和相对应的polyfill

后端开发

举报/反馈

相关文章

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

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