Skip to content

ast-schema Schema 编辑

页面结构编辑插件,以 JSON 代码形式直接编辑页面的 DSL(Domain Specific Language) Schema,适合需要精确控制页面结构的场景。

插件功能

  • 基于 Monaco Editor 的 JSON 编辑器,支持语法高亮、自动补全和错误提示
  • 实时同步画布变更,订阅 schemaChange 事件自动更新编辑器内容
  • 保存时自动校验 JSON 格式,解析异常时阻止保存并提示错误
  • 禁止修改 component 字段(修改 component 等同于修改物料类型)

页面 DSL 结构

页面 DSL 是一棵以 Page 为根节点的组件树,用 JSON 描述页面的完整结构,包括组件层级、状态、数据源、云函数等。

顶层结构(RootNode)

json
{
  "id": "page_001",
  "props": {},
  "state": {},
  "cloudFunction": {},
  "dataSource": {},
  "children": []
}
字段类型说明
idstring页面唯一标识
propsobject页面级属性
stateobject页面响应式状态,可在组件中通过 this.state.xxx 访问
cloudFunctionobject云函数定义,可在组件中通过 this.cloudFuns.xxx() 调用
dataSourceobject数据源配置,可在组件中通过 this.dataSource.xxx 访问
childrenarray子组件节点数组

组件节点结构(NodeSchema)

每个组件节点描述一个 UI 元素,递归嵌套形成组件树:

json
{
  "id": "abc12345",
  "component": "Button",
  "props": { "type": "primary" },
  "children": "点击按钮",
  "invisible": false,
  "slot": "default",
  "directives": [],
  "events": []
}
字段类型说明
idstring节点唯一标识(8 位随机字符)
componentstring组件名称,必须在可用组件列表中
propsobject组件属性,键值对形式,值支持静态值或 JSExpression
childrenstring | JSExpression | NodeSchema[]子内容:纯文本、表达式或子组件数组
invisibleboolean | JSExpression是否隐藏,类似 Vue 的 v-if / v-show
slotstring插槽名称,不指定时默认为 "default"
directivesNodeDirective[]指令列表,用于 vFor 循环渲染等
eventsNodeEvent[]事件绑定列表,通过工作流引擎处理

JSExpression 表达式

DSL 中的动态值统一使用 JSExpression 格式表示,类似于 Vue 模板中的 插值:

json
{ "type": "JSExpression", "value": "this.state.count" }
字段类型说明
typestring固定为 "JSExpression"
valuestringJavaScript 表达式字符串
idstring可选,表达式唯一标识

数据访问路径

访问方式说明示例
this.state.xxx页面状态this.state.userName
this.context.itemvFor 循环当前项this.context.item.name
this.context.indexvFor 循环当前索引this.context.index
this.dataSource.xxx数据源this.dataSource.userList
this.cloudFuns.xxx()云函数调用this.cloudFuns.fetchData()

使用场景

JSExpression 可用于以下位置:

  • props 属性值{ "type": "JSExpression", "value": "this.state.title" }
  • children 文本{ "type": "JSExpression", "value": "'¥' + this.state.price" }
  • invisible 条件{ "type": "JSExpression", "value": "!this.state.isLoggedIn" }
  • directives 值{ "type": "JSExpression", "value": "this.state.products" }
  • events handler{ "type": "JSExpression", "value": "this.__workflow__.execute('node-xxx', {...})" }

指令系统(Directives)

vFor 循环渲染

vFor 指令用于列表循环渲染,等价于 Vue 的 v-for

json
{
  "id": "product_item",
  "component": "View",
  "props": {
    "key": { "type": "JSExpression", "value": "this.context.item.id" }
  },
  "directives": [
    {
      "id": "vfor_001",
      "name": "vFor",
      "value": { "type": "JSExpression", "value": "this.state.products" },
      "iterator": { "item": "item", "index": "index" }
    }
  ],
  "children": [
    {
      "id": "product_name",
      "component": "Text",
      "children": { "type": "JSExpression", "value": "this.context.item.name" }
    }
  ]
}
字段类型说明
idstring指令唯一标识
namestring指令名称,循环渲染为 "vFor"
valueJSExpression循环数据源,指向 this.state 中的数组
iteratorobject迭代变量命名:item 为当前项变量名,index 为索引变量名

要点

  • 循环子节点中通过 this.context.item 访问当前项,this.context.index 访问当前索引
  • 循环项必须设置 key 属性,推荐使用 this.context.item.id
  • iterator 的变量名可自定义,如 { "item": "product", "index": "prodIndex" }

嵌套 vFor

多层循环时,每层使用不同的 iterator 变量名,内层可访问所有外层 context:

json
{
  "directives": [
    {
      "id": "vfor_category",
      "name": "vFor",
      "value": { "type": "JSExpression", "value": "this.state.categories" },
      "iterator": { "item": "category", "index": "catIndex" }
    }
  ],
  "children": [
    {
      "id": "product_item",
      "component": "View",
      "directives": [
        {
          "id": "vfor_product",
          "name": "vFor",
          "value": { "type": "JSExpression", "value": "this.context.category.products" },
          "iterator": { "item": "product", "index": "prodIndex" }
        }
      ],
      "children": [
        {
          "component": "Text",
          "children": { "type": "JSExpression", "value": "this.context.product.name" }
        }
      ]
    }
  ]
}

事件绑定(Events)

事件绑定通过工作流引擎(Workflow)处理,使用 events 字段配置:

json
{
  "id": "section_header",
  "component": "View",
  "events": [
    {
      "id": "evt_click_001",
      "name": "onClick",
      "handler": {
        "type": "JSExpression",
        "value": "this.__workflow__.execute('node-xxx', { eventType: 'click', eventData: event })"
      }
    }
  ]
}
字段类型说明
idstring事件唯一标识
namestring事件名称,如 "onClick""onInput"
handlerJSExpression事件处理器,调用工作流引擎的 execute 方法

handler 参数说明

  • 第一个参数:工作流节点 ID,对应 NodeComponent 节点的 id
  • 第二个参数:事件数据对象,包含 eventType(事件类型)和 eventData(原生事件对象)

常用事件名clickdblclickinputchangesubmitscroll

插槽(Slots)

子节点通过 slot 字段指定渲染到父组件的哪个插槽位置:

json
{
  "id": "input_with_icon",
  "component": "UniEasyinput",
  "props": { "placeholder": "请输入搜索内容" },
  "children": [
    {
      "id": "prefix_icon",
      "component": "Icon",
      "slot": "prefixIcon",
      "props": { "name": "search" }
    },
    {
      "id": "suffix_text",
      "component": "Text",
      "slot": "suffixIcon",
      "children": "搜索"
    }
  ]
}
  • 不指定 slot 时默认渲染到 "default" 插槽
  • 可用的插槽名称由物料 Meta 中的 configure.slots 定义

完整示例

以下是一个完整的 旅游推荐页 DSL结构:

json
{
	"css": ".container {\n  padding: 10px;\n  background-color: #f5f5f5;\n}\n.search-bar {\n  padding: 10px 0;\n}\n.banner {\n  height: 180px;\n  border-radius: 8px;\n  overflow: hidden;\n}\n.banner-img {\n  width: 100%;\n  height: 100%;\n}\n.category {\n  display: flex;\n  justify-content: space-between;\n  padding: 15px 0;\n}\n.category-item {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n.category-icon {\n  width: 40px;\n  height: 40px;\n  margin-bottom: 5px;\n}\n.category-text {\n  font-size: 12px;\n  color: #333;\n}\n.section {\n  margin-top: 15px;\n  background-color: #fff;\n  border-radius: 8px;\n  padding: 10px;\n}\n.section-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 10px;\n}\n.section-title {\n  font-size: 16px;\n  font-weight: bold;\n}\n.section-more {\n  font-size: 12px;\n  color: #999;\n}\n.hot-list {\n  white-space: nowrap;\n}\n.hot-item {\n  display: inline-block;\n  width: 120px;\n  margin-right: 10px;\n}\n.hot-image {\n  width: 120px;\n  height: 90px;\n  border-radius: 4px;\n}\n.hot-title {\n  display: block;\n  font-size: 12px;\n  margin-top: 5px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.hot-price {\n  display: block;\n  font-size: 12px;\n  color: #ff5a5f;\n}\n.strategy-item {\n  display: flex;\n  margin-bottom: 10px;\n}\n.strategy-image {\n  width: 100px;\n  height: 70px;\n  border-radius: 4px;\n  margin-right: 10px;\n}\n.strategy-info {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n}\n.strategy-title {\n  font-size: 14px;\n  font-weight: bold;\n}\n.strategy-desc {\n  font-size: 12px;\n  color: #666;\n}\n.strategy-footer {\n  display: flex;\n  justify-content: space-between;\n  font-size: 10px;\n  color: #999;\n}",
	"state": {
		"banners": [
			{
				"image": "https://picsum.photos/750/350?random=1"
			},
			{
				"image": "https://picsum.photos/750/350?random=2"
			},
			{
				"image": "https://picsum.photos/750/350?random=3"
			}
		],
		"hotList": [
			{
				"id": 1,
				"title": "故宫博物院",
				"price": 60,
				"image": "https://picsum.photos/200/150?random=9"
			},
			{
				"id": 2,
				"title": "长城一日游",
				"price": 120,
				"image": "https://picsum.photos/200/150?random=10"
			},
			{
				"id": 3,
				"title": "颐和园",
				"price": 30,
				"image": "https://picsum.photos/200/150?random=11"
			},
			{
				"id": 4,
				"title": "天坛公园",
				"price": 15,
				"image": "https://picsum.photos/200/150?random=12"
			}
		],
		"categories": [
			{
				"id": 1,
				"name": "景点门票",
				"icon": "https://picsum.photos/50/50?random=4"
			},
			{
				"id": 2,
				"name": "酒店住宿",
				"icon": "https://picsum.photos/50/50?random=5"
			},
			{
				"id": 3,
				"name": "旅游线路",
				"icon": "https://picsum.photos/50/50?random=6"
			},
			{
				"id": 4,
				"name": "当地美食",
				"icon": "https://picsum.photos/50/50?random=7"
			},
			{
				"id": 5,
				"name": "交通出行",
				"icon": "https://picsum.photos/50/50?random=8"
			}
		],
		"strategyList": [
			{
				"id": 1,
				"title": "北京三日游攻略",
				"desc": "带你玩转北京著名景点",
				"author": "旅行达人",
				"views": 1250,
				"image": "https://picsum.photos/300/200?random=13"
			},
			{
				"id": 2,
				"title": "上海美食地图",
				"desc": "本地人推荐的地道美食",
				"author": "美食家",
				"views": 980,
				"image": "https://picsum.photos/300/200?random=14"
			}
		]
	},
	"cloudFuns": {},
	"dataSource": {},
	"children": [
		{
			"id": "bl658m7g4",
			"component": "View",
			"props": {
				"class": "container"
			},
			"events": [],
			"children": [
				{
					"id": "bo658m7g5",
					"component": "Swiper",
					"props": {
						"class": "banner",
						"autoplay": true,
						"circular": true,
						"interval": 3000
					},
					"events": [],
					"children": [
						{
							"id": "bp658m7g5",
							"component": "SwiperItem",
							"props": {
								"key": {
									"type": "JSExpression",
									"value": "this.context.index"
								}
							},
							"events": [],
							"children": [
								{
									"id": "bq658m7g5",
									"component": "Image",
									"props": {
										"src": {
											"type": "JSExpression",
											"value": "this.context.item.image"
										},
										"mode": "aspectFill",
										"class": "banner-img"
									},
									"events": [],
									"children": []
								}
							],
							"directives": [
								{
									"id": "ch658m7sw",
									"name": "vFor",
									"value": {
										"type": "JSExpression",
										"value": "this.state.banners"
									},
									"iterator": {
										"item": "item",
										"index": "index"
									}
								}
							]
						}
					]
				},
				{
					"id": "br658m7g5",
					"component": "View",
					"props": {
						"class": "category"
					},
					"events": [],
					"children": [
						{
							"id": "bs658m7g5",
							"component": "View",
							"props": {
								"key": {
									"type": "JSExpression",
									"value": "this.context.item.id"
								},
								"class": "category-item"
							},
							"events": [],
							"children": [
								{
									"id": "bt658m7g5",
									"component": "Image",
									"props": {
										"src": {
											"type": "JSExpression",
											"value": "this.context.item.icon"
										},
										"class": "category-icon"
									},
									"events": [],
									"children": []
								},
								{
									"id": "bu658m7g5",
									"component": "Text",
									"props": {
										"class": "category-text"
									},
									"events": [],
									"children": {
										"type": "JSExpression",
										"value": "this.context.item.name"
									}
								}
							],
							"directives": [
								{
									"id": "ci658m7sw",
									"name": "vFor",
									"value": {
										"type": "JSExpression",
										"value": "this.state.categories"
									},
									"iterator": {
										"item": "item",
										"index": "index"
									}
								}
							]
						}
					]
				},
				{
					"id": "bv658m7g5",
					"component": "View",
					"props": {
						"class": "section"
					},
					"events": [],
					"children": [
						{
							"id": "bw658m7g6",
							"component": "View",
							"props": {
								"class": "section-header"
							},
							"events": [],
							"children": [
								{
									"id": "bx658m7g6",
									"component": "Text",
									"props": {
										"class": "section-title"
									},
									"events": [],
									"children": "热门推荐"
								},
								{
									"id": "by658m7g6",
									"component": "Text",
									"props": {
										"class": "section-more"
									},
									"events": [],
									"children": "更多 >"
								}
							]
						},
						{
							"id": "bz658m7g6",
							"component": "ScrollView",
							"props": {
								"class": "hot-list",
								"scroll-x": ""
							},
							"events": [],
							"children": [
								{
									"id": "c0658m7g6",
									"component": "View",
									"props": {
										"key": {
											"type": "JSExpression",
											"value": "this.context.item.id"
										},
										"class": "hot-item"
									},
									"events": [],
									"children": [
										{
											"id": "c1658m7g6",
											"component": "Image",
											"props": {
												"src": {
													"type": "JSExpression",
													"value": "this.context.item.image"
												},
												"mode": "aspectFill",
												"class": "hot-image"
											},
											"events": [],
											"children": []
										},
										{
											"id": "c2658m7g6",
											"component": "Text",
											"props": {
												"class": "hot-title"
											},
											"events": [],
											"children": {
												"type": "JSExpression",
												"value": "this.context.item.title"
											}
										},
										{
											"id": "c3658m7g6",
											"component": "Text",
											"props": {
												"class": "hot-price"
											},
											"events": [],
											"children": {
												"type": "JSExpression",
												"value": "'¥' + this.context.item.price + '起'"
											}
										}
									],
									"directives": [
										{
											"id": "cn658m7sw",
											"name": "vFor",
											"value": {
												"type": "JSExpression",
												"value": "this.state.hotList"
											},
											"iterator": {
												"item": "item",
												"index": "index"
											}
										}
									]
								}
							]
						}
					]
				},
				{
					"id": "c4658m7g6",
					"component": "View",
					"props": {
						"class": "section"
					},
					"events": [],
					"children": [
						{
							"id": "c5658m7g6",
							"component": "View",
							"props": {
								"class": "section-header"
							},
							"events": [],
							"children": [
								{
									"id": "c6658m7g6",
									"component": "Text",
									"props": {
										"class": "section-title"
									},
									"events": [],
									"children": "精选攻略"
								},
								{
									"id": "c7658m7g6",
									"component": "Text",
									"props": {
										"class": "section-more"
									},
									"events": [],
									"children": "更多 >"
								}
							]
						},
						{
							"id": "c8658m7g6",
							"component": "View",
							"props": {
								"class": "strategy-list"
							},
							"events": [],
							"children": [
								{
									"id": "c9658m7g6",
									"component": "View",
									"props": {
										"key": {
											"type": "JSExpression",
											"value": "this.context.item.id"
										},
										"class": "strategy-item"
									},
									"events": [],
									"children": [
										{
											"id": "ca658m7g6",
											"component": "Image",
											"props": {
												"src": {
													"type": "JSExpression",
													"value": "this.context.item.image"
												},
												"mode": "aspectFill",
												"class": "strategy-image"
											},
											"events": [],
											"children": []
										},
										{
											"id": "cb658m7g6",
											"component": "View",
											"props": {
												"class": "strategy-info"
											},
											"events": [],
											"children": [
												{
													"id": "cc658m7g6",
													"component": "Text",
													"props": {
														"class": "strategy-title"
													},
													"events": [],
													"children": {
														"type": "JSExpression",
														"value": "this.context.item.title"
													}
												},
												{
													"id": "cd658m7g6",
													"component": "Text",
													"props": {
														"class": "strategy-desc"
													},
													"events": [],
													"children": {
														"type": "JSExpression",
														"value": "this.context.item.desc"
													}
												},
												{
													"id": "ce658m7g6",
													"component": "View",
													"props": {
														"class": "strategy-footer"
													},
													"events": [],
													"children": [
														{
															"id": "cf658m7g6",
															"component": "Text",
															"props": {
																"class": "strategy-author"
															},
															"events": [],
															"children": {
																"type": "JSExpression",
																"value": "this.context.item.author"
															}
														},
														{
															"id": "cg658m7g6",
															"component": "Text",
															"props": {
																"class": "strategy-views"
															},
															"events": [],
															"children": {
																"type": "JSExpression",
																"value": "this.context.item.views + '浏览'"
															}
														}
													]
												}
											]
										}
									],
									"directives": [
										{
											"id": "cr658m7sw",
											"name": "vFor",
											"value": {
												"type": "JSExpression",
												"value": "this.state.strategyList"
											},
											"iterator": {
												"item": "item",
												"index": "index"
											}
										}
									]
								}
							]
						}
					]
				}
			]
		}
	]
}