这里讨论的 DSL 并不是真正意义上的 DSL,毕竟从新定义一门专用语言并不是本意,这里借用了 DSL 的概念,主要从 3 个方面来讲述个人对 DSL 的看法:
- DSL
- 解析器 -> 解析 DSL
- 可视化编辑器 -> 生成、编辑、存储 DSL
简述
DSL(domain specific language),是一种特定格式的描述配置。
可以作为 DSL 的格式:
- JSON
- protobuf
- yaml
- js
- 自定义格式
DSL 和解析器:
- typeORM: ORM 解析器,例如定义数据表:
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
}
- sails
- React
个人理解, DSL 本质是一个特定格式的文件,需要一个解析器(parser)解析(parse)。
定制 DSL
如果要定制 DSL,我们需要准备相当的编译原理知识:
-
前端编译
- 预编译,就是文本替换
- 词法分析(lexical analysis),拆词
- 语法分析(parsing),分析各个词是否符合规则
- 语义分析(semantic analysis),分析各个词是否符合语法要求
- 产出中间代码,例如 AST
- clang 工具
-
后端编译
- 根据前端编译生成的 AST 转译成目标代码,这个过程叫 compile,交给 compiler 完成这部分工作
- 代码优化
- 生成目标代码
- LLVM 工具
- GCC 包括前后端编译工作
我相信发明一门专用的语言并不是项目需求,我们更需要一份约定俗成的配置描述。
对于前端项目,应该优先考虑将 json 格式来作为 DSL。如果用业界标准的 json schema 那样现成工具是很多的。
回到 js ,著名工具 babel 提供了 js 代码的转译(transform)功能,具体过程是 parse -> tramsform -> generate。如果需要特定格式的内容,可以使用 babel ,根据实际业务需求完成所需的转译工作。
这样会比重新发明语言要简单且稳定。
DSL 和解析器
那么如果我们可以将 DSL 看作是一个约定俗成的文件描述,事情会变得简单,我们只需要将数据结构设计好,然后再编写解析器即可。
由于 DSL 只是一份描述文件,本身并不具备任何执行能力。举个例子,例如使用 json 描述一个 selector
选择器组件:
{
"type": "selector",
"value": "Selector's value"
}
这份 json 格式的描述文件本身不具备任何能力,所以需要写一个解析器(parser)来解析:
import Selector from 'SelectorComponent'
import selectorDesc from './path-to-json'
const Parser {
switcher => (config) {
const { type, ...props } = config;
switch(type) {
case 'selector':
return (
<Selector {...props} />
);
}
}
render(config) {
return (
<div>
{
this.switcher(config)
}
</div>
)
}
}
// 渲染
const p = new Parser();
p.render(selectorDesc);
// -> 页面输出,交给对应的页面渲染工具,例如 React VUE 或 angular
一个简易的配置描述 + 解析器大致完成。
但是这里有两个问题:
- 垂直扩展难度大
- 缺少系统运行时获取或更改系统状态的 hook,就是系统加载配置或者插件的生命周期回调
造成上述问题的是因为 json 是纯文本,并不具备与执行环境的交互能力,所以如果要监听 Selector 的某些回调事件,例如 onChange,并且加上一些特定的业务逻辑,需要在解析器做对应的功能,例如:
{
"type": "selector",
"value": "Selector's value",
"onChange": "function(event) { ...logic }",// 组建事件
"onLoaded": "function(event) { ...logic }" // 生命周期
}
const Parser {
mounted = () => {
// 系统运行状态的生命周期
}
switcher => (config) {
const { type, ...props } = config;
switch(type) {
case 'selector':
const { onChange } = config;
return (
<Selector {...props}
// 这里做解析字符串操作
onChange={() => eval(onChange)} />
);
default:
return <Other />
}
}
render(config) {
...
}
}
// 渲染
const p = new Parser();
p.render(selectorDesc);
// ... 系统的其他操作
p.mounted(); // 执行对应的生命周期
可以使用 eval 或 Function 通过字符串构造交互函数,但是作用域必须小心处理。
这样的配置会有一个好处,就是与执行环境无关,但是对于前端,特别是浏览器环境来说,json 并不是很适合作为业务描述格式。
像通讯协议 protobuf 是一种全新的 DSL,由客户端和服务端各自做一遍 protobuf 解析
如果是 js 文件作为描述,就可以解决上述的两个问题:
// config
const selectorConfig = {
"type": "selector",
"value": "Selector's value",
"onChange": function(event, state) { ...logic },// 组建事件
"onLoaded": function(event, state) { ...logic } // 生命周期
}
export {
selectorConfig
}
解析器部分只需要在适当的时机调用(call)配置中描述的函数,还可以将系统运行中的状态传入到回调中:
const Parser {
state = {
isMounted: false
}
mounted = (config) => {
// 系统运行状态的生命周期
this.setState({
isMounted: true
});
config.forEach((item) => {
item?.onLoaded({}, this.state);
})
}
switcher => (config) {
const { type, ...props } = config;
switch(type) {
case 'selector':
return (
// onChange 并不需要额外的代码来兼容
<Selector {...props} />
);
}
}
render(config) {
...
}
}
这样会提高自定义 DSL 的表达能力。
系统横向与垂直能力
横向拓展是指再添加一个组件,横向扩展性是否好用是指是否很好的多添加一个组件,例如:
// config
const selectorConfig = {
"type": "selector",
"value": "Selector's value"
}
const radioConfig = {
"type": "radio",
"value": "Selector's value",
}
export {
selectorConfig, radioConfig
}
垂直拓展是指其中深入控制一个组件,垂直扩展性意味着系统是否强大,以及足够灵活,例如:
// config
const selectorConfig = {
"type": "selector",
"value": "Selector's value",
"onChange": function(event, state) { ...logic },// 组建事件
"onLoaded": function(event, state) { ...logic } // 生命周期
}
export {
selectorConfig
}
通过保证核心解析器核心能力,通过对 js 描述文件(DSL 概念)的解析,系统便有了易用性、灵活性、可扩展性。
Scaffold 实现(解析器 + 交互框架)
以上是个人对前端管理系统开发中的一些理解,过去根据上述思路构建出一个 Scaffold,用以解析业务描述文件(配置)。如果熟悉 react,还支持使用 react 的语法自定义页面。
以下是基于上述思路定义的配置文件(.js):
import React from "react";
import { ShowModal, CloseModal, TableRow } from "@deer-ui/core";
import { Services } from "@dashboard/services";
import { HOCReportRender } from "@dashboard/template-engine";
import { getTestData, keyFieldsForReport } from "@dashboard/mock-data/report-data";
class TestReportClass extends Services {
propsForTable = {
rowKey: (record) => record.avatar
}
state = {
...this.state
};
constructor(props) {
super(props);
this.conditionOptions = [
{
ref: 'ref1',
tips: [123,321,222],
type: 'radio',
title: '单选控件',
values: {
value1: 'value1',
value2: 'value2',
value3: 'value3',
}
},
{
ref: 'input',
type: 'input',
title: '输入',
},
{
ref: 'refSelector',
type: 'select',
title: '多选控件',
isMultiple: true,
isNum: true,
defaultValue: [1, 2],
values: {
1: 'value1',
2: 'value2',
3: 'value3',
}
},
]
this.columns = [
...this.getFields({
names: keyFieldsForReport
}),
{
key: "action",
filter: (str, ...other) => this.getRecordBtns(...other)
}
];
this.templateOptions = {
needCheck: true,
checkedOverlay: (
<div>
<span className="btn theme">批量操作逻辑</span>
</div>
)
};
}
// 与 HOCReportRender 模版对接的查询接口
queryData = async (reportData) => {
const postData = this.reportDataFilter(reportData);
const agentOptions = {
actingRef: "querying",
after: this.reportAfter
};
await this.reqAgent(getTestData, agentOptions)(postData);
};
showDetail(item) {
const ModalId = ShowModal({
title: "详情",
width: 700,
children: <TableRow columns={this.columns} record={item} />
});
}
// 与 HOCReportRender 模版对接的按钮接口
recordActionBtns = [
{
text: "详情",
id: "detail",
action: (...args) => {
this.showDetail(...args);
}
}
];
}
const TestReport = HOCReportRender(TestReportClass);
export default TestReport;
这份 js 文件配置,主要还是由前端组员编写。
那这里进一步做拓展,将这份文件继续抽离成几个部分:
- 组件引入描述
- 组件配置描述
- 页面中的组件布局描述
- 组件的行为描述
- 页面被加载的生命周期描述
特定区域抽离成为可视化编辑工具的选项,例如将查询条件区域抽离,查询条件中的每一个组件的 props 抽离,提供可执行代码(这里需要注意运行时作用域安全,需要沙箱技术),按钮摆放、事件抽离等。
可视化编辑器
可视化编辑器其实就是在不断操作这份 DSL 数据,提供可视化界面,屏蔽技术细节。
这里有一些个人对于项目进度的规划想法,将系统分为两个部分:
-
DSL 标准定义和核心解析器
- DSL 建议使用 js
- UI 配置
- UI 回调
- 系统生命周期回调
- 解析器
- DSL 解析
- 系统运行时生命周期回调
-
可视化编辑引擎,可视化生成 DSL 并提供存储
- 编辑器部分内容
- 舞台 stage,编辑器的中心
- 元素 element,最小的 UI 元素
- 组件 component,由 element 构成的处理特定用户交互的控件
- 容器 container,记录和展示组件之间的布局关系
- 页面 page,由多个 container 之间的布局组成的页面
- props 编辑器,用于编辑元素或组件的表现形式
- 数据存储部分
-
项目
-
页面
- 组件
- 元素
-
参考项目