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": []
}| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 页面唯一标识 |
props | object | 页面级属性 |
state | object | 页面响应式状态,可在组件中通过 this.state.xxx 访问 |
cloudFunction | object | 云函数定义,可在组件中通过 this.cloudFuns.xxx() 调用 |
dataSource | object | 数据源配置,可在组件中通过 this.dataSource.xxx 访问 |
children | array | 子组件节点数组 |
组件节点结构(NodeSchema)
每个组件节点描述一个 UI 元素,递归嵌套形成组件树:
json
{
"id": "abc12345",
"component": "Button",
"props": { "type": "primary" },
"children": "点击按钮",
"invisible": false,
"slot": "default",
"directives": [],
"events": []
}| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 节点唯一标识(8 位随机字符) |
component | string | 组件名称,必须在可用组件列表中 |
props | object | 组件属性,键值对形式,值支持静态值或 JSExpression |
children | string | JSExpression | NodeSchema[] | 子内容:纯文本、表达式或子组件数组 |
invisible | boolean | JSExpression | 是否隐藏,类似 Vue 的 v-if / v-show |
slot | string | 插槽名称,不指定时默认为 "default" |
directives | NodeDirective[] | 指令列表,用于 vFor 循环渲染等 |
events | NodeEvent[] | 事件绑定列表,通过工作流引擎处理 |
JSExpression 表达式
DSL 中的动态值统一使用 JSExpression 格式表示,类似于 Vue 模板中的 插值:
json
{ "type": "JSExpression", "value": "this.state.count" }| 字段 | 类型 | 说明 |
|---|---|---|
type | string | 固定为 "JSExpression" |
value | string | JavaScript 表达式字符串 |
id | string | 可选,表达式唯一标识 |
数据访问路径
| 访问方式 | 说明 | 示例 |
|---|---|---|
this.state.xxx | 页面状态 | this.state.userName |
this.context.item | vFor 循环当前项 | this.context.item.name |
this.context.index | vFor 循环当前索引 | 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" }
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 指令唯一标识 |
name | string | 指令名称,循环渲染为 "vFor" |
value | JSExpression | 循环数据源,指向 this.state 中的数组 |
iterator | object | 迭代变量命名: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 })"
}
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 事件唯一标识 |
name | string | 事件名称,如 "onClick"、"onInput" |
handler | JSExpression | 事件处理器,调用工作流引擎的 execute 方法 |
handler 参数说明:
- 第一个参数:工作流节点 ID,对应
NodeComponent节点的id - 第二个参数:事件数据对象,包含
eventType(事件类型)和eventData(原生事件对象)
常用事件名:click、dblclick、input、change、submit、scroll
插槽(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"
}
}
]
}
]
}
]
}
]
}
]
}