插件生命周期钩子
插件生命周期钩子用于执行构建时副作用、修改 HTML、写入最终部署文件或定制底层构建器。
生命周期钩子所需的状态放在 setup() 中,只返回真正需要这些状态的钩子。
插件需要增加模块,或把代码挂到页面和入口时,请使用生成代码。声明式生成比通过钩子写临时文件更容易检查与组合。
生命周期概览
emitIR() 与 emitPageIR() 用于声明生成代码,并且必须保持确定性。它们在配置和插件
设置解析完成后、setup() 之前执行,evjs 可能多次调用。如果其中任一方法失败,
setup() 不会执行。
由于生成阶段早于 setup(),生成式贡献不能读取其中初始化的状态。共享不可变输入应放在
插件选项或源码文件中;网络、进程、监听器等需要清理的状态应留在 setup() 返回的钩子内。
configure() 和 setup() 见插件开发。通过 definePlugin() 创建的插件以 ctx.options 获得类型化应用选项。
| 钩子 | 用途 |
|---|---|
configureBundler(config, ctx) | 适配器专属 Loader、解析、优化或其他底层设置 |
clientDevMiddleware({ signal }) | 在静态资源和 HTML fallback 前拦截客户端开发请求 |
devServerReady({ origin, signal }) | 客户端监听可用后连接开发工具 |
beforeBuild(ctx) | 在输出完成前,启动依赖最新打包结果的工作 |
transformOutput(output, ctx) | 调整资源组或增加部署元信息 |
transformHtml(document, ctx) | 修改一份生成 HTML 或请求时文档外壳 |
afterBuild(result) | 输出平台文件或报告已完成构建 |
dispose(ctx) | 释放 setup() 或开发阶段钩子创建的资源 |
在 setup() 中保存状态
长期资源只创建一次,并在 dispose() 中关闭:
import { definePlugin } from "@evjs/ev/plugin";
export const reporter = definePlugin({
id: "reporter",
setup(ctx) {
const client = createReporter(ctx.options);
return {
afterBuild({ deploymentMetadata, isRebuild }) {
client.record({ deploymentMetadata, isRebuild });
},
async dispose() {
await client.close();
},
};
},
});
每次成功执行 setup() 后,dispose() 最多执行一次,并按插件逆序运行。即使初始化只完成了一部分,清理逻辑也应保持安全。
监听插件输入
setup()、emitIR() 和 configureBundler() 上下文提供 addWatchFile(),用于影响插件行为的项目本地文件:
setup(ctx) {
ctx.addWatchFile("./config/analytics.json");
}
监听输入变化时,evjs 会按需刷新开发环境。生成代码依赖的数据应在 emitIR() 中读取,使代码随输入变化。不要监听生成的 .ev 或 dist 文件。
开发服务器就绪
外部工具需要真实客户端来源时使用 devServerReady():
setup() {
let closeTools: (() => Promise<void>) | undefined;
return {
async devServerReady({ origin, signal }) {
const tools = await connectDevTools({ origin, signal });
closeTools = () => tools.close();
},
async dispose() {
await closeTools?.();
},
};
}
origin是活动构建器报告的监听地址。- 开发环境开始关闭时,
signal会触发中止。 - 请传递或监听该信号,让异步工作及时结束。
- 此钩子仅在开发中运行,不代表第一份应用产物或服务端运行时已经就绪。
依赖产物的工作请放在 afterBuild()。
客户端开发中间件
clientDevMiddleware() 按稳定的插件顺序,在客户端开发服务器 fallback 前注册 Node 请求/响应中间件:
setup() {
return {
clientDevMiddleware({ signal: sessionSignal }) {
return async (request, response, next, { origin, signal }) => {
if (!request.url?.startsWith("/__plugin/")) return next();
if (signal.aborted || sessionSignal.aborted) return;
response.setHeader("content-type", "application/json");
response.end(JSON.stringify({ origin }));
};
},
};
}
该钩子可以返回一个中间件、数组或 undefined。需要在下游处理后继续工作时应 await next();向 next() 传错或直接抛错会生成适配器诊断响应。请求上下文提供真实公共 origin 和不可变开发 Session 的中止信号。WebSocket upgrade 会绕过插件中间件并透明转发给构建器。内置 Utoopack 和 Webpack 适配器均支持此能力;选择未声明 dev.clientMiddleware 的适配器会在监听前失败。
构建与重新构建
只有打包生成有效输出周期时才执行 beforeBuild() 与 afterBuild();prepare 和 inspect 不会调用。
开发环境中:
- 首次成功输出使用
isRebuild: false; - 后续成功输出周期使用
isRebuild: true; - 失败周期不调用
afterBuild()。
afterBuild() 在框架文件写入完成后运行。该钩子抛错仍会让生产构建失败,因此适合生成必需产物;可选上报失败应由插件自行处理。
转换构建产物
transformOutput() 可以调整已连接的资源组内容,并增加插件部署元信息。部署元信息必须是可无损序列化的普通 JSON。
不要用输出钩子重命名页面、路由、文档、运行时路径或框架输出目录。这些选择属于应用配置、页面配置或声明式生成内容。
转换 HTML
transformHtml() 接收一份解析后的 HtmlDocument 及其上下文:
transformHtml(document, ctx) {
document.head?.appendChild(
document.createComment(` build ${ctx.buildId} `),
);
if (ctx.owner.kind === "page") {
document.documentElement?.setAttribute(
"data-page",
ctx.owner.pageId,
);
}
}
常用上下文字段包括:
documentId、applicationId、fileName与template;- 标识应用、页面或插件文档的
owner; assets、buildId与publicPath;- 供高级检查使用的当前输出。
应根据 owner.kind 分支,不要从文件名猜所有权。文档类型从公共插件入口导入:
import type { HtmlDocument } from "@evjs/ev/plugin";
添加简单的 meta、link、script 或 style 时,应优先使用生成代码中的声明式 html.tag 扩展槽位。
使用最终构建结果
afterBuild() 为常见部署工作提供聚焦值:
setup() {
return {
afterBuild({ deploymentMetadata, frameworkRuntime, isRebuild }) {
writePlatformManifest({
assets: deploymentMetadata.assets,
routes: deploymentMetadata.routes,
server: deploymentMetadata.server,
runtime: frameworkRuntime,
isRebuild,
});
},
};
}
路由、文档、资源和服务端入口优先使用 deploymentMetadata。只有插件确实需要部署投影中没有的构建时资源细节时,才使用更宽的 output。
配置构建器
definePlugin() 默认与构建器无关。需要修改底层类型化配置时,请使用适配器辅助函数;每个辅助函数只针对自己的适配器执行。
Utoopack 示例:
import { merge, utoopack } from "@evjs/bundler-utoopack";
import { definePlugin } from "@evjs/ev/plugin";
export const yamlPlugin = definePlugin({
id: "yaml-support",
setup() {
return {
configureBundler: utoopack((config) => {
merge(config, {
module: {
rules: {
".yaml": { type: "json" },
},
},
});
}),
};
},
});
Webpack 示例:
import { webpack } from "@evjs/bundler-webpack";
configureBundler: webpack((configs) => {
for (const config of configs) {
config.resolve ??= {};
config.resolve.alias ??= {};
config.resolve.alias["@app"] = "./src";
}
});
构建器钩子可以修改受支持的底层设置,但不能替换框架页面入口或客户端/服务端输出目录。需要改变启动组合时,请使用声明式生成内容。
添加终端快捷键
交互快捷键由插件描述对象声明,不属于生命周期钩子:
const tools = definePlugin({
id: "tools",
cliShortcuts() {
return [
{
key: "u",
description: "show dev url",
action(session) {
console.log(session.origin);
},
},
];
},
});
key 必须是单个非空白字符。action 可以读取当前客户端 origin,并通过 close() 结束完整的 ev dev 进程。应用侧控制见本地开发。
小型完整示例见插件实践。