写点什么

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

作者:@零度
  • 2022 年 5 月 05 日
  • 本文字数:4661 字

    阅读完需:约 15 分钟

背景

这里的背景介绍会从与 Vite 紧密相关的两个概念的发展史说起,一个是 JavaScript 的模块化标准,另一个是前端构建工具。

共存的模块化标准

为什么 JavaScript 会有多种共存的模块化标准?因为 js 在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从 CommonJS 开始,到 ES6 正式推出 ES Modules 规范结束,所有争论,终成历史,ES Modules 也成为前端重要的基础设施。

  • CommonJS:现主要用于 Node.js(Node@13.2.0 开始支持直接使用 ES Module)

  • AMD:require.js 依赖前置,市场存量不建议使用

  • CMD:sea.js 就近执行,市场存量不建议使用

  • ES Module:ES 语言规范,标准,趋势,未来

发展中的构建工具

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



当前工程化痛点

现在常用的构建工具如 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,开发者才能开始开发。



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



基本用法

创建 vite 项目

$ npm create vite@latest

选取模板

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



启动

{

"scripts": {

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

"build": "vite build", // 为生产环境构建产物

"preview": "vite preview" // 本地预览生产构建产物

}

}

实现原理

ESbuild 编译

esbuild 使用 go 编写,cpu 密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比:浏览器:“开始了吗?”服务器:“已经结束了。”开发者:“好快,好喜欢!!”



依赖预构建

  • 模块化兼容: 如开头背景所写,现仍共存多种模块化标准代码,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 中的模块,需要做路径转换后交给浏览器_web前端培训

  • 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 代码是服务端逻辑,用于处理代码的构建与页面模块的请求。

简单看了下源码(vite@2.7.2),核心功能主要是以下几个方法(以下为源码截取,部分逻辑做了删减):

  1. 命令行启动服务 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



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

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

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

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

  • Vite 使用 esbuild 编译,构建速度比 webpack 快几个数量级

兼容性

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

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

文章来源于前端开发

用户头像

@零度

关注

关注尚硅谷,轻松学IT 2021.11.23 加入

IT培训 www.atguigu.com

评论

发布
暂无评论
web前端培训Vite的原理源码解析_前端开发_@零度_InfoQ写作社区