拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Formily Vue ExpressionScope 组件详解:为 JSON Schema 表达式注入局部作用域

Formily Vue ExpressionScope 组件详解为 JSON Schema 表达式注入局部作用域【免费下载链接】formily Cross Device High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3项目地址: https://gitcode.com/gh_mirrors/fo/formily导读在 Formily Vueformily/vue中JSON Schema 字符串表达式如{{$values.xxx}}依赖运行时编译其求值上下文称为表达式作用域Expression Scope。SchemaField通过scope属性提供全局作用域但当你需要在自定义组件内部为子树中的 JSON Schema 表达式注入一套局部作用域时就需要用到ExpressionScope组件。本文基于 expression-scope.md 文档结合formily/vue源码、示例与测试深入讲解该组件的签名、用法、作用域合并原理及其与SchemaField、RecursionField的协作机制帮助你彻底掌握在 Vue 2 / Vue 3 项目中定制 JSON Schema 表达式求值上下文的方法。组件定位给 JSON Schema 表达式传递局部作用域ExpressionScope是formily/vue提供的一个内置组件官方描述如下用于自定义组件内部给 json-schema 表达式传递局部作用域它的核心价值在于当你在SchemaField内部注册了自定义组件通过x-component引用而该组件的子树中又包含其他 JSON Schema 字段节点时这些子节点的表达式默认只能访问SchemaField传入的全局scope。ExpressionScope允许自定义组件在渲染子树时向下层字段额外注入一份局部作用域与全局作用域按规则合并后一起参与表达式编译。典型应用场景包括封装带子字段的复合组件如布局容器、组合输入、在组件内部给模板字符串、x-component-props、x-reactions等表达式提供组件私有上下文变量。类型签名与 PropsExpressionScope是 Vue 组件类型官方签名如下interface IExpressionScopeProps { value?: any } type ExpressionScope Vue.Componentany, any, any, IExpressionScopePropsvalue要注入的局部作用域对象通常是一个普通对象如{ $innerScope: ... }可以是任意类型any。组件本身不渲染任何 DOM仅作为作用域上下文的分发节点存在源码中最终渲染为Fragment见下文。对应的类型定义位于 types/index.tsexport type IExpressionScopeProps { value: any }说明文档签名中value标注为可选value?实现层则以props: [value]声明。源码合并逻辑对无效undefined/null的value会直接跳过因此即使不传value也不会报错此时它等效于一个透传节点。标准用法在自定义组件内部包裹子树ExpressionScope通常不出现在业务模板中而是被自定义组件内部使用。最标准的写法是自定义组件在setup中返回ExpressionScope包裹的插槽内容并通过props.value传入局部作用域。完整可运行示例见 expression-scope.vuetemplate FormProvider :formform SchemaField :scope{ $outerScope: outer scope value } SchemaVoidField x-componentContainer SchemaVoidField namediv x-componentText :x-component-props{ text: {{$innerScope $outerScope}} } / /SchemaVoidField /SchemaField /FormProvider /template script import { defineComponent } from vue/composition-api import { createForm } from formily/core import { FormProvider, h, createSchemaField, ExpressionScope, } from formily/vue const Container defineComponent({ setup(_props, { slots }) { return () h( ExpressionScope, { props: { value: { $innerScope: inner scope value } }, }, slots ) }, }) const Text defineComponent({ props: [text], setup(props) { return () h(div, {}, { default: () props.text }) }, }) const SchemaField createSchemaField({ components: { Container, Text }, }) export default { components: { FormProvider, ...SchemaField }, data() { return { Text, form: createForm(), } }, } /script运行效果页面最终渲染为inner scope value outer scope value即子字段Text的text表达式{{$innerScope $outerScope}}同时拿到了$innerScope—— 由Container组件内部ExpressionScope注入的局部作用域$outerScope—— 由SchemaField的scope属性注入的全局作用域。关键注意点ExpressionScope的value在h调用中需放在props对象内h(ExpressionScope, { props: { value: {...} } }, slots)这是 Vue 2composition-api与 Vue 3 统一的 props 传递写法。被包裹的子字段必须仍然是 Schema 字段节点如SchemaVoidField才能触发 JSON Schema 表达式编译普通 DOM 上的{{}}不会被 Formily 编译。局部作用域对该组件插槽内的所有后代 Schema 字段生效形成作用域链支持多层嵌套见下文。作用域合并原理lazyMerge inject/provide要真正理解ExpressionScope需要看它的完整实现 ExpressionScope.tsimport { lazyMerge } from formily/shared import { computed, defineComponent, inject, provide, Ref } from vue-demi import { SchemaExpressionScopeSymbol, Fragment, h } from ../shared import { IExpressionScopeProps } from ../types export const ExpressionScope defineComponent({ name: ExpressionScope, props: [value], setup(props: IExpressionScopeProps, { slots }) { const scopeRef injectRef(SchemaExpressionScopeSymbol) const expressionScopeRef computed(() lazyMerge(scopeRef.value, props.value) ) provide(SchemaExpressionScopeSymbol, expressionScopeRef) return () h(Fragment, {}, slots) }, })实现要点读取上级作用域通过inject(SchemaExpressionScopeSymbol)拿到当前最近的表达式作用域引用。该 Symbol 定义在 shared/context.tsexport const SchemaExpressionScopeSymbol: InjectionKey RefRecordstring, any Symbol(schemaExpression)合并生成新作用域computed(() lazyMerge(scopeRef.value, props.value))—— 将上级作用域与本次传入的局部作用域合并得到一份新的只读响应式作用域。lazyMerge来自 shared/src/merge.ts其特性包括若源值为无效值undefined 等则直接返回目标值支持函数类型的延迟取值lazy函数会在真正求值时才执行合并过程中同名字段由局部作用域覆盖上级作用域。向下传递provide(SchemaExpressionScopeSymbol, expressionScopeRef)将合并结果作为新的作用域提供给子树。不渲染任何实体返回h(Fragment, {}, slots)只透传插槽不产生额外 DOM 节点。因此ExpressionScope本质上是作用域链上的一个合并节点离字段越近的 ExpressionScope其value优先级越高同一层级内后合并的局部变量会覆盖先存在的同名变量。与 SchemaField / RecursionField 的协作机制起点SchemaField 注入全局 scopeSchemaField是作用域链的源头。在 SchemaField.ts 中全局作用域由两部分合并而来const scopeRef computed(() lazyMerge(options.scope, props.scope)) // ... provide(SchemaExpressionScopeSymbol, scopeRef)即createSchemaField({ scope })工厂选项中的scope与SchemaField :scope...属性上的scope会先合并再通过SchemaExpressionScopeSymbol注入到整个 Schema 树中。求值RecursionField 把作用域并入字段 props当RecursionField递归渲染每个字段时见 RecursionField.ts会把当前作用域再次合并进schema.toFieldProps的编译上下文const getPropsFromSchema (schema: Schema) schema?.toFieldProps?.({ ...optionsRef.value, get scope() { return lazyMerge(optionsRef.value.scope, scopeRef.value) }, })这样每个字段在编译x-component-props、x-reactions、title等表达式时都能拿到工厂 scope SchemaField scope 沿途各层 ExpressionScope 注入的局部 scope的完整合并结果。作用域链示意图FormProvider └── SchemaField :scope{ $outerScope: outer scope value } ← 注入全局 scope └── Container自定义组件 └── ExpressionScope :value{ $innerScope: inner scope value } └── SchemaVoidField子字段 ← 表达式可同时访问两个变量 └── Text :text{{$innerScope $outerScope}}在 JSON Schema 侧的表现从formily/json-schema的角度看所有字符串表达式{{...}}都是在编译阶段用当前作用域求值的。编译入口见 compiler.tscompile(expression: string, scope {}) { // ... 通过 Registry 使用 scope 执行表达式 }因此ExpressionScope注入的变量会出现在该编译scope中供模板字符串、x-component-props、x-reactions的when/target/fulfill等所有表达式引用。这一点在 transformer.ts 的getBaseScope与lazyMerge(baseScope, ...)逻辑中也能看到反应式reaction计算同样会拼接作用域链。测试用例验证仓库为ExpressionScope提供了专门的单元测试 expression.scope.spec.ts其核心断言如下test(expression scope, async () { // Container 组件内部包裹 ExpressionScope注入 { $innerScope: inner scope value } // SchemaField 注入 { $outerScope: outer scope value } // 子字段表达式{{$innerScope $outerScope}} expect(getByTestId(test-input).textContent).toBe( inner scope valueouter scope value ) })测试使用testing-library/vue渲染完整表单最终断言$innerScope局部与$outerScope全局在子字段表达式中同时可用。这从测试维度确认了全局 scope 与局部 scope 合并求值的行为契约。相关上下文SchemaExpressionScopeContext如果你需要在自定义组件中读取当前最近的表达式作用域而非注入新作用域可以直接使用注入键SchemaExpressionScopeSymbol。官方在 injections.md 中将其封装为SchemaExpressionScopeContextconst SchemaExpressionScopeContext injectany(SchemaExpressionScopeSymbol)它在 shared/context.ts 中导出可用于调试、派生自定义渲染逻辑等场景。注意直接inject拿到的是RefRecordstring, any类型的响应式引用读取时需访问.value。实战建议与常见问题何时该用 ExpressionScope而不是直接在 SchemaField 上传 scope若作用域对整棵 Schema 树生效优先使用SchemaField的scope属性若作用域只对某个自定义组件内部的子树生效例如组件私有状态、上下文变量且不希望污染外部字段就用ExpressionScope在组件内部包裹插槽。同名变量的覆盖规则局部作用域ExpressionScope的value会覆盖全局作用域中的同名变量多层嵌套时内层覆盖外层。利用这一规则可以为不同子树定制同名但取值不同的表达式变量。Vue 2 兼容性formily/vue基于vue-demi构建ExpressionScope同时兼容 Vue 2配合vue/composition-api与 Vue 3示例代码即为 Vue 2 写法。value支持响应式数据expressionScopeRef是computed的结果当props.value变化时注入给子树的作用域会随之更新进而触发依赖该变量的表达式重新编译求值配合 Formily 的响应式体系。小结ExpressionScope是 Formily Vue 中连接自定义组件与JSON Schema 表达式求值的桥梁组件它以零 DOM 开销的方式在组件内部为子树注入局部作用域并通过lazyMerge与全局作用域SchemaField的scope合并最终在RecursionField递归编译字段时生效。掌握它你就能在封装复合表单组件时为组件内部字段的模板字符串、组件 props 与联动规则提供独立、可控且响应式的表达式上下文。【免费下载链接】formily Cross Device High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3项目地址: https://gitcode.com/gh_mirrors/fo/formily创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门