Builder.io 异步下拉框插件详解:url 模板化、mapper 映射与依赖联动机制
【免费下载链接】builderVisual Development for React, Vue, Svelte, Qwik, and more项目地址: https://gitcode.com/GitHub_Trending/bu/builder
本文以 Builder.io 官方插件仓库中的plugins/async-dropdown模块为主体,系统讲解这个"异步下拉框(dynamic-dropdown)"编辑器的完整用法与底层实现。读完本文,你将掌握:如何在withBuilder中配置dynamic-dropdown类型的输入字段(url、mapper、expectMultipleDropdowns等参数),URL 模板变量是如何从应用上下文中解析并替换的,mapper字符串函数在插件侧的执行方式,以及该插件如何根据依赖字段(dependencyComponentVariables)的变化自动重新拉取数据并清理已选值。
插件定位:把"数据从哪来"交给代码方决定
README 开篇说明了插件的设计思想:
The idea behind the plugin is to generalize the use of a dropdown so the code provider will tell the plugin how to process the received data. (这个插件的思路是:将下拉框的使用泛化,由代码提供方告诉插件如何处理收到的数据。)
也就是说,下拉框选项并不写死在 Builder 可视化编辑器里,而是由你的应用侧提供两个"契约":
url:选项数据从哪个接口获取(GET 请求);mapper:接口返回的 JSON 如何被转换成下拉框可识别的选项结构。
驱动整个插件的 React 组件是 Dropdown 组件,而真正向 Builder 编辑器注册dynamic-dropdown字段类型的入口在 components/index.tsx:
Builder.registerEditor({ name: 'dynamic-dropdown', component: Component, });这就是为什么你在withBuilder配置中要写type: "dynamic-dropdown"—— 该字符串必须与编辑器注册名一致。
在生产代码中使用插件
第一步:把插件加入组织
按 README 的指引,先到 Builder 账户的 Organization 页面,在 Plugins 区域添加@builder.io/plugin-dynamic-dropdown。需要留意的是,仓库 package.json 中该包的实际名称是@builder.io/plugin-async-dropdown(当前版本2.0.2),两者命名存在历史差异;接入时以组织插件市场里实际列出的可安装项为准。
第二步:在 withBuilder 中声明 dynamic-dropdown 输入
README 给出的完整配置范式如下,这是本插件最重要的"契约":
withBuilder(Component, { name: "Component", inputs: [ { name: "dropdown", type: "dynamic-dropdown", options: { url: "https://www.example.com/{{version}}/{{endpoint}}?pathParam={{pathValue}}", mapper: `({data}) => data.reduce((state, property) => { return { ...state, [property.key]: property.options.map((option) => ({name: option.key, value: option.value})) } }, {})`, expectMultipleDropdowns: true }, {...} ] });各参数含义(结合 README 说明与源码实现补充):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url | string | 必填 | 选项数据接口的地址,支持模板变量占位符;缺失时插件直接抛错 |
mapper | string | 必填 | 一段箭头函数源码字符串,接收{ data }(即urlGET 请求的 JSON 响应),返回维度对象 |
expectMultipleDropdowns | boolean | false | 为true时按 mapper 返回的每个维度渲染一个下拉框,字段值为对象;为false时只取第一个维度,字段值为单个 value |
dependencyComponentVariables | string[] | 可选(源码扩展) | 声明依赖的其他字段 key,其取值变化会触发重新请求并清空已选值 |
disableClear | boolean | 可选(源码扩展) | 为true时禁止用户清空已选值 |
url与mapper都是必填项,这一点在 dropdownPropsExtractor.ts 中有硬性校验:
const { url, mapper, dependencyComponentVariables } = props.field.options || ({} as any); if (isNullOrEmpty(url)) throw new Error('Missing { url: "" } required option'); if (isNullOrEmpty(mapper)) throw new Error('Missing { mapper: "" } required option');url 的模板化:变量从哪来
README 指出:"urlargument will be templated with handlebars. The plugin is smart enough to figure out the handlebars values from the application context to replace them."(url参数会用模板语法做插值,插件会自行从应用上下文中找出这些值并替换。)
从源码看,模板渲染由 Mustache 完成(mustache是 package.json 中的正式依赖),其"占位符名 → 取值"的视图(view)由 dropdownPropsExtractor.ts 的getView组装,数据源有三处:
- 当前内容模型的全部字段值:
props.context.designerState.editingContentModel.data.toJSON(),也就是说 URL 中的{{version}}、{{endpoint}}、{{pathValue}}会优先匹配你正在编辑的内容模型字段; - targeting 定向变量:
props.targeting,来自内容模型查询条件(见 components/index.tsx 中把 query 逐项摊平为{ property: value }的逻辑); - 依赖组件变量:
dependencyComponentVariables中列出的每个 key,会从props.object.get(key)取值后注入视图(见 dropdownPropsExtractor.ts)。
此外,renderTemplate还会在渲染前调用validateTemplateVariables做变量校验:解析出模板中所有占位符,任何一个在当前视图中解析为 falsy 值,就会抛出Tokens {{xxx}} not replaced错误——这意味着模板变量取不到值时不会静默发出带{{...}}的坏请求,而是提前失败并在控制台暴露问题。
mapper:在插件侧执行的字符串函数
README 对mapper的定义是:"a string method that will be executed on the side of the plugin given the answer from theurlGET call. It has to be an arrow function that will receive a{ data }object coming from the url request."(一个字符串形式的箭头函数,在插件侧执行,接收来自 URL 请求的{ data }对象。)
它的返回值签名是固定契约——一个"维度名 → 选项数组"的对象:
{ dimension1: [ {name: "A_NAME", value: "VALUE1"}, {name: "ANOTHER_NAME", value: "VALUE2"}, ], dimension2: [ {name: "NAME1", value: "X"}, ] }"object 的每个维度都会生成一个新的下拉框"。选项本身的结构对应源码中的 IOption 接口:{ name: string; value: any },其中name展示给用户,value是真正写回 Builder 字段内容的值。
mapper 字符串的执行由 mapperEvaluator.ts 中的safeEvaluate完成:
const safeEvaluate = (code: string, context: any = {}) => { let result = null; try { const fn = new Function(`return ${code}`)(); result = fn(context); } catch (e) { console.error('safeEvaluate error: ', e); } return result; };即通过new Function把 mapper 字符串编译为函数,再以其入参{ data }调用;执行异常会被捕获并打印到控制台,data则是urlGET 请求解析后的 JSON 响应。
expectMultipleDropdowns:单值与多值的字段语义差异
这是 README 中明确强调的行为开关:
- 开启时(
true):插件返回值为各维度的键值对象,例如{dimension1: "VALUE1", dimension2: "X"}; - 未开启时(默认
false):返回值为不带 key 的裸值,例如"VALUE1"。
源码中的分支位于 components/index.tsx:expectMultipleDropdowns为真渲染MultipleDropdowns,否则渲染SingleDropdown。
- SingleDropdown.tsx 只取 mapper 结果
Object.keys(options)[0](第一个维度)的选项,onSelectChange直接把裸值传给props.onChange(selectedValue); - MultipleDropdowns.tsx 维护一个
{ [dimension]: selectedValue }状态,任一维度变化都把整个对象传给props.onChange(newSelections)。
这一行为差异有对应测试佐证:tests/dynamicDropdown.test.tsx 中,return only value when expectMultipleDropdowns is disabled断言onChange收到的是字符串'aValue1';而updates state with values from all dropdowns断言收到的是累积对象{ oneDimension: 'anotherValue1', anotherDimension: 'anotherValue2' }。
底层数据流:请求、缓存与依赖联动
把上面几个环节串起来,插件的完整调用链是:
Component (index.tsx) ├─ getDependenciesKeyFrom(props) → 依赖值拼成的 key,作为 useEffect 的依赖 └─ SingleDropdown / MultipleDropdowns └─ useEffect([props.newDependenciesKey]) └─ orchestrateSelections(props) (selectionsOrchestrator.ts) ├─ getMassagedProps(props) 校验 url/mapper → Mustache 渲染 url ├─ executeGet(url) fetch + response.json() ├─ safeEvaluate(mapper, { data }) └─ selectionsCache(Map,key = `${url}-${mapper}`)值得注意的几个实现细节:
- 内存级缓存。selectionsOrchestrator.ts 用一个模块级
Map以url + mapper作为缓存键——同一渲染会话内,URL 未变化就不会重复发请求。这也解释了为什么dependencyComponentVariables要体现在 URL 中才能触发新请求:依赖值变化 → 模板渲染出不同的 URL → 缓存 miss → 重新 fetch。 - 请求是裸 fetch。selectionsClient.ts 的实现就是
fetch(url)后response.json(),因此被请求的接口必须允许浏览器跨域(CORS)且返回 JSON。 - 空结果与异常的兜底。
orchestrateSelections在 mapper 返回 falsy 或抛错时会返回{};此时 SingleDropdown / MultipleDropdowns 会渲染NothingToSelect占位提示,而不是空白下拉框。 - 依赖联动与选中值清理。getDependenciesKeyFrom 把
dependencyComponentVariables各字段的当前取值用-拼成字符串;渲染组件把该 key 放进useEffect依赖数组([props.newDependenciesKey]),并通过dependenciesKeyRef与上一轮的 key 比较(dependenciesHelper.ts 的haveDependenciesChanged)。一旦依赖值变化,旧选项被重新拉取,同时调用cleanupSelections()执行props.onChange(null)清空已选值——因为旧选项集对应的 value 在新数据里可能已不存在。 - disableClear。canDisableClear.tsx 读取
field.options.disableClear,透传给 Dropdown 组件 的canClearValue判断:仅当未禁用清空且事件为 click 时才允许onSelectChange(null, dimension)清空该维度。
本地开发与调试
README 的 "Developing on top of this plugin?" 部分给出了本地开发流程(注意:README 中写的目录名plugins/dynamic-dropdown是旧称,实际目录为plugins/async-dropdown):
# Install(在本仓库中) cd plugins/async-dropdown npm install # Develop npm start从 package.json 看,npm start实际执行的是cross-env SERVE=true && webpack-dev-server --mode development(基于 webpack.config.js 的开发服务器),npm run build则先tsc --module commonjs再webpack --mode production,产物输出到dist/下的 system / es5 两种模块格式(对应main/module字段)。
把开发中的 Builder 指向本地插件:
- 在你的组织插件设置里添加
http://localhost:1268/builder-plugin-dynamic-dropdown.system.js到插件列表(README 原文如此;注意产物文件名以当前 package.json 的dist/builder-plugin-async-dropdown.system.js为准,接入时以实际构建产物的系统模块文件名与加载地址为准); - 开发完成后记得把它替换回生产环境链接;
- 因为 Builder 是 https 站点,加载 http:// 内容时浏览器会给出警告,需要按 README 的提示在浏览器中允许"加载不安全脚本"(load unsafe scripts);
- 每次改动后重启 Builder 编辑器即可看到插件最新版本;卸载插件则从组织插件设置中移除即可。
测试覆盖点速览
tests目录下的四个测试文件覆盖了 README 承诺的核心行为,可作为行为验证依据:
- dynamicDropdown.test.tsx:单/多维度渲染分支、
value → name的回显映射、expectMultipleDropdowns开/关时的onChange载荷形态(裸值 vs 对象)、依赖值变化时重新渲染与onChange(null)清理、disableClear开/关时清空行为,以及无选项或请求失败时渲染NOTHING_TO_SELECT占位; - dropdownPropsExtractor.test.ts:url/mapper 缺失报错与模板变量替换;
- mapperEvaluator.test.ts:字符串函数求值与异常兜底;
- selectionsOrchestrator.test.ts:请求编排与缓存逻辑。
运行测试可执行(见 package.json 脚本):
cd plugins/async-dropdown npm test小结
Builder.io 的async-dropdown插件用"URL 模板 + mapper 字符串函数"这一对契约,把下拉框选项的数据来源与加工逻辑从可视化编辑器中剥离出来交给应用侧:url负责声明式地引用当前内容模型、targeting 与依赖字段的值,mapper负责把任意 JSON 整形为维度 → [{name, value}]。expectMultipleDropdowns决定字段是存一个值还是一个键值对象;dependencyComponentVariables则让下拉框能随其他字段联动刷新并自动失效旧选择。理解了这套机制后,你就可以在任意 Builder 自定义组件上接入来自后端接口、且随编辑上下文动态变化的下拉选项。
【免费下载链接】builderVisual Development for React, Vue, Svelte, Qwik, and more项目地址: https://gitcode.com/GitHub_Trending/bu/builder
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考