Vite 中的热更新
HMR PAI
HMR 可能已经在特定于框架的启动器模板中处理过了
Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API。
- 必需的条件守卫
if (import.meta.hot) { // HMR 代码 } - hot.accept(cb)
要接收模块自身,应使用 import.meta.hot.accept,参数为接收已更新模块的回调函数:
export const count = 1 if (import.meta.hot) { import.meta.hot.accept((newModule) => { if (newModule) { // newModule is undefined when SyntaxError happened console.log('updated: count is now ', newModule.count) } }) }
HMR API
-
hot.accept hot.accept(cb)
要接收模块自身时使用, 参数为接收已更新模块的回调函数 接受” 热更新的模块被认为是 HMR 边界。hot.accept(deps,cb)
模块也可以接受直接依赖项的更新, 而无需重新加载自身 -
hot.dispose(cb)
一个接收自身的模块或者一个期望被其他模块接收的模式可以使用 *hot.dispose* 来清除任何由其更新副本产生的持久副作用 -
hot.prune(cb)
注册一个回调,当模块在页面上不再被导入时调用 -
hot.data
import.meta.hot.data 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。 -
hot.decline
目前是一个空操作并暂留用于向后兼容; 指明某模块是不可热更新的,请使用 *hot.invalidate()* -
hot.invalidate(message?: string)
调用该方法 HMR 服务将使调用方的导入失效; 建议在 accept 回调中调用 invalidate -
hot.on(event, cb)
监听自定义 HMR 事件。 以下 HMR 事件由 Vite 自动触发:
'vite:beforeUpdate' 当更新即将被应用时(例如,一个模块将被替换)
'vite:afterUpdate' 当更新已经被应用时(例如,一个模块已被替换)
'vite:beforeFullReload' 当完整的重载即将发生时
'vite:beforePrune' 当不再需要的模块即将被剔除时
'vite:invalidate' 当使用 import.meta.hot.invalidate() 使一个模块失效时
'vite:error' 当发生错误时(例如,语法错误)
-
hot.send(event, data)
发送自定义事件到 Vite 开发服务器
例子:
// renderA.js
let timer;
export function render() {
timer = setInterval(() => {
index++;
document.querySelector('#app').innerHTML = `
<div>
<h1>Hello Vite!</h1>
<a href="https://cn.vitejs.dev/guide/api-hmr.html" target="_blank">Document${index</a>
</div>
`
}, 1000);
}
let index = import.meta.hot.data?.cache?.getIndex?.() || 0;
// 帮助保存在同一个模块中热更新之前保存的状态, 直接用到更新之后的模块中
if (import.meta.hot) {
import.meta.hot.data.cache = {
getIndex() {
return index;
}
}
import.meta.hot.dispose(() => {
// 清理副作用
if (timer) {
clearInterval(timer);
}
})
// 表示此模块不可热更新
// import.meta.hot.decline();
}
export { index };
// main.js
import { render } from './renderA';
import './style.css';
render();
if (import.meta.hot) {
import.meta.hot.accept(['./renderA'], ([newA]) => {
if (newA.index > 15) {
import.meta.hot.invalidate()
} else {
newA?.render();
}
})
import.meta.hot.accept();
}
预编译优化
CommonJS 和 UMD 兼容性
CommonJS to ESM
开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM
性能
Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能
在vite.config.js 中使用optimizeDeps选项进行 ¶依赖优化选项
optimizeDeps: {
entries: string | string[];
exclude: string[]; // 在预构建中强制排除的依赖项
include: string[]; // 强制预构建链接的包
esbuildOptions: EsbuildBuildOptions; // 在部署扫描和优化过程中传递给 esbuild 的选项 1. 忽略了 external 选项,请使用 Vite 的 optimizeDeps.exclude 选项 2. plugins 与 Vite 的 dep 插件合并
force: boolean; // 设置为 true 可以强制依赖预构建, 而忽略之前已经缓存过的、已经优化过的依赖。
}
注意: 依赖预构建仅会在开发模式下应用,并会使用 esbuild 将依赖转为 ESM 模块。在生产构建中则会使用 @rollup/plugin-commonjs。
服务端集成
在非node环境中集成Vite
集成Vite项目:
-
Vite 配置中配置入口文件和启动创建manifest:
// vite.config.js export default defineConfig({ build: { manifest: true } }) -
在开发环境中, 在服务器的HTML模版中注入以下内容:
<!-- 如果是在开发环境中 --> <script type="module" src="http://localhost:3000/@vite/client"></script> <script type="module" src="http://localhost:3000/main.js"></script>⚠️: 还要确保服务器配置为提供Vite工作目录中的静态资源, 否则图片等资源将无法正确加载
-
在生产环境中, 在运行yarn build之后, 会生成menifest。json文件将与静态文件一同生成, 在模版语言中将生成的文件引入进去
⚠️注意: 需要将引入文件目录映射到生成的静态文件目录下 例如: pug模版语言
// pug 文件: index.pug(一种模版语言) html head title= title link(href=css rel="stylesheet") body h1= message div(id="app") script(src=vendor) script(src=index)// server.js const express = require('express') const app = express() const manifest = require('./dist/manifest.json') app.set("view engine", 'pug') app.use(express.static("dist")) // 将引入文件地址映射到dist静态文件目录中 app.get('/', (req, res) => { res.render("index", { title: 'key', message: 'Hello there', index: manifest['index.html'].file, vendor: manifest['index.html'].dynamicImports.vendor, css: manifest['index.html'].css[0] }) })
node环境中集成Vite
服务端SSR: 一个典型的SSR 应用应该由如下的源文件结构
- index.html
- src
- main.js # 导出环境无关的(通用的)应用代码
- entry.client.js // 将应用挂载到一个 DOM 元素上
- entry.server.js // 使用某框架的 SSR API 渲染该应用
index.html 将需要引用 entry-client.js 并包含一个占位标记供给服务端渲染时注入
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>
情景逻辑:
- 设置开发服务器
// server.js
import express from 'express';
import fs from 'fs';
import { createServer as createViteServer } from 'vite';
const app = express();
// 以中间件模式创建 Vite 应用,这将禁用 Vite 自身的 HTML 服务逻辑
// 并让上级服务器接管控制
createViteServer({
server: {
// middlewareMode: 'html'
middlewareMode: 'ssr' // 对于请求的管理会转交给自己的服务
}
}).then(vite => {
app.use(vite.middlewares) // 使用 vite 的 Connect 实例作为中间件
// 对于所有的请求通过以下方式渲染
app.get('*', async (req, res) => {
try {
// 1. 读取 index.html
let template = fs.readFileSync('index.html', 'utf-8') // 读取内容
// 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端,
// 同时也会从 Vite 插件应用 HTML 转换。
template = await vite.transformIndexHtml(req.url, template)
// 3. 加载服务器入口。vite.ssrLoadModule 将自动转换
// 你的 ESM 源码使之可以在 Node.js 中运行!无需打包
// 并提供类似 HMR 的根据情况随时失效。
const { render } = await vite.ssrLoadModule('/src/server-entry.jsx')
// 4. 渲染应用的 HTML。这假设 entry-server.js 导出的 `render`
// 函数调用了适当的 SSR 框架 API。
const html = await render(req.url);
// 5. 注入渲染后的应用程序 HTML 到模板中。
const responseHtml = template.replace("<!-- APP_HTML -->", html)
// 6. 返回渲染后的 HTML。
res.status(200).set('content-type', 'text/html').end(responseHtml)
} catch (error) {
// 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回
// 你的实际源码中。
vite.ssrFixStacktrace(error)
res.status(500).end(error.message)
}
})
app.listen(5001, () => {
console.log('http://localhost:5001');
}); // 监听端口
});
-
生产环境构建
- 生成一个客户端构建
- 再生成一个SSR构建, 使其通过import()直接加载, 这样便无需再使用Vite的ssrLoadModule
- package.json
{ "scripts": { "dev": "node server", "build:client": "vite build --outDir dist/client", "build:server": "vite build --outDir dist/server --ssr src/entry-server.js" } }Note:使用 --ssr 标志 表明这将会是一个SSR构建, 同时需要指定SSR 的入口
import express from 'express'; import fs from 'fs'; const app = express(); const template = fs.readFileSync('dist/client/index.html', 'utf-8') const isProd = process.env.NODE_ENV === 'production'; app.get('*', async (req, res) => { const { render } = (await import('./dist/server/server-entry.js')) const context = {} const html = await render(req.url, context) if (context.url) { res.redirect(301, context.url) } const responseHtml = template.replace('<!-- APP_HTML -->', html) res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHtml) }) app.listen(4002, () => { console.log('http://localhost:4002') })注意:可以通过 process.env.NODE_ENV 条件分支,需要添加一些用于生产环境的特定逻辑
-
生产预加载构建
vite build 支持使用 --ssrManifest 标志, 会生成一份ssr-manifest.json
"build:client": "vite build --outDir dist/client --ssrManifest" // 生成dist/client/ssr-manifest.json