DSL 模式与数据模型
VTJ 低代码平台利用一种全面的领域特定语言(DSL)来以声明式方式描述应用程序。DSL Schema 定义了所有实体(项目、页面、组件和数据源)的结构契约,而 Model 层则通过事件驱动架构提供运行时操作能力。理解这些基础数据模型对于扩展平台、创建自定义物料或构建集成至关重要。
架构概览
DSL 系统遵循 Schema 定义(静态结构)与 Model 实现(运行时行为)的清晰分离。Schema 充当数据序列化的蓝图,而 Model 封装了业务逻辑、状态管理以及用于变更跟踪的事件发射。
flowchart TD
subgraph "静态 Schema 定义"
ProjectSchema -->|定义| ProjectModel
BlockSchema -->|定义| BlockModel
NodeSchema -->|定义| NodeModel
end
subgraph "运行时 Model 类"
ProjectModel -->|包含| BlockModel
BlockModel -->|包含| NodeModel
NodeModel -->|使用| PropModel
NodeModel -->|使用| EventModel
NodeModel -->|使用| DirectiveModel
PropModel -->|包含| SharedTypes
EventModel -->|包含| SharedTypes
DirectiveModel -->|包含| SharedTypes
end
subgraph "共享类型定义"
JSExpression
JSFunction
JSONValue
end
NodeSchema - 组件结构
NodeSchema 接口定义了 VTJ DSL 中任何组件的基础构建块。每个 UI 元素——从简单的按钮到复杂的容器——都表示为具有标准化结构的节点。
核心属性
| 属性 | 类型 | 必填 | 描述 |
|---|---|---|---|
id | string | 否 | 节点的唯一标识符 |
name | string | 是 | 组件名称(例如 'el-button', 'div') |
from | NodeFrom | 否 | 组件来源/原始引用 |
locked | boolean | 否 | 节点是否锁定以禁止编辑 |
invisible | boolean | 否 | 节点在设计器中是否隐藏 |
props | NodeProps | 否 | 组件属性和特性 |
events | NodeEvents | 否 | 绑定到节点的事件处理器 |
directives | NodeDirective[] | 否 | Vue 风格指令(v-if, v-for 等) |
children | NodeChildren | 否 | 子组件 |
slot | string | NodeSlot | 否 | 命名插槽位置 |
组件来源 (NodeFrom)
组件可以源于多种来源,由 NodeFrom 类型定义:
type NodeFrom =
| string // NPM 包名
| NodeFromSchema // 内部 Schema 引用
| NodeFromUrlSchema // 远程 JSON Schema
| NodeFromPlugin; // 远程插件组件
interface NodeFromSchema {
type: "Schema";
id: string; // Block Id 引用
}
interface NodeFromUrlSchema {
type: "UrlSchema";
url: string; // JSON Schema URL
}
interface NodeFromPlugin {
type: "Plugin";
urls: string[]; // 插件资源 URL
library?: string; // 导出的库名称
}
属性系统 (NodeProps)
props 对象包含所有组件属性,支持静态值、动态表达式和函数定义:
interface NodeProps {
key?: string | number | JSExpression;
ref?: string | JSExpression | JSFunction;
style?: Record<string, any>;
class?: string | string[] | JSExpression;
[index: string]: JSONValue | JSExpression | JSFunction;
}
NodeSchema 示例
{
"id": "button_123",
"name": "el-button",
"props": {
"type": "primary",
"size": {
"type": "JSExpression",
"value": "state.buttonSize"
},
"onClick": {
"type": "JSFunction",
"value": "function() { state.count++ }"
}
},
"events": {
"click": {
"name": "click",
"handler": {
"type": "JSFunction",
"value": "function(event) { console.log(event); }"
}
}
}
}
BlockSchema - 组件块结构
BlockSchema 代表一个可复用的组件块,它封装了状态、逻辑和模板结构。Blocks 是创建自定义物料的主要单元,可以用作页面、组件或局部模板。
完整的 Block 结构
| 属性 | 类型 | 描述 |
|---|---|---|
id | string | 唯一的块标识符 |
name | string | 组件/块名称 |
locked | boolean | 锁定状态 |
inject | BlockInject[] | 依赖注入 |
state | BlockState | 响应式状态数据 |
lifeCycles | Record<string, JSFunction> | 生命周期钩子 |
methods | Record<string, JSFunction> | 自定义方法 |
computed | Record<string, JSFunction> | 计算属性 |
watch | BlockWatch[] | 侦听器 |
css | string | 组件样式 |
props | Array<string | BlockProp> | 公共 Props 定义 |
emits | Array<string | BlockEmit> | 自定义事件 |
expose | string[] | 暴露的公共属性 |
slots | Array<string | BlockSlot> | 定义的插槽 |
nodes | NodeSchema[] | 模板节点树 |
dataSources | Record<string, DataSourceSchema> | 数据源 |
高级特性
状态管理:
type BlockState = Record<string, JSONValue | JSExpression | JSFunction>;
// 示例
{
"count": 0,
"user": {
"type": "JSExpression",
"value": "fetchUser()"
},
"computedValue": {
"type": "JSFunction",
"value": "function() { return this.count * 2 }"
}
}
侦听器:
interface BlockWatch {
id?: string;
source: JSFunction | JSExpression;
deep?: boolean;
immediate?: boolean;
handler: JSFunction;
}
// 示例
{
"id": "watch_1",
"source": {
"type": "JSExpression",
"value": "state.userData"
},
"deep": true,
"handler": {
"type": "JSFunction",
"value": "function(newVal, oldVal) { console.log('Changed:', newVal); }"
}
}
ProjectSchema - 项目配置
ProjectSchema 定义了整个应用程序结构,包括页面、块、依赖项和全局配置。它是任何 VTJ 应用程序的顶层入口点。
项目属性
| 属性 | 类型 | 描述 |
|---|---|---|
id | string | 项目唯一标识符 |
name | string | 项目名称 |
description | string | 项目描述 |
platform | PlatformType | 目标平台 |
pages | PageFile[] | 页面文件列表 |
blocks | BlockFile[] | 可复用的块组件 |
homepage | string | 默认页面 ID |
dependencies | Dependencie[] | 外部依赖项 |
apis | ApiSchema[] | API 定义 |
meta | MetaSchema[] | Meta 查询配置 |
config | ProjectConfig | 项目配置 |
uniConfig | UniConfig | UniApp 特定配置 |
globals | GlobalConfig | 全局应用配置 |
i18n | I18nConfig | 国际化配置 |
env | EnvConfig[] | 环境变量 |
平台类型
type PlatformType = "web" | "h5" | "uniapp";
全局配置
GlobalConfig 接口提供应用程序级别的配置,用于路由、状态管理和 HTTP 处理:
interface GlobalConfig {
css?: string; // 全局样式
store?: JSFunction; // Pinia/Vuex store
access?: JSFunction; // 权限控制
enhance?: JSFunction; // 应用增强
axios?: JSFunction; // Axios 配置
request?: JSFunction; // 请求拦截器
response?: JSFunction; // 响应拦截器
beforeEach?: JSFunction; // 路由前置守卫
afterEach?: JSFunction; // 路由后置钩子
}
DataSourceSchema - 数据管理
数据源支持声明式的数据获取和转换,支持 API、Mock 数据和 Meta 查询。
数据源类型
type DataSourceType = "api" | "cube" | "meta" | "mock";
DataSourceSchema 接口
interface DataSourceSchema {
type: DataSourceType;
ref?: string; // 引用项目 API
name: string; // 数据源名称
label?: string; // 显示标签
transform?: JSFunction; // 数据转换
test?: JSFunction; // 测试用例函数
mockTemplate?: JSFunction; // Mock 数据模板
}
API Schema 定义
项目级别的 API 单独定义,并由数据源引用:
interface ApiSchema {
id: string; // 唯一标识符
name: string; // API 名称
label?: string; // 描述
url: string; // 请求 URL
category?: string; // 分组/类别
method?: ApiMethod; // HTTP 方法
settings?: Record<string, any>; // 请求设置
headers?: JSExpression | JSFunction;
jsonpOptions?: Record<string, any>;
mockTemplate?: JSFunction;
mock?: boolean; // 启用 Mock
}
type ApiMethod = "get" | "post" | "put" | "delete" | "patch" | "jsonp";
文件结构 - 页面和块
VTJ DSL 通过 PageFile 和 BlockFile 接口区分页面文件和块文件。
通用文件属性
interface BlockFile {
type: FileType; // 'block' 或 'page'
id: string;
name: string; // 文件名
title: string; // 显示标题
category?: string;
market?: MarketInstallInfo; // 市场安装信息
fromType?: "Schema" | "UrlSchema" | "Plugin";
preset?: boolean; // 预设(不可编辑)
urls?: string; // 资源 URL
library?: string; // 插件库名称
dsl?: BlockSchema; // 文件内容
}
页面特定属性
页面扩展了块文件,增加了路由和导航特定的属性:
interface PageFile extends BlockFile {
dir?: boolean; // 目录/容器
layout?: boolean; // 布局页面
icon?: string; // 菜单图标
children?: PageFile[]; // 子页面
mask?: boolean; // 布局内页面
hidden?: boolean; // 从菜单中隐藏
raw?: boolean; // 源代码页面(非低代码)
pure?: boolean; // 纯净页面(无页头/页脚)
cache?: boolean; // 启用页面缓存
meta?: Record<string, any>; // 路由元信息
needLogin?: boolean; // UniApp: 需要登录
style?: Record<string, any>; // UniApp: 窗口样式
}
Model 类 - 运行时层
Schema 定义了静态结构,而 Model 类提供运行时操作能力。Models 实现了事件驱动模式以跟踪变更。
NodeModel
NodeModel 类封装 NodeSchema 并提供了操作节点结构的方法:
class NodeModel {
constructor(schema: NodeSchema, parent: NodeModel | null);
// 结构操作
update(schema: Partial<NodeSchema>, silent?: boolean): void;
setChildren(
children: NodeSchema[] | string | JSExpression,
silent?: boolean,
): void;
setSlot(slot?: string | NodeSlot, silent?: boolean): void;
// 树操作
appendChild(node: NodeModel, silent?: boolean): void;
removeChild(node: NodeModel, silent?: boolean): void;
insertAfter(node: NodeModel, silent?: boolean): void;
insertBefore(node: NodeModel, silent?: boolean): void;
// 属性管理
setProp(
name: string,
value: JSONValue | JSExpression | JSFunction,
defaultValue?: JSONValue | JSExpression | JSFunction,
silent?: boolean,
): void;
removeProp(name: string, silent?: boolean): void;
getPropValue(name: string): any;
// 事件和指令
setEvent(scheam: NodeEvent, silent?: boolean): void;
removeEvent(name: string, silent?: boolean): void;
setDirective(scheam: NodeDirective | DirectiveModel, silent?: boolean): void;
removeDirective(dirctive: DirectiveModel, silent?: boolean): void;
// 状态管理
lock(silent?: boolean): void;
unlock(silent?: boolean): void;
setVisible(visible: boolean, silent?: boolean): void;
// 导出
toDsl(): NodeSchema;
dispose(silent?: boolean): void;
}
所有 Model 方法都接受一个可选的
silent参数。当传入silent: true时,操作将执行更改而不会触发事件。这对于批量操作非常有用,可以避免过多的事件发射和重新渲染。
BlockModel
BlockModel 管理块级别的状态、逻辑和节点树:
class BlockModel {
constructor(schema: BlockSchema);
// 状态和函数
setFunction(
type: "methods" | "computed" | "lifeCycles",
name: string,
value: JSFunction,
silent?: boolean,
): void;
removeFunction(
type: "methods" | "computed" | "lifeCycles",
name: string,
silent?: boolean,
): void;
setState(
name: string,
value: JSONValue | JSExpression | JSFunction,
silent?: boolean,
): void;
removeState(name: string, silent?: boolean): void;
// 侦听器
setWatch(watch: BlockWatch, silent?: boolean): void;
removeWatch(watch: BlockWatch, silent?: boolean): void;
// Props, emits, slots, inject
setProp(prop: BlockProp, silent?: boolean): void;
setEmit(emit: string | BlockEmit, silent?: boolean): void;
setSlot(slot: string | BlockSlot, silent?: boolean): void;
setInject(inject: BlockInject, silent?: boolean): void;
// 数据源
setDataSource(source: DataSourceSchema, silent?: boolean): void;
removeDataSource(name: string, silent?: boolean): void;
// 节点树操作
addNode(
node: NodeModel,
target?: NodeModel,
position: DropPosition = "inner",
silent?: boolean,
): void;
removeNode(node: NodeModel, silent?: boolean): void;
move(
node: NodeModel,
target?: NodeModel,
position: DropPosition = "inner",
silent?: boolean,
): void;
movePrev(node: NodeModel, silent?: boolean): void;
moveNext(node: NodeModel, silent?: boolean): void;
cloneNode(target: NodeModel, silent?: boolean): void;
// 导出和生命周期
toDsl(version?: string): BlockSchema;
dispose(): void;
}
type DropPosition = "left" | "right" | "top" | "bottom" | "inner";
ProjectModel
ProjectModel 管理整个应用程序结构:
class ProjectModel {
constructor(schema: ProjectSchema);
// 文件管理
active(file: BlockFile | PageFile, silent?: boolean): void;
deactivate(silent?: boolean): void;
// 页面操作
createPage(
page: PageFile,
parentId?: string,
silent?: boolean,
): Promise<void>;
updatePage(page: PageFile, silent?: boolean): void;
clonePage(page: PageFile, parentId?: string, silent?: boolean): void;
removePage(id: string, silent?: boolean): void;
getPage(id: string): PageFile | undefined;
getPages(): PageFile[];
// 块操作
createBlock(block: BlockFile, silent?: boolean): Promise<void>;
updateBlock(block: BlockFile, silent?: boolean): void;
cloneBlock(block: BlockFile, silent?: boolean): void;
removeBlock(id: string, silent?: boolean): void;
getBlock(id: string): BlockFile | undefined;
// 依赖项
setDeps(item: Dependencie, silent?: boolean): void;
removeDeps(item: Dependencie, silent?: boolean): void;
// APIs 和 Meta
setApi(item: ApiSchema, silent?: boolean): void;
removeApi(name: string, silent?: boolean): void;
setMeta(item: MetaSchema, silent?: boolean): void;
removeMeta(code: string, silent?: boolean): void;
// 配置
setConfig(config: ProjectConfig, silent?: boolean): void;
setUniConfig(
key: keyof UniConfig,
value: Record<string, any>,
silent?: boolean,
): void;
setGloblas(
key: keyof GlobalConfig,
value: string | JSFunction,
silent?: boolean,
): void;
setI18n(i18n: I18nConfig, silent?: boolean): void;
setEnv(env: EnvConfig[], silent?: boolean): void;
// 发布和代码生成
publish(file?: PageFile | BlockFile): void;
genSource(): void;
// 导出
toDsl(_version?: string): ProjectSchema;
}
共享类型
DSL 使用共享类型定义来区分表达式、函数和静态数据。
JSExpression 和 JSFunction
interface JSExpression {
type: "JSExpression";
id?: string;
value: string; // JavaScript 表达式字符串
}
interface JSFunction {
type: "JSFunction";
id?: string;
value: string; // JavaScript 函数字符串
}
JSON 值类型
type JSONValue =
| boolean
| string
| number
| null
| undefined
| JSONArray
| JSONObject;
type JSONArray = JSONValue[];
interface JSONObject {
[key: string]: JSONValue;
}
事件系统
Model 层实现了事件驱动架构以跟踪变更。当发生修改时,每种 Model 类型都会发射特定的事件。
事件常量
// NodeModel 事件
export const EVENT_NODE_CHANGE = "EVENT_NODE_CHANGE";
// BlockModel 事件
export const EVENT_BLOCK_CHANGE = "EVENT_BLOCK_CHANGE";
// ProjectModel 事件
export const EVENT_PROJECT_CHANGE = "EVENT_PROJECT_CHANGE";
export const EVENT_PROJECT_ACTIVED = "EVENT_PROJECT_ACTIVED";
export const EVENT_PROJECT_DEPS_CHANGE = "EVENT_PROJECT_DEPS_CHANGE";
export const EVENT_PROJECT_PAGES_CHANGE = "EVENT_PROJECT_PAGES_CHANGE";
export const EVENT_PROJECT_BLOCKS_CHANGE = "EVENT_PROJECT_BLOCKS_CHANGE";
export const EVENT_PROJECT_APIS_CHANGE = "EVENT_PROJECT_APIS_CHANGE";
export const EVENT_PROJECT_META_CHANGE = "EVENT_PROJECT_META_CHANGE";
export const EVENT_PROJECT_PUBLISH = "EVENT_PROJECT_PUBLISH";
export const EVENT_PROJECT_FILE_PUBLISH = "EVENT_PROJECT_FILE_PUBLISH";
export const EVENT_PROJECT_GEN_SOURCE = "EVENT_PROJECT_GEN_SOURCE";
完整 DSL 示例
一个最小但完整的 VTJ 项目 DSL 演示了所有 Schema 组件是如何协同工作的:
{
"__VTJ_PROJECT__": true,
"__VERSION__": "1.0.0",
"name": "my-app",
"description": "Sample VTJ Application",
"platform": "web",
"homepage": "home",
"config": {
"title": "My Application",
"logo": "/logo.png",
"themeSwitchable": true
},
"globals": {
"css": "body { margin: 0; }",
"store": {
"type": "JSFunction",
"value": "() => {\n return {\n state: {\n user: null\n }\n }\n}"
}
},
"apis": [
{
"id": "api_user",
"name": "getUserInfo",
"url": "/api/user/info",
"method": "get",
"category": "user"
}
],
"pages": [
{
"type": "page",
"id": "home",
"name": "home",
"title": "Home Page",
"dsl": {
"__VTJ_BLOCK__": true,
"id": "home_block",
"name": "Home",
"state": {
"count": 0,
"message": "Hello VTJ"
},
"methods": {
"increment": {
"type": "JSFunction",
"value": "function() { this.state.count++ }"
}
},
"dataSources": {
"userData": {
"type": "api",
"ref": "api_user",
"name": "userData",
"transform": {
"type": "JSFunction",
"value": "function(res) { return res.data }"
}
}
},
"nodes": [
{
"id": "container",
"name": "div",
"props": {
"class": "container"
},
"children": [
{
"id": "title",
"name": "h1",
"children": "Welcome to VTJ"
},
{
"id": "counter",
"name": "div",
"children": {
"type": "JSExpression",
"value": "`Count: ${state.count}`"
}
},
{
"id": "button",
"name": "el-button",
"props": {
"type": "primary"
},
"events": {
"click": {
"name": "click",
"handler": {
"type": "JSFunction",
"value": "function() { this.methods.increment() }"
}
}
}
}
]
}
]
}
}
]
}
Model 使用模式
创建和操作节点
import { NodeModel } from "@vtj/core";
// 创建一个新节点
const buttonNode = new NodeModel({
id: "btn_1",
name: "el-button",
props: {
type: "primary",
},
});
// 静默更新属性(批量操作)
buttonNode.update({ props: { size: "large" } }, true);
// 添加事件处理器
buttonNode.setEvent({
name: "click",
handler: {
type: "JSFunction",
value: 'function() { console.log("clicked") }',
},
});
// 导出为 DSL
const dsl = buttonNode.toDsl();
使用块
import { BlockModel, NodeModel } from "@vtj/core";
// 创建一个带有状态的块
const block = new BlockModel({
name: "Counter",
state: {
count: 0,
},
methods: {
increment: {
type: "JSFunction",
value: "function() { this.state.count++ }",
},
},
});
// 向块添加节点
const textNode = new NodeModel({ name: "div", children: "Count: 0" });
const buttonNode = new NodeModel({
name: "el-button",
children: "Increment",
});
block.addNode(textNode);
block.addNode(buttonNode);
// 导出完整的块 Schema
const blockDsl = block.toDsl();
项目管理
import { ProjectModel } from "@vtj/core";
// 从 Schema 初始化项目
const project = new ProjectModel(projectSchema);
// 打开文件进行编辑
const homePage = project.getPage("home");
if (homePage) {
project.active(homePage);
}
// 创建新页面
await project.createPage({
type: "page",
id: "about",
name: "about",
title: "About Us",
dsl: {
name: "About",
nodes: [],
},
});
// 添加 API 依赖
project.setApi({
id: "api_items",
name: "getItems",
url: "/api/items",
method: "get",
});
// 导出项目用于序列化
const exportedProject = project.toDsl();
最佳实践
当对复杂的节点树执行批量更新时,请使用
silent: true参数以避免触发过多的变更事件。完成所有更新后,如果需要,可以手动发射单个变更事件。
Schema 版本控制
始终在你的 Schema 中包含版本元数据,以便未来的迁移和兼容性:
const schema = {
__VTJ_BLOCK__: true,
__VERSION__: "1.0.0",
// ... 其他属性
};
表达式 vs 函数
- 使用
JSExpression用于解析为数据的简单值绑定(例如state.count + 1) - 使用
JSFunction用于具有副作用的可执行代码(例如事件处理器、生命周期钩子、计算属性)
内存管理
当 Model 不再需要时,始终调用 dispose() 来清理事件监听器并防止内存泄漏:
block.dispose();
project.dispose();