Skip to main content

Vite

概述

前端工程痛点

  1. 模块化需求,不同的模块化类型,如ESM、CommonJS、UMD等。
  2. 兼容旧浏览器,编译高级语法,如 .ts.jsx等。
  3. 开发效率,生产环境的代码执质量。

why

  1. Vite 基于浏览器原生 ESM 的支持实现模块加载。
  2. 开发环境基于原生 ES 模块,使用 ESbuild 依赖预构建,支持 HMR 模块热替换。
  3. 生产环境基于 Rollup 实现打包,更好的 Tree Shaking 优化。
  4. 上手简单,开箱即用。

双引擎架构

ESbuild
  1. 依赖预构建,作为打包工具。
  2. 单文件编译,可编译 TypeScript 和 JSX。
  3. 代码压缩,可作为压缩工具。
Rollup
  1. 生产环境打包器,CSS代码分割,预加载指令生成,异步加载优化等。
  2. 插件可与 Vite 插件兼容。

Webpack

Vite 与 Webpack 经常被进行比较,它们区别的体现在如下方面:

  1. 两者定位不一样。Webpack Core 是一个纯打包工具;而 Vite 是一个更上层的工具链(Webpack、Web常用配置、webpack-dev-server的结合)。
  2. 两者预设场景不一样。Webpack Core 只针对打包不预设场景,所以设计得及其灵活;而 Vite 缩窄预设场景来降低复杂度,预设了 Web 场景。
  3. 插件机制不一致。Webpack 的本质是先打包再加载,loader及插件机制跟打包的这个设计前提耦合过深;而 Vite 的插件机制是基于 Rollup 的,单个模块的解析及加载跟打包环节完全解耦。

依赖预构建

ESM 存在诸多问题,如第三方打包无法控制、可能会产生请求瀑布流的问题,而依赖预构建可以优化这种类似的问题。

构建过程

在预构建过程中,Vite 主要做了两件事:

  1. 使用 ESbuild 对第三方库进行打包,由于 ESbuild 使用 Go 语言编写,速度非常快,几乎是无感的。
  2. 重写依赖的 URL,如 react 可能会重写为 /node_modules/.vite/deps/react.js,浏览器则自动请求此构建后的文件。

依赖搜索

Vite 在搜索依赖时,会按照一定的策略和顺序进行检索:

  1. 优先查找预构建缓存。
  2. 若果没有找到缓存,则自动寻找引入的依赖项。
  3. 服务启动后,Vite 将重新运行依赖项构建过程并加入缓存,当访问到某些还没有构建的依赖时,Vite 会即时编译并缓存,并重新加载页面。

模拟预构建

可以使用 ESbuild 进行预构建模拟实现:

import esbuild from 'esbuild'

const deps = []

function depScanPlugin(deps) {
return {
name: 'esbuild-dep-scan',
setup(build) {
build.onResolve(
{
filter: /^[^\.]/
},
(args) => {
deps.push(args.path)
}
)
}
}
}

;(async () => {
await esbuild.build({
entryPoints: ['./src/index.tsx'],
write: false,
bundle: true,
outdir: './dist',
plugins: [depScanPlugin(deps)]
})

await esbuild.build({
entryPoints: deps,
write: true,
bundle: true,
format: 'esm',
outdir: './node_modules/.vite/deps'
})
})();

其实 Vite 的预构建过程也大体上也是这样,只是在这基础上做了更多的事情。

自定义行为

如果说不想针对某些第三方依赖进行预构建,可将其排除:

vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
optimizeDeps: {
exclude: [
'react',
'react-dom'
]
}
})

CSS

原生 CSS 存在诸多问题,如开发体验欠佳、样式污染、浏览器兼容及代码体积等问题。

而 Vite 中内置支持 CSS 及其相关预处理器,仅需安装相关的依赖即可。

预处理器

sass 为例:

pnpm add sass -D

在配置文件中可进行相关配置:

export default defineConfig({
css: {
preprocessorOptions: {
less: {
// ...
},
styl: {
// ...
},
scss: {
// ...
},
},
},
})

CSS Modules

以模块化的方式使用 CSS,可将 CSS 类名处理成哈希值,避免同名的情况下样式污染的问题。

import React, { useCallback, useState } from 'react'
import styles from './styles/mian.module.scss'

const App = () => {
const [count, setCount] = useState(0)

const handlePlus = useCallback(() => {
setCount(count + 1)
}, [count])

return (
<div>
<div className={styles['bg-red']}>{count}</div>
<button onClick={handlePlus}>+</button>
</div>
)
}

export default App

PostCSS

PostCSS 是一个工具,用于通过 JavaScript 插件对 CSS 代码进行转换和处理。它允许开发者使用插件链的方式来操作 CSS,从而实现自动添加浏览器前缀、嵌套样式、支持变量等功能。PostCSS 被广泛应用于 CSS 工具链中,特别是用于增强 CSS 的可维护性和兼容性。

Vite 内置支持 PostCSS,只需安装对应的依赖即可。

pnpm add postcss postcss-preset-env -D

PostCSS 支持独立的配置文件,但不支持热更新,只能重启服务;而 Vite 配置文件中也支持单独的 PostCSS 配置,但支持热更新。

import { defineConfig } from 'vite'
import presetEnv from 'postcss-preset-env'

export default defineConfig({
css: {
postcss: {
plugins: [
presetEnv({
browsers: [
'last 2 versions',
'> 1%',
'IE 11'
],
autoprefixer: {}
})
]
}
},
})

Tailwind CSS

Tailwind CSS 是一个流行的 CSS 原子化解决方案,主要解决 CSS 开发体验的问题。Tailwind CSS 需要 PostCSS 配合构建。

pnpm add tailwindcss -D

在配置文件中以 PostCSS 插件的形式配置:

import { defineConfig } from 'vite'
import type { UserConfig } from 'vite'
import presetEnv from 'postcss-preset-env'
import tailwindcss from 'tailwindcss'

export default defineConfig({
css: {
postcss: {
plugins: [
presetEnv({
browsers: [
'last 2 versions',
'> 1%',
'IE 11'
],
autoprefixer: {}
}),
tailwindcss()
]
}
},
}) as UserConfig

静态资源

别名

别名可让模块路径重定向到具体的路径,可简化模块的导入。

import { defineConfig } from 'vite'
import type { UserConfig } from 'vite'
import { resolve } from 'node:path'

export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@assets': resolve(__dirname, './src/assets')
}
},
}) as UserConfig

图片

解析为URL

以下几种方式的图片资源解析后会返回 URL:

  1. 以模块的形式导入图片。
import logo from './assets/logo.png'
  1. 在 CSS 中以 url() 的方式使用图片资源。
.logo {
background: url('./assets/logo.png');
}
  1. JSX 或 Vue SFC 模板中的资源引用都将自动转换为导入。
  2. 普通的动态变量路径不会自动转换导入。
动态导入
  1. import 动态导入。
import spring from '@assets/spring.jpg'
const imgPath = ref(spring)
const handleChange = (e: Event) => {
const v = (e.target as HTMLButtonElement).value
// 动态导入
import(`@assets/${v}.jpg`).then((res) => {
imgPath.value = res.default
})
}

打包后会生成相应的 .js 文件,一般不推荐使用。

  1. new URL()

可使用 new URL() 进行动态变量的处理:

const imgPath = ref('spring');

const url = computed(() => {
const href = new URL(`../assets/${imgPath.value}.jpg`, import.meta.url).href;
return href;
})

多模块导入

import.meta.glob 可进行多模块导入,简化繁琐的导入:

const monthImgs = import.meta.glob('@/assets/month/*.jpg', { eager: true });

const imgUrls = Object.values(monthImgs).map((mod) => {
return (mod as {default:string}).default
})
console.log(imgUrls);

其他资源

有一些资源并没有被 Vite 标记为静态资源,比如 .md 文件等,可采用以下几种方式解决。

  1. 将此种资源进行模块声明,方便 TypeScript 识别。
declare module '*.md' {
const str: string
export default str
}
  1. 显示 URL 引入,即解析后返回具体的 URL。
import md from './readme.md?url'
  1. 将资源引入为字符串,即将原内容输出。
import md from './readme.md?raw'
  1. 扩展内部列表。
export default defineConfig({
assetsInclude: [".md"]
})

public 目录

public 目录位于项目的根目录,该目录的资源在开发时通过 / 根路径访问,生产环境会被完整复制到目标目录的根目录下。

以下资源文件可考虑放到 public 目录:

  1. 不会被源码引入的静态文件。
  2. 必须保持原有文件名的文件。
  3. 不想引入该资源,只想得到其 URL。

内联资源

静态资源可以有两种构建方式,即内联和外链。

Vite 中,默认对体积小于 4KB 的资源使用内联方式,即将其作为 base64 格式的字符串内联。

note

对于比较小的资源,适合内联到代码中,一方面对代码体积的影响很小,另一方面可以减少不必要的网络请求,优化网络性能。

SVG 格式的文件不受影响,始终会打包成单独的文件。

除此之外,可通过 assetsInlineLimit 指定内联的阈值:

export default defineConfig({
build: {
assetsInlineLimit: 1024 * 5
}
})

HMR

HMR(Hot Module Replacement),即热模块替换,主要解决了模块局部更新以及状态保存的问题。在页面模块更新的时候,直接把页面中发生变化的模块替换为新的模块,同时不会影响其它模块的正常运作。

原理

HMR 基于原生的 ESM 模块规范实现。在文件发生改变时,Vite 会监听到相应 ES 模块的变化,从而触发相应的 API,实现局部的更新。

import.meta 对象为现代浏览器原生的一个内置对象,Vite 是在这个对象上的 hot 属性中定义了一套完整的属性和方法。在 Vite 当中,可以通过 import.meta.hot 来访问关于 HMR 的这些属性和方法。

API

// 自身模块的更新
hot.accept(cb: (mod: ModuleNamespace | undefined) => void): void

// 指定某个子模块的更新
hot.accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void

// 指定多个子模块的更新
hot.accept(deps: readonly string[], cb: (mod: ModuleNamespace | undefined) => void): void

// 模块销毁
hot.dispose()

// 共享数据
hot.data

插件机制

Vite 也提供了插件机制,以增强开发及构建的功能,开发者也可根据 Plugin API 自定义插件。

Vite 的生产构建是基于 Rollup,因此 Rollup 的插件完全兼容 Vite,Vite 插件中的所有 Rollup 插件钩子都会生效。

使用插件

Vite 中使用插件很简单,只需在 plugins 选项中注册即可:

import { defineConfig } from 'vite'
import type { UserConfig } from 'vite'

// React 插件
import react from '@vitejs/plugin-react'

export default defineConfig({
// 使用插件
plugins: [react()]
}) as UserConfig

自定义插件

开发者可以自定义自己的插件。插件推荐以 vite-plugin- 开头,格式一般如下:

import { Plugin } from 'vite'
export default function testPlugin():Plugin {
return {
// 插件名称
name: 'vite-plugin-test',

// 调整插件顺序
enforce: 'pre' | 'post',

// 在开发或构建模式中应用
apply: 'server' | 'build',

// hooks
}
}

钩子函数

通用钩子

通用钩子即为 Rollup 中的钩子,可兼容 Vite,具体可查看 Rollup 章节

独有钩子

Vite 插件除了兼容 Rollup 的钩子,也有独有的钩子:

  • config 可读取最开始的配置信息。
  • configResolved 可读取最终的配置信息。
  • configureServer 可获取服务私立,对开发服务器进行处理,如添加中间件等。
  • transformIndexHtml 转换 HTML 的内容。
  • handleHotUpdate 热模块更新过滤或者进行自定义热更新处理。
import { Plugin } from 'vite'
export default function testPlugin():Plugin {
return {
name: 'vite-pluign-test',
config(config, configEnv) { },
configResolved(resolvedConfig) { },
options(opts) { },
configureServer(server) { },
buildStart() { },
buildEnd() { },
closeBundle() { }
}
}
执行顺序
  1. 服务启动阶段

config

configResolved

options

configureServer

buildStart

  1. 请求阶段。

resolveId

load

transform

  1. 热更新阶段。

handleHotUpdate

  1. 服务关闭阶段。

buildEnd

closeBundle

插件顺序

  1. Alias(路径别名)相关插件。
  2. 带有 enforce: 'pre'的用户插件。
  3. Vite 核心插件(解析、编译等)。
  4. 没有 enforce 值的用户插件。
  5. Vite 生产环境构建使用的插件。
  6. 带有 enforce: 'post' 的用户插件。
  7. Vite 后置构建插件(压缩,报告等)。

性能优化

代码分割

拆包策略

Vite 在生产环境下使用 Rollup 进行构建,因此拆包也是基于 Rollup 完成。

默认自动处理 Initial Chunk 和 Async Chunk,并且会自动提取 Async Chunk 中的 CSS 为 Async CSS。

如果不想让 CSS 进行分包,而是并入一个文件,可配置如下:

export default defineConfig({
build: {
cssCodeSplit: false
}
})
自定义拆包

针对更细粒度的拆包,可基于通过 Rollup 进行手动分包:

export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
react: ['react', 'react-dom'],
antd: ['antd'],
},
// manualChunks(id) { }
}
}
}
})
循环引用问题

manualChunks 可以是一个函数,接收一个参数 id 即模块路径,用于自定义分包。但 manualChunks 逻辑过于简单,仅仅通过路径 id 来决定打包到哪个 Chunk 中,而漏掉了间接依赖的情况,可能会带来循环引用的问题。

vite-plugin-chunk-split 是一个 Vite 插件,支持多种拆包策略,可避免手动操作 manualChunks 潜在的循环依赖问题。

import { chunkSplitPlugin } from 'vite-plugin-chunk-split';

export default defineConfig({
plugins: [
chunkSplitPlugin({
// 指定拆包策略
customSplitting: {
'vue-vendor': ['vue', 'vue-router'],
'element-plus': ['element-plus'],
// 支持正则表达式 (src 中 components 下的所有文件被会被打包为`component-util`的 chunk 中)
'components-util': [/src\/components/]
}
})
],
})

预加载指令

Vite 会为入口 Chunk 和打包出的 HTML 中的直接引入自动生成 <link rel="modulepreload"> 指令。

modulepreload 是对于原生 ESM 模块的 preload。对于一般的模块,通常使用 preload 进行预加载,对于ESM模块,则使用 modulepreload

<script type="module" crossorigin src="/assets/index-CmhshF_D.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-YsBxPMQB.js">

modulepreload 的浏览器兼容性并不是太好,仅为74%,不过在 Vite 中可以通过配置一键开启 modulepreload 的 Polyfill。

export default defineConfig({
build: {
polyfillModulePreload: true
}
})

异步 Chunk

在实际项目中,Rollup 通常会生成一些共用的 Chunk:

// Home -> Async Chunk A -> Common Chunk C
// List -> Async Chunk B -> Common Chunk C

在无优化的情况下,当异步 Chunk A 被导入时,浏览器必须请求和解析 Chunk A 后,才会知道 Chunk A 需要共用 Common C,这会导致额外的等待时间。

Vite 使用一个预加载步骤自动重写代码,来分割动态导入调用,实现了当 Chunk A 被请求时,Common C 也通过被请求。

// Home -> ( Chunk A + Common C )

代码压缩

Vite 默认使用 ESbuild 压缩代码,因为其速度快,但是压缩效率 terser 更好。

export default defineConfig({
build: {
// minify: 'esbuild',
minify: 'terser'
}
})

网络优化

http/2

可以使用 http/2 来优化请求效率,开发阶段可使用 vite-plugin-mkcert 来开启 http/2,生产环境可使用 Nginx 开启 http/2

pnpm add vite-plugin-mkcert -D
预加载
  1. 使用 preloadmodulepreload 进行资源预加载。
  2. 使用 prefetch 可让浏览器在空闲时间进行资源加载。
DNS预解析

dns-prefetchpreconnect 搭配使用,进行DNS预解析,并降低请求延迟。

构建分析

可使用 rollup-plugin-visualizer 插件分析依赖模块的大小占比,从而可进行更有针对性的构建优化。

import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
plugins: [
visualizer({
opne: true,
})
]
})

CDN

使用 CDN 可以优化构建产出体积,但生产环境建议使用私域 CDN,不要使用公共免费的 CDN,因为其不稳定。

import externalGlobals from 'rollup-plugin-external-globals'

export default defineConfig({
build: {
resolve: {
alias: {
// 使用 alias 配置CDN 必须为 ESM 模块
'lodash-es': 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm'
}
}

rollupOptions: {
// 不打包指定的模块
external: ['vue', 'vue-router', 'element-plus'],
plugins: [
// 将指定的模块替换为全局变量
externalGlobals({
vue: 'Vue',
'vue-router': 'VueRouter',
'element-plus': 'ElementPlus'
})
]
}
}
})

/**
index.html:
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.3.12/dist/index.full.min.js"></script>
*/

gzip 压缩

gzip 是一种压缩方式,压缩效率更高。文件通过网络请求到客户端后由浏览器器进行解析,可降低网络传输消耗。

Vite 中可使用 vite-plugin-compression2 进行自定义 gzip 压缩:

import { compression } from 'vite-plugin-compression2'

export default defineConfig({
plugins: [
compression({
// 压缩算法,默认gzip
algorithm: 'brotliCompress',
// 匹配文件
include: [/\.(js)$/, /\.(css)$/,],
// 压缩超过此大小的文件,以字节为单位
threshold: 10240,
// 是否删除源文件,只保留压缩文件
/deleteOriginalAssets: true
}),
]
})

图片压缩

图片资源如果体积过大,也会影响加载性能,因此可以对图片进行适当的压缩以优化性能:

import viteImagemin from 'vite-plugin-imagemin'

export default default({
plugins: [
viteImagemin({
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 20,
},
pngquant: {
quality: [0.8, 0.9],
speed: 4,
},
svgo: {
plugins: [
{
name: 'removeViewBox',
},
{
name: 'removeEmptyAttrs',
active: false,
},
],
},
}),
]
})

压缩图片会影响构建速度,因此适可而止。

实现分析

优化处理

对于一些第三方模块,如 vuereact 等,其文件内容通常不会改变,如果使用 import 的形式进行网络请求,如果依赖过深,那么网络开销会非常大。此外前端并不识别 import Vue from 'vue' 此类路径,因此对于此类模块,需要进行优化处理,即用 ESbuild 进行预构建,将三方模块进行构建与合并,在模块请求时进行请求合并后的模块即可。这样,当浏览器解析到 import 路径时,会从该资源目录进行查找并返回。

客户端代码

在初始化时,通过会通过脚本的方式注入一些客户端需要的运行时代码,以支持一些功能,比如热热更新、CSS注入等。

// 热更新
const initWebSocket = (() => {
const host = new URL(import.meta.url).host
let ws = new WebSocket(`ws://${host}`)
ws.addEventListener('open', () => {}, { once: true })
ws.addEventListener('message', ({ data }) => {
handleMessage(JSON.parse(data))
})
})()

const handleMessage = (data) => {
switch (data.type) {
case 'connected':
console.log(`socket connected.`)
break
case 'update':
console.log(`hmr update.`)
import(`${data.file}?t=${Date.now()}`)
break
case 'full-reload':
console.log(`page reload.`)
window.location.reload()
break
}
}

// 样式注入
const updateStyle = (content) => {
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.textContent = content
document.head.append(style)
}
export { updateStyle }

开发服务器

在开发环境,Vite 主要启动开发服务器、进行文件转换、模块路径替换等处理。

Vite 底层使用 connect HTTP 框架作为开发器,类似于 Express,支持中间件,性能优秀。

将项目根目录及静态资源路径作为文件资源目录,以供访问时提供:

// 这里以 Koa 作为开发服务器
const app = new Koa()

app.use(
staticFiles(root + '/src/assets', {
hidden: true,
})
)

app.use(
staticFiles(root, {
hidden: true,
})
)

// listening
server.listen(port, hostName, () => {
console.log(`➜ Local: http://${hostName}:${port}/`)
createWebSockerServer(root, server)
})

// handle error
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`Port ${port} is in use, trying another one...`)
server.listen(++port, hostName)
}
})

HTML 处理

开发服务器启动后,会先请求资源目录下的 index.html,在收到请求后,对 HTML 进行重写后响应给客户端:

const content = await getContent(htmlContent)

ctx.body = (content) => {
return content.replace(
'</head>',
`<script type="module" src="${CLIENT_PATH}"></script>\n</head>`
)
}

JS 处理

对于 .js 文件,在获取到文件内容后,首先会找出文件中的三方模块,将其路径重写为优化处理后的文件路径,并响应给客户端。

import Vue from 'vue' 
// => import Vue from '/node_modules/.vite/deps/vue.js'
// => http://localhost:5173/node_modules/.vite/deps/vue.js

JSX 处理

对于 .jsx 文件,处理与 .js 文件类似,但在此之前,先要将 JSX 转换成 JS,然后进行路径重写并返回。

// 1. JSX -> JS
const { code } = await babel.transformAsync(content, {
plugins: [
[
(
await import('@babel/plugin-transform-react-jsx')
).default,
],
],
})

// 2. 路径重写,与 JS 处理一致
// 3. 响应

但由于浏览器并不识别 .jsx 为后缀的资源类型,因此在响应之前,还需设置 Content-Type,以告知浏览器响应其类型,使其可以被解析:

ctx.set('Content-Type', 'application/javascript')

CSS 处理

在进行模块化处理时,也需将 CSS 看作一个 JavaScript 模块,可以理解为通过 JavaScript 的方式使用 CSS,因此也许将其内容重写。

浏览器虽然可以识别 .css 为后缀的资源类型,但在重写后变成了 JavaScript 语法,因此也需要进行 Content-Type 设置:

ctx.set('Content-Type', 'application/javascript')

ctx.body = (content) => {
const code = [
`import { updateStyle } from "${CLIENT_PATH}"`,
`const css = ${JSON.stringify(content)}`,
`updateStyle(css)`,
].join('\n')
return code
}

执行后,会创建一个 style 标签,并插入样式内容,即可完成样式更新。

图片处理

图片的本质是一个路径,因此只需导出其路径即可。

import logo from './assets/logo.png'
// => import logo from '/src/assets/logo.png'
// => export default '/src/assets/logo.png'
// => <img src={logo} />
// => <img src={'http://localhost:5173/src/assets/logo.png'} />

vue 处理

同样,浏览器也不识别 .vue 后缀,因此也需要设置其 Content-Type'application/javascript'

首先,要将文件内容转换成 JavaScript,对于 Vue 模板的转换,可使用官方提供的 vue/compiler-sfc

import { parse as vueParse, compileTemplate } from 'vue/compiler-sfc'

const rewriteVueToJs = (content, url) => {
const vueName = 'main'

// 解析为 AST 树
const vueAst = vueParse(content)

let code = []

code = [
...code,
// 模板编译
compileTemplate({ source: vueAst.descriptor.template.content, id: 'app' })
.code,
// 热更新使用
vueAst.descriptor.script.content.replace(
'export default',
`const ${vueName} =`
),
`${vueName}.render = render`,
`${vueName}.__hmrId = "${getHash(url)}"`,
`__VUE_HMR_RUNTIME__.reload(${vueName}.__hmrId, ${vueName})`,
`export default ${vueName}`,
].join('\n')

return code
}

如果模板中有样式,则继续重写其路径,并使用 type=style 进行标记,使得浏览器再发一次请求,使用 CSS 的方式进行处理。

if (vueAst.descriptor.styles.length) {
code.push(`import "${url}?type=style&t=${Date.now()}"`)
}

最终进行三方路径重写并响应给客户端。

HMR

HMR 的本质是使用 WebSocket 进行模块的局部替换,因此使用 HMR 需要客户端和服务端共同参与。

对于客户端来说,需要在服务启动时进行 WebSocket 连接,如上的 initWebSocket 的客户端代码。

对于服务端来说,服务启动时需要进行模块收集,并在源码文件变化时,通过客户端以更新。

import path from 'node:path'
import chokidar from 'chokidar'
import { WebSocketServer } from 'ws'

const hotModules = new Map()

const createWebSockerServer = (root, server) => {
const wss = new WebSocketServer({
server,
})
wss.on('connection', (ws) => {
ws.send(JSON.stringify({ type: 'connected' }))
})
createHmr(root, wss)
}

const createHmr = (root, wss) => {
chokidar
.watch(path.join(root, 'src'), { ignoreInitial: true })
.on('change', (file) => {
if (hotModules.has(file)) {
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(
JSON.stringify({ type: 'update', file: hotModules.get(file) })
)
}
})
} else {
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(JSON.stringify({ type: 'full-reload' }))
}
})
}
})
}

export { createWebSockerServer, hotModules }

现阶段的 HMR,还需配合框架提供的热更新代码,只依赖 Vite 是无法实现的,如上的 Vue,而 React 就相对更加麻烦。

VoidZero

Vite 的问题

  1. 底层使用不一致。开发环境预处理和构建打包,分别使用 ESbuild 和 Rollup,并没有做到统一。还有各种第三方插件,尤其是针对向 Vue、React 这些框架的插件,其功能都是类似的,但是实现差别很大,也没有做到统一。
  2. 虽然 ESbuild 采用 Go 语言开发,但是其它模块基本上都是使用 JavaScript 进行开发的,性能不够好。

VoidZero

VoidZero 是尤雨溪在今年 2024年 8 月创办的一家公司,其目标是使用 Rust 重构 JavaScript 工具链,使其更快,更安全,对开发者更友好。

Rust 是近几年流行的编译型编程语言,利用 Rust 语言开发 JavaScript 工具性能会好很多。

目前正在开发 Rolldown,其目标是替代 ESbuild 和 Rollup,做到底层工具统一,最终将会作为 Vite 的底层依赖。

交互性

Rust 是如何与 Node 进行交互的呢 ?

Rust 编译后生成可执行文件,如 .exe.sh 等,这些文件 Node 并不识别,因此需要通过某种方式转成 .node 原生模块。这种情况通常采用第三方库,如 NAPI-RS,一个用 Rust 构建预编译的 Node.js 插件的框架。

将其转换成 .node 后,即可在 Node 调用并与 Rust 交互。