Vite
概述
前端工程痛点
- 模块化需求,不同的模块化类型,如ESM、CommonJS、UMD等。
- 兼容旧浏览器,编译高级语法,如
.ts、.jsx等。 - 开发效率,生产环境的代码执质量。
why
- Vite 基于浏览器原生 ESM 的支持实现模块加载。
- 开发环境基于原生 ES 模块,使用 ESbuild 依赖预构建,支持 HMR 模块热替换。
- 生产环境基于 Rollup 实现打包,更好的 Tree Shaking 优化。
- 上手简单,开箱即用。
双引擎架构
ESbuild
- 依赖预构建,作为打包工具。
- 单文件编译,可编译 TypeScript 和 JSX。
- 代码压缩,可作为压缩工具。
Rollup
- 生产环境打包器,CSS代码分割,预加载指令生成,异步加载优化等。
- 插件可与 Vite 插件兼容。
Webpack
Vite 与 Webpack 经常被进行比较,它们区别的体现在如下方面:
- 两者定位不一样。Webpack Core 是一个纯打包工具;而 Vite 是一个更上层的工具链(Webpack、Web常用配置、webpack-dev-server的结合)。
- 两者预设场景不一样。Webpack Core 只针对打包不预设场景,所以设计得及其灵活;而 Vite 缩窄预设场景来降低复杂度,预设了 Web 场景。
- 插件机制不一致。Webpack 的本质是先打包再加载,loader及插件机制跟打包的这个设计前提耦合过深;而 Vite 的插件机制是基于 Rollup 的,单个模块的解析及加载跟打包环节完全解耦。
依赖预构建
ESM 存在诸多问题,如第三方打包无法控制、可能会产生请求瀑布流的问题,而依赖预构建可以优化这种类似的问题。
构建过程
在预构建过程中,Vite 主要做了两件事:
- 使用 ESbuild 对第三方库进行打包,由于 ESbuild 使用 Go 语言编写,速度非常快,几乎是无感的。
- 重写依赖的 URL,如
react可能会重写为/node_modules/.vite/deps/react.js,浏览器则自动请求此构建后的文件。
依赖搜索
Vite 在搜索依赖时,会按照一定的策略和顺序进行检索:
- 优先查找预构建缓存。
- 若果没有找到缓存,则自动寻找引入的依赖项。
- 服务启动后,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 的预构建过程也大体上也是这样,只是在这基础上做了更多的事情。
自定义行为
如果说不想针对某些第三方依赖进行预构建,可将其排除:
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 类名处理成哈希值,避免同名的情况下样式污染的问题。
- App.tsx
- main.module.scss
- vite.config.ts
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
.bg-red {
background-color: red;
}
import { defineConfig } from 'vite'
export default defineConfig({
css: {
modules: {
generateScopedName: '[name]_[local]_[hash:base64:5]'
// ...
}
},
})
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 插件的形式配置:
- vite.config.ts
- tailwind.config.ts
- tailwind.css
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 type { Config } from 'tailwindcss'
export default {
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}'
],
theme: {
extend: {}
},
plugins: []
} as Config
@tailwind base;
@tailwind components;
@tailwind utilities;
静态资源
别名
别名可让模块路径重定向到具体的路径,可简化模块的导入。
- vite.config.ts
- tsconfig.json
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
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
},
}
图片
解析为URL
以下几种方式的图片资源解析后会返回 URL:
- 以模块的形式导入图片。
import logo from './assets/logo.png'
- 在 CSS 中以
url()的方式使用图片资源。
.logo {
background: url('./assets/logo.png');
}
- JSX 或 Vue SFC 模板中的资源引用都将自动转换为导入。
- 普通的动态变量路径不会自动转换导入。
动态导入
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 文件,一般不推荐使用。
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 文件等,可采用以下几种方式解决。
- 将此种资源进行模块声明,方便 TypeScript 识别。
declare module '*.md' {
const str: string
export default str
}
- 显示 URL 引入,即解析后返回具体的 URL。
import md from './readme.md?url'
- 将资源引入为字符串,即将原内容输出。
import md from './readme.md?raw'
- 扩展内部列表。
export default defineConfig({
assetsInclude: [".md"]
})
public 目录
public 目录位于项目的根目录,该目录的资源在开发时通过 / 根路径访问,生产环境会被完整复制到目标目录的根目录下。
以下资源文件可考虑放到 public 目录:
- 不会被源码引入的静态文件。
- 必须保持原有文件名的文件。
- 不想引入该资源,只想得到其 URL。
内联资源
静态资源可以有两种构建方式,即内联和外链。
Vite 中,默认对体积小于 4KB 的资源使用内联方式,即将其作为 base64 格式的字符串内联。
对于比较小的资源,适合内联到代码中,一方面对代码体积的影响很小,另一方面可以减少不必要的网络请求,优化网络性能。
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() { }
}
}
执行顺序
- 服务启动阶段
- 请求阶段。
- 热更新阶段。
- 服务关闭阶段。
插件顺序
- Alias(路径别名)相关插件。
- 带有
enforce: 'pre'的用户插件。 - Vite 核心插件(解析、编译等)。
- 没有
enforce值的用户插件。 - Vite 生产环境构建使用的插件。
- 带有
enforce: 'post'的用户插件。 - 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
预加载
- 使用
preload和modulepreload进行资源预加载。 - 使用
prefetch可让浏览器在空闲时间进行资源加载。
DNS预解析
dns-prefetch 与 preconnect 搭配使用,进行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,
},
],
},
}),
]
})
压缩图片会影响构建速度,因此适可而止。
实现分析
优化处理
对于一些第三方模块,如 vue、react 等,其文件内容通常不会改变,如果使用 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 的问题
- 底层使用不一致。开发环境预处理和构建打包,分别使用 ESbuild 和 Rollup,并没有做到统一。还有各种第三方插件,尤其是针对向 Vue、React 这些框架的插件,其功能都是类似的,但是实现差别很大,也没有做到统一。
- 虽然 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 交互。