NocoBase RunJS 模块导入完全指南:ctx.libs 内置库、importAsync/requireAsync 动态加载与 ESM CDN 配置
【免费下载链接】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 区块、JS 字段、JS 操作等场景的 JavaScript 执行环境,支持顶层await、ctx上下文 API、容器内渲染与模块导入。本文聚焦 RunJS 的模块导入体系:如何使用零成本的内置库(ctx.libs)、如何通过ctx.importAsync()/ctx.requireAsync()按需加载 ESM 与 UMD/AMD 第三方模块,以及如何通过环境变量切换默认 CDN(如自建 esm.sh 服务或 jsDelivr)。读完本文,你将能够在 RunJS 中安全、高效地引入任意第三方库,并理解其底层 URL 解析与模块加载原理。
一、总览:RunJS 的两类模块
RunJS 中可使用的模块分为两类,使用方式完全不同:
| 类别 | 访问方式 | 是否需要 import |
|---|---|---|
| 内置模块 | 通过ctx.libs直接使用 | 不需要 |
| 外部模块 | ctx.importAsync()(ESM)或ctx.requireAsync()(UMD/AMD)按需加载 | 需要异步加载 |
内置模块零成本、开箱即用;外部模块则按 URL 动态加载,覆盖任意第三方库场景。这一设计在 RunJS 概述 中也被列为 RunJS 的四大核心能力之一(顶层异步、导入外部模块、容器内渲染、全局变量)。
二、内置模块:ctx.libs(无需 import)
RunJS 内置了常用库,可直接通过ctx.libs访问,无需import或异步加载:
| 属性 | 说明 |
|---|---|
| ctx.libs.React | React 本体,用于 JSX 与 Hooks |
| ctx.libs.ReactDOM | ReactDOM(如需 createRoot 等可配合使用) |
| ctx.libs.antd | Ant Design 组件库 |
| ctx.libs.antdIcons | Ant Design 图标 |
| ctx.libs.dayjs | 日期时间处理库(dayjs) |
| ctx.libs.lodash | Lodash 工具库 |
| ctx.libs.math | Math.js:数学表达式、矩阵运算等 |
| ctx.libs.formula | Formula.js:类 Excel 公式(SUM、AVERAGE 等) |
源码依据:内置库的注册与懒加载
从源码看,ctx.libs并非一次性加载全部库,而是通过注册表 + 懒加载 + 缓存机制实现的。在 packages/core/flow-engine/src/runjsLibs.ts 中:
DEFAULT_RUNJS_LIBS数组声明了全部默认内置库,其中React、ReactDOM、antd、dayjs为context级缓存(从当前 RunJS 上下文取值),而antdIcons、lodash、formula、math为global级缓存(使用import('lodash')、import('@formulajs/formulajs')、import('mathjs')等动态导入);ctx.libs的每个属性通过 getter首次访问时才真正解析(resolveRegisteredLibSync),并把结果物化为可写数据属性;- 任何库的解析结果会缓存在
__runjsLibResolvedCache(global 级)或按上下文缓存的 Map 中,避免重复加载。
也就是说,ctx.libs.math只有在你的代码真正访问它的那一刻才会被加载,未使用到的内置库不会产生额外开销。
示例:React 与 antd
const { Button } = ctx.libs.antd; ctx.render(<Button>点击</Button>);示例:ctx.libs.math
const result = ctx.libs.math.evaluate('2 + 3 * 4'); // result === 14mathjs的evaluate支持任意合法数学表达式,包括函数调用与常量,例如ctx.libs.math.evaluate('round(sqrt(16), 2)')。
示例:ctx.libs.formula
const values = [1, 2, 3, 4]; const sum = ctx.libs.formula.SUM(values); const avg = ctx.libs.formula.AVERAGE(values);formula提供类 Excel 的公式集合(SUM、AVERAGE、IF、VLOOKUP 等),适合在 JS 字段中做表格风格的数据计算。
提示:在 RunJS 编辑器中,
ctx.libs下各库的属性和典型用法(如ctx.libs.lodash.get(obj, 'a.b')、ctx.libs.formula.SUM(1, 2, 3))均有代码补全提示,实现见 packages/core/flow-engine/src/runjs-context/contexts/base.ts。
三、外部模块:按需加载第三方库
需要第三方库时,根据模块格式选择加载方式:
- ESM 模块→ 使用
ctx.importAsync() - UMD/AMD 模块→ 使用
ctx.requireAsync()
两者的核心区别在于模块格式与解析方式:importAsync使用浏览器原生 dynamic import 加载真正的 ESM 产物;requireAsync则借助 NocoBase 前端已有的 requirejs(AMD)加载 UMD/AMD 或全局脚本。
注意:若库同时提供 ESM 版本,优先使用
ctx.importAsync(),以获得更好的模块语义与 Tree-shaking 支持。
四、导入 ESM 模块:ctx.importAsync()
使用ctx.importAsync()按 URL 动态加载 ESM 模块,适用于 JS 区块、JS 字段、JS 操作等场景。
importAsync<T = any>(url: string): Promise<T>;参数说明:
- url:ESM 模块地址。支持简写格式
<包名>@<版本>或带子路径<包名>@<版本>/<文件路径>(如vue@3.4.0、lodash@4/lodash.js),会按配置拼接 CDN 前缀;也支持完整 URL(http:///https://开头,原样使用)。 - 返回:解析后的模块命名空间对象。若模块只有
default一个导出,会直接返回default值,无需再写.default(见下方源码分析)。
源码依据:importAsync 的实现链路
ctx.importAsync定义在 packages/core/flow-engine/src/flowContext.ts:
this.defineMethod('importAsync', async function (this: any, url: string) { // 判断是否为 CSS 文件(支持 example.css?v=123 等形式) if (isCssFile(url)) { return this.loadCSS(url); } return await runjsImportModule(this, url, { importer: runjsImportAsync }); });这里有两个值得注意的设计:
- CSS 直通:如果 URL 是
.css文件(支持 query/hash,如style.css?v=123),importAsync会转而调用loadCSS注入<link rel="stylesheet">,而不是做 JS 导入——判断逻辑见 packages/core/flow-engine/src/utils/resolveModuleUrl.ts 中的isCssFile()。 - 归一化导出:
runjsImportModule在拿到模块对象后调用normalizeModule()——许多经由 esm.sh / esbuild 转换的模块会把主导出挂在default上,若模块只有default一个导出键,则直接返回default,提升易用性。
runjsImportModule的完整实现位于 packages/core/flow-engine/src/utils/runjsModuleLoader.ts,它还负责:
- antd 特判重写:当简写为
antd@x.y.z(不带子路径)时,会自动追加bundle=1查询参数(将依赖内联,解决 antd 在 esm.sh 上命名导出缺失问题),并在检测到外部 React 已加载时追加deps=react@版本,react-dom@版本,避免同一页面出现多个 React 实例; - 全局缓存:以解析后的完整 URL 为 key,缓存在
globalThis.__nocobaseImportAsyncCache,同一 URL 只加载一次; - 内置库覆盖:当导入
react、react-dom/client、antd、@ant-design/icons时,会自动覆盖ctx.libs.React、ctx.libs.ReactDOM、ctx.libs.antd、ctx.libs.antdIcons(及顶层ctx.React等别名),保证ctx.render使用的 React 与后续导入的库版本一致(setRunJSLibOverride,见 packages/core/flow-engine/src/runjsLibs.ts)。
测试 packages/core/flow-engine/src/tests/runjsExternalLibs.test.ts 验证了上述行为,例如:
await ctx.importAsync('react@18.2.0'); // runjsImportAsync 被调用为 https://esm.sh/react@18.2.0 与 https://esm.sh/react-dom@18.2.0/client // 且 ctx.React / ctx.libs.React / ctx.ReactDOM / ctx.libs.ReactDOM 均被覆盖为外部导入的实例 await ctx.importAsync('antd@5.29.3'); // 实际请求 https://esm.sh/antd@5.29.3?bundle=1动态导入的兼容性处理
runjsImportAsync(runjsModuleLoader.ts)解决了 RunJS 与 NocoBase 前端 AMD 体系(requirejs)共存时的一个典型问题:
- 许多 UMD/CJS 库(如 lodash)在运行时探测
define.amd,若存在则优先走 AMD 分支,导致 esm.sh 等 CDN 的“CJS/UMD → ESM 包装”无法从module.exports提取导出,最终 ESM 导出为undefined; - 因此
importAsync在真正执行import()的瞬间会临时屏蔽define.amd(通过属性描述符恢复原状),并先用<link rel="modulepreload">预取模块、等待 requirejs 空闲,尽量缩小全局副作用窗口; - 导入语句带有
@vite-ignore/webpackIgnore: true标记以避免被打包器重写,若仍被拦截,再用eval('u => import(u)')兜底。
这些都属于 best-effort 处理:即使内部结构变化或浏览器能力差异导致某一步失败,也不会阻断正常流程。
默认为 https://esm.sh
未配置时,简写形式会使用https://esm.sh作为 CDN 前缀。例如:
const Vue = await ctx.importAsync('vue@3.4.0'); // 等价于从 https://esm.sh/vue@3.4.0 加载自建 esm.sh 服务 / 自定义 CDN
若需内网或自建 CDN,可部署兼容 esm.sh 协议的服务,并通过环境变量指定:
- ESM_CDN_BASE_URL:ESM CDN 基础地址(默认
https://esm.sh) - ESM_CDN_SUFFIX:可选后缀(如 jsDelivr 的
/+esm)
环境变量如何生效
构建时这两个环境变量会被注入为浏览器全局变量,实现见 packages/core/app/client-v2/rsbuild.config.ts:
window['__esm_cdn_base_url__'] = window['__esm_cdn_base_url__'] || process.env.ESM_CDN_BASE_URL || 'https://esm.sh'; window['__esm_cdn_suffix__'] = window['__esm_cdn_suffix__'] || process.env.ESM_CDN_SUFFIX || '';而resolveModuleUrl(resolveModuleUrl.ts)在运行时读取window.__esm_cdn_base_url__与window.__esm_cdn_suffix__完成拼接:
// 相对路径会被拼接上 CDN 前缀和后缀(默认添加后缀) resolveModuleUrl('vue@3.4.0') // => 'https://esm.sh/vue@3.4.0' // 如果使用 jsdelivr,需要配置 ESM_CDN_SUFFIX='/+esm' // resolveModuleUrl('vue@3.4.0') => 'https://cdn.jsdelivr.net/npm/vue@3.4.0/+esm' // 不添加后缀(适用于 UMD 库或 CSS 文件) resolveModuleUrl('vue@3.4.0', { addSuffix: false }) // => 'https://esm.sh/vue@3.4.0' // 原始 URL(适用于 UMD 库) resolveModuleUrl('lodash@4.17.21/lodash.js', { raw: true }) // => 'https://esm.sh/lodash@4.17.21/lodash.js?raw' // 完整 URL 保持不变 resolveModuleUrl('https://cdn.jsdelivr.net/npm/vue@3.4.0') // => 'https://cdn.jsdelivr.net/npm/vue@3.4.0'切换到 jsDelivr 的配置示例
以 jsDelivr 的 ESM 服务为例(其路径格式为https://cdn.jsdelivr.net/npm/<包名>@<版本>/<文件>/+esm):
# 构建/启动 NocoBase 前端时注入 export ESM_CDN_BASE_URL='https://cdn.jsdelivr.net/npm' export ESM_CDN_SUFFIX='/+esm'配置后,await ctx.importAsync('vue@3.4.0')将被解析为https://cdn.jsdelivr.net/npm/vue@3.4.0/+esm。
自建兼容 esm.sh 协议的服务可参考官方开源的 esm-server 项目(搜索 "nocobase esm-server" 即可找到仓库)。
五、导入 UMD/AMD 模块:ctx.requireAsync()
使用ctx.requireAsync()按 URL 异步加载 UMD/AMD 或挂载到全局的脚本。
requireAsync<T = any>(url: string): Promise<T>;- url:支持两种形式:
- 简写路径:
<包名>@<版本>/<文件路径>,与ctx.importAsync()相同,会按当前 ESM CDN 配置解析;解析时会加上?raw,直接请求该路径的原始文件(多为 UMD 构建)。例如echarts@5/dist/echarts.min.js实际请求https://esm.sh/echarts@5/dist/echarts.min.js?raw(当默认使用 esm.sh 时)。 - 完整 URL:任意 CDN 的完整地址(如
https://cdn.jsdelivr.net/npm/xxx)。
- 简写路径:
- 返回:加载后的库对象(具体形式取决于该库的导出方式)
加载后,许多 UMD 库会挂到全局对象(如window.xxx),使用时按该库文档即可。
源码依据:requireAsync 的实现
ctx.requireAsync定义在 packages/core/flow-engine/src/flowContext.ts:
this.defineMethod('requireAsync', async (url: string) => { // 判断是否为 CSS 文件(支持 example.css?v=123 等形式) if (isCssFile(url)) { return this.loadCSS(url); } const u = resolveModuleUrl(url, { raw: true }); return await runjsRequireAsync(this.requirejs, u); });与importAsync相同,CSS 文件同样会走loadCSS分支。非 CSS 时调用resolveModuleUrl(url, { raw: true })解析出...?raw的原始文件地址,再交给runjsRequireAsync(runjsModuleLoader.ts)通过 requirejs 加载:
requirejs([url], (mod) => resolve(mod), reject);runjsRequireAsync与runjsImportAsync共用同一把全局串行锁(withRunjsModuleLoadLock),避免并发加载期间对全局对象(如define.amd)的临时改动互相干扰。
示例
// 简写路径(经 esm.sh 解析为 ...?raw) const echarts = await ctx.requireAsync('echarts@5/dist/echarts.min.js'); // 完整 URL const dayjs = await ctx.requireAsync('https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js');说明:
?raw模式返回的是 CDN 上的原始构建文件,后缀配置(如/+esm)不会追加,这一点与 ESM 导入不同——raw: true时resolveModuleUrl直接拼接?raw并跳过addSuffix逻辑。
六、选择建议与最佳实践
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| React、antd、dayjs、lodash、mathjs、formulajs 等内置库 | ctx.libs.xxx | 零加载成本,有类型/补全提示 |
| 有 ESM 产物的第三方库 | ctx.importAsync() | 更好的模块语义与 Tree-shaking,导出归一化 |
| 仅提供 UMD/AMD 或全局脚本的库(如部分图表库、老式插件) | ctx.requireAsync() | 与 NocoBase 前端 requirejs 体系一致 |
| 样式文件 | importAsync/requireAsync传.cssURL | 自动走loadCSS注入<link> |
实践要点:
- 优先
ctx.libs:内置库已经过验证并与 RunJS 环境(React 渲染、antd 主题)深度集成,非必要不重复加载外部版本; - 版本对齐:当
importAsync导入外部react时,运行时会自动顺带加载匹配的react-dom@<同版本>/client并覆盖ctx.libs.ReactDOM(见runjsImportModule的 override 逻辑),保证ctx.render渲染一致;若导入 antd 而ctx.libs.React仍是内置版本,运行时会给出[RunJS Hint]提示建议同时导入外部 React 与 antd; - 复用缓存:
importAsync对同一 URL 有全局缓存(__nocobaseImportAsyncCache),重复调用不会重复请求网络; - 避免多次 React 实例:若渲染组件时出现 "Invalid hook call" 或
Cannot read properties of null (reading 'useState')类错误,通常是多个 React 实例共存导致,可先await ctx.importAsync('react@<与报错栈一致的版本>')再读取ctx.libs.React/ 调用 hooks——运行时会基于错误栈自动生成修复提示。
七、进阶阅读
模块导入是 RunJS 上下文能力的一部分,与之紧密相关的还有:
- RunJS 概述:顶层 await、容器内渲染、全局变量等整体能力
- JSX 渲染:如何在 RunJS 中编写 JSX
- 容器渲染:
ctx.render()的三种渲染形式 - 上下文 API:
ctx完整方法说明 - Window 全局:浏览器全局对象的使用
若想深入源码,可从 packages/core/flow-engine/src/utils/runjsModuleLoader.ts(加载器核心)、packages/core/flow-engine/src/utils/resolveModuleUrl.ts(URL 解析)、packages/core/flow-engine/src/runjsLibs.ts(内置库注册与 override)以及 packages/core/flow-engine/src/tests/runjsExternalLibs.test.ts(外部库行为测试)入手。
【免费下载链接】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),仅供参考