NocoBase RunJS ctx.on() 事件订阅指南:字段双向绑定、资源刷新监听与监听器生命周期管理
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
RunJS 是 NocoBase 中用于JS 区块(JSBlock)、JS 字段(JSField / JSEditableField)、JS 操作(JSAction)等场景的 JavaScript 执行环境,代码运行在受限沙箱中并通过统一的ctx上下文访问页面能力(参见 RunJS 概述)。ctx.on()是其中负责订阅上下文事件的核心 API:通过它可以在字段值被外部修改时同步更新 UI、响应资源刷新/保存等生命周期事件,从而把自定义 JS 代码无缝接入表单联动、数据刷新等 NocoBase 既有机制。读完本文,你将掌握ctx.on()的类型定义、事件分发规则、与ctx.off()的配对清理模式,以及js-field:value-change、resource:refresh等事件的底层实现与完整实战写法。
ctx.on() 的定位与适用场景
在 RunJS 脚本中,ctx.on()用于订阅"上下文事件"——例如字段值变化、属性变化、资源刷新等。事件会根据名称前缀被映射到两种不同的通道:
- 以
resource:为前缀的事件 → 走ctx.resource的内部事件总线; - 其余事件 → 通常映射为
ctx.element上的自定义 DOM 事件(CustomEvent)。
ctx.on()的典型适用场景如下:
| 场景 | 说明 |
|---|---|
| JSField / JSEditableField | 监听字段值从外部(表单、联动等)变更时同步更新 UI,实现双向绑定 |
| JSBlock / JSItem / JSColumn | 监听容器上的自定义事件,响应数据或状态变化 |
| resource 相关 | 监听资源刷新、保存等生命周期事件,在数据更新后执行逻辑 |
类型定义
on(eventName: string, handler: (event?: any) => void): void;- eventName:事件名称字符串。以
resource:开头的名称走资源事件总线,其余名称走ctx.element上的 DOM 事件(若容器存在)。 - handler:事件回调。DOM 事件通道下,参数为事件对象,新值通过
ev.detail携带。
常见事件一览
| 事件名 | 说明 | 事件来源 |
|---|---|---|
js-field:value-change | 字段值被外部修改(如表单联动、默认值更新) | ctx.element上的 CustomEvent,ev.detail为新值 |
resource:refresh | 资源数据已刷新 | ctx.resource事件总线 |
resource:saved | 资源保存完成 | ctx.resource事件总线 |
事件最终映射规则:以
resource:为前缀的走ctx.resource.on,其余通常走ctx.element上的 DOM 事件(若存在)。
实战示例
字段双向绑定(React useEffect + 清理)
在 JS 字段中渲染自定义 UI 时,通常需要把本地 React state 与表单字段值保持同步。以下代码在挂载时订阅js-field:value-change,并在卸载时通过 cleanup 移除监听:
React.useEffect(() => { const handler = (ev) => setValue(ev?.detail ?? ''); ctx.on?.('js-field:value-change', handler); return () => { ctx.off?.('js-field:value-change', handler); }; }, []);这里ev.detail即外部写入的新字段值;回调中把它同步到本地setValue,实现"外部值变化 → UI 更新"的单向数据流。配合ctx.setValue/ctx.getValue(参见 ctx.setValue())即可构成完整的双向绑定。
原生 DOM 监听(ctx.on 不可用时的替代)
不同 context 类型提供的能力不同,ctx.on可能不存在。此时可直接使用ctx.element的原生addEventListener:
// 当 ctx.on 未提供时,可直接使用 ctx.element const handler = (ev) => { if (selectEl) selectEl.value = String(ev?.detail ?? ''); }; ctx.element?.addEventListener('js-field:value-change', handler); // 清理时:ctx.element?.removeEventListener('js-field:value-change', handler);资源刷新后更新 UI
对于依赖资源数据的场景,可以订阅ctx.resource的事件总线,在数据刷新后重新读取数据并渲染:
ctx.resource?.on('refresh', () => { const data = ctx.resource?.getData?.(); // 根据 data 更新渲染 });从源码看,这一模式在 NocoBase 内置模型中被广泛使用。例如 JSItemActionModel.tsx 中即存在resource.on('refresh', handler)与resource.off('refresh', handler)的配对订阅写法,可作为自定义脚本的参照实现。
源码级原理:js-field:value-change 是如何产生的
ctx.on之所以能监听到字段值变化,是因为渲染层在值变更时向容器派发了标准的 DOM CustomEvent。以可编辑 JS 字段为例,其实现位于 JSEditableFieldModel.tsx:
- 事件派发:在
JSFormRuntime组件中,useEffect监听value变化并派发事件:
useEffect(() => { if (!containerRef.current || !scriptCode) return; const event = new CustomEvent('js-field:value-change', { detail: value }); containerRef.current.dispatchEvent(event); }, [value, scriptCode]);这里detail携带的正是最新字段值,与文档中"ev.detail为新值"的约定完全对应(见 JSEditableFieldModel.tsx)。
容器与 element 暴露:脚本真正运行前,
runJs步骤的 handler 通过ctx.onRefReady等待容器引用就绪,再以ctx.defineProperty('element', ...)暴露容器——且返回值包裹在ElementProxy中(带 XSS 保护),并通过动态 getter 绑定ref.current,避免容器变更后失效(见 JSEditableFieldModel.tsx)。默认模板即使用该事件:
JSEditableFieldModel的默认脚本模板本身就演示了"监听js-field:value-change→ 同步本地 state →onChange时回写ctx.setValue"的完整闭环(见 JSEditableFieldModel.tsx):
React.useEffect(() => { const handler = (ev) => setValue(ev?.detail ?? ''); ctx.element?.addEventListener('js-field:value-change', handler); return () => ctx.element?.removeEventListener('js-field:value-change', handler); }, []);- 测试验证:该行为有对应的单元测试覆盖,见 JSEditableFieldModel.test.tsx 中的
EDITABLE_CODE用例——它直接以ctx.element?.addEventListener('js-field:value-change', handler)作为被测脚本,验证外部值变化能够驱动 UI 同步。
只读字段(JSFieldModel)中的事件语义
对于只读形态的 JS 字段(如表格列、详情项),模型实现位于 JSFieldModel.tsx:它渲染一个占位<span>容器,当props.value变化时通过useHooksBeforeRender重新执行jsSettings脚本刷新内容(见 JSFieldModel.tsx)。由于容器在只读场景同样存在,js-field:value-change的 DOM 监听模式依旧可用;而ctx.setValue这类写能力则仅在带表单绑定的上下文中提供。
与 ctx.off 的配合
ctx.on注册的监听必须成对管理,具体规则(参见 ctx.off()):
- 使用
ctx.on注册的监听,应在适当时机通过ctx.off移除,避免内存泄漏或重复触发; - 在 React 中,通常在
useEffect的 cleanup 函数中调用ctx.off; ctx.off可能不存在,使用时建议加可选链:ctx.off?.('eventName', handler)。
资源事件总线同样遵循这一模式:
const handler = () => { /* ... */ }; ctx.resource?.on('refresh', handler); // 适当时机 ctx.resource?.off('refresh', handler);注意事项
- 配对取消:每次
ctx.on(eventName, handler)都应有对应的ctx.off(eventName, handler),且传入的handler引用必须一致——不同的函数引用无法正确移除监听。 - 生命周期:在组件卸载或 context 销毁前移除监听,否则可能导致内存泄漏。从源码结构看,NocoBase 的字段模型在挂载时会做首次执行的幂等保护(如
JSFieldModel中的_mountedOnce标记,见 JSFieldModel.tsx),但自定义脚本中的监听清理责任仍在脚本作者一方。 - 事件可用性:不同 context 类型支持的事件不同,具体以各组件文档为准;
ctx.on缺失时请退回到ctx.element的原生addEventListener。 - 可写能力按需判断:
js-field:value-change只负责"通知值变化",写入操作应通过ctx.setValue?.(value)完成,且仅在带表单绑定的上下文中可用(详见 ctx.setValue() 的注意事项)。
相关文档
- ctx.off() - 移除事件监听
- ctx.element - 渲染容器与 DOM 事件
- ctx.resource - 资源实例及其
on/off - ctx.setValue() - 设置字段值(会触发
js-field:value-change)
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考