json schema 生成 vue 页面

低代码的核心模块主要可以用下图表示,从编辑器可以通过拖拽编辑等生成 json,然后将 json 生成代码,同时,支持将编辑后的代码反转为 json,然后继续在编辑器上配置。

将 json schema 生成 vue 页面是低代码平台的一个核心功能,大多数低代码平台配置的产物就是一份 json,这份 json 包括了在 Low Code 模式下配置范围内生成的规范字段,也包括了在 Pro Code 模式下(在 Low Code 模式无法支持的情况下)自己编辑代码生成的代码片段,他们合起来了一份 json 配置最终可以生成一份符合需求的 vue 文件,并且基于 json 可以再次在低代码平台迭代开发编辑。

vue 使用的是模版语法,整个文件大概就是这样子

其中 Tpl 代表符合规范的 json 生成的代码(低代码 Low 模式配置系统),每个位置都有自己的 tpl,unKnownCode 代表用户自己编辑的且不能转换成低代码配置的代码(这部分基本当成字符串透传就可以),而 customSourceTpl 代表用户编写的并且低代码平台转化不了成配置的源码,其他部分都是固定的,只需要将 json 转换成对应的字符串然后插入到对应的 tpl 上面,就可以生成一份 vue 文件

生成一个最简单的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div></div>
</template>
<script>
import root from "@/store/root";
import { req, store, actions } from "@lb/j2v-util";
export default {
name: "",
data() {
return {
store,
actions,
};
},
async mounted() {},
methods: {},
computed: {},
watch: {},
components: {},
};
</script>
<style lang="scss" scoped></style>

上面是一个最简单的页面对应下面的 json。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"type": "page",
"path": "/page",
"body": [],
"methods": [],
"importMaps": {
"@/store/root": "root",
"@lb/j2v-util": ["req", "store", "actions"]
},
"dataSource": [],
"cssConf": {
"scoped": true,
"lang": "scss",
"content": ""
}
}

其中,

  1. type 表示 组件的名字,type = page 代表页面组件,
  2. body 表示子组件数组,每个组件可能包含子组件,这会包含在 body(bodyKey,不一定是 body,例如 form-model 的 bodyKey 是 controls)中
  3. path 表示组件在组件树的位置路径 /${type}/${bodyKey}/${index}/${type} 用这种形式表示
  4. methods,importMaps,dataSource,cssConf 都是 type=page 页面级才有的属性,methods 对应 vue 页面的方法体,importMaps 代表 import 代码片段,dataSource 则代表了 data,computed,watch 等一系列的属性,cssConf 则代表 style 部分的代码片段

生成一个表单页面

这是 antdv 的 formModel 组件 的一个典型表单
模版,

来看看我们对应的 json 配置 以及对应 生成的 vue 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
{
"body": [
{
"path": "/page/body/0/form-model",
"type": "form-model",
"props": {
"ref": "ruleForm",
"$model": "lb_template_form_form",
"$labelCol": "{ span: 4 }",
"$wrapperCol": "{ span: 14 }"
},
"controls": [
{
"body": [
{
"path": "/page/body/0/form-model/controls/0/form-model-item/body/0/input",
"type": "input",
"$value": "lb_template_form_form.name"
}
],
"path": "/page/body/0/form-model/controls/0/form-model-item",
"type": "form-model-item",
"props": {
"prop": "name",
"rule": [
{
"message": "Please input Activity name",
"trigger": "blur",
"required": true
},
{
"max": 5,
"min": 3,
"message": "Length should be 3 to 5",
"trigger": "blur"
}
],
"label": "Activity name"
}
},
{
"body": [
{
"path": "/page/body/0/form-model/controls/1/form-model-item/body/0/select",
"type": "select",
"$value": "lb_template_form_form.region",
"dataTarget": "lb_template_form_formData",
"$textAdapter": "item.key",
"$valueAdapter": "item.value"
}
],
"path": "/page/body/0/form-model/controls/1/form-model-item",
"type": "form-model-item",
"props": {
"prop": "region",
"rule": [
{
"message": "Please select Activity zone",
"trigger": "change",
"required": true
}
],
"label": "Activity zone",
"placeholder": "please select your zone"
}
},
{
"body": [
{
"path": "/page/body/0/form-model/controls/2/form-model-item/body/0/date-picker",
"type": "date-picker",
"props": {
"type": "date",
"style": "width:100%",
"placeholder": "Pick a date"
},
"$value": "lb_template_form_form.date1"
}
],
"path": "/page/body/0/form-model/controls/2/form-model-item",
"type": "form-model-item",
"props": {
"prop": "date1",
"rule": [
{
"message": "Please pick a date",
"trigger": "change",
"required": true
}
],
"label": "Activity time"
}
},
{
"body": [
{
"path": "/page/body/0/form-model/controls/3/form-model-item/body/0/switch",
"type": "switch",
"$value": "lb_template_form_form.delivery"
}
],
"path": "/page/body/0/form-model/controls/3/form-model-item",
"type": "form-model-item",
"props": {
"prop": "delivery",
"label": "Instant delivery"
}
},
{
"body": [
{
"body": [
{
"path": "/page/body/0/form-model/controls/4/form-model-item/body/0/checkbox-group/body/0/checkbox",
"type": "checkbox",
"props": {
"value": "1"
},
"textAdapter": "Online"
},
{
"path": "/page/body/0/form-model/controls/4/form-model-item/body/0/checkbox-group/body/1/checkbox",
"type": "checkbox",
"props": {
"value": "2"
},
"textAdapter": "Promotion"
},
{
"path": "/page/body/0/form-model/controls/4/form-model-item/body/0/checkbox-group/body/2/checkbox",
"type": "checkbox",
"props": {
"value": "3"
},
"textAdapter": "Offline"
}
],
"path": "/page/body/0/form-model/controls/4/form-model-item/body/0/checkbox-group",
"type": "checkbox-group",
"$value": {
"source": "lb_template_form_form.type"
}
}
],
"path": "/page/body/0/form-model/controls/4/form-model-item",
"type": "form-model-item",
"props": {
"prop": "type",
"rule": [
{
"type": "array",
"message": "Please select at least one activity type",
"trigger": "change",
"required": true
}
],
"label": "Activity type"
}
},
{
"body": [
{
"body": [
{
"path": "/page/body/0/form-model/controls/5/form-model-item/body/0/radio-group/body/0/radio",
"type": "radio",
"props": {
"value": "1"
},
"textAdapter": "Sponsor"
},
{
"path": "/page/body/0/form-model/controls/5/form-model-item/body/0/radio-group/body/1/radio",
"type": "radio",
"props": {
"value": "2"
},
"textAdapter": "Venue"
},
{
"path": "/page/body/0/form-model/controls/5/form-model-item/body/0/radio-group/body/2/checkbox",
"type": "checkbox",
"props": {
"value": "3"
},
"textAdapter": "Offline"
}
],
"path": "/page/body/0/form-model/controls/5/form-model-item/body/0/radio-group",
"type": "radio-group",
"$value": "lb_template_form_form.resource"
}
],
"path": "/page/body/0/form-model/controls/5/form-model-item",
"type": "form-model-item",
"props": {
"prop": "resource",
"label": "Resources"
}
},
{
"body": [
{
"path": "/page/body/0/form-model/controls/6/form-model-item/body/0/textarea",
"type": "textarea",
"$value": "lb_template_form_form.desc"
}
],
"path": "/page/body/0/form-model/controls/6/form-model-item",
"type": "form-model-item",
"props": {
"prop": "desc",
"rule": [
{
"message": "Please input activity form",
"trigger": "blur",
"required": true
}
],
"label": "Activity form"
}
},
{
"body": [
{
"body": [
{
"path": "/page/body/0/form-model/controls/7/form-model-item/body/0/row/body/0/button",
"type": "button",
"event": {
"click": "onSubmit"
},
"props": {
"type": "primary"
},
"textAdapter": "Create"
},
{
"path": "/page/body/0/form-model/controls/7/form-model-item/body/0/row/body/1/button",
"type": "button",
"event": {
"click": "resetForm"
},
"props": {
"style": "margin-left:10px"
},
"textAdapter": "Reset"
}
],
"path": "/page/body/0/form-model/controls/7/form-model-item/body/0/row",
"type": "row"
}
],
"path": "/page/body/0/form-model/controls/7/form-model-item",
"type": "form-model-item",
"props": {
"$wrapperCol": "{ span: 14, offset: 4 }"
}
}
]
}
],
"path": "/page",
"type": "page",
"cssConf": {
"lang": "scss",
"scoped": true,
"content": ""
},
"methods": [
{
"fn": "this.$refs.ruleForm.validate(valid => { \n if (valid) {\nalert('submit!');\n } else {\nconsole.log('error submit!!');\nreturn false;\n }\n });",
"args": [],
"name": "onSubmit"
},
{
"fn": " this.$refs.ruleForm.resetFields();",
"args": [],
"name": "resetForm"
}
],
"dataSource": [
{
"init": {
"desc": "",
"name": "",
"type": [],
"date1": null,
"region": null,
"delivery": false,
"resource": ""
},
"name": "lb_template_form_form"
},
{
"init": [
{
"key": "Zone one",
"value": "Zone one"
},
{
"key": "Zone two",
"value": "Zone two"
}
],
"name": "lb_template_form_formData"
}
],
"importMaps": {
"@/store/root": "root",
"@lb/j2v-util": ["req", "store", "actions"]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<template>
<div>
<a-form-model
ref="ruleForm"
:model="lb_template_form_form"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 14 }"
:rules="rules"
>
<a-form-model-item prop="name" label="Activity name">
<a-input v-model="lb_template_form_form.name"></a-input>
</a-form-model-item>
<a-form-model-item
prop="region"
label="Activity zone"
placeholder="please select your zone"
>
<a-select v-model="lb_template_form_form.region">
<a-select-option
v-for="( item,index) in lb_template_form_formData"
:key="item.value"
>
{{item.key}}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item prop="date1" label="Activity time">
<a-date-picker
type="date"
style="width:100%"
placeholder="Pick a date"
v-model="lb_template_form_form.date1"
>
</a-date-picker>
</a-form-model-item>
<a-form-model-item prop="delivery" label="Instant delivery">
<a-switch v-model="lb_template_form_form.delivery"></a-switch>
</a-form-model-item>
<a-form-model-item prop="type" label="Activity type">
<a-checkbox-group v-model="lb_template_form_form.type">
<a-checkbox value="1">Online</a-checkbox>
<a-checkbox value="2">Promotion</a-checkbox>
<a-checkbox value="3">Offline</a-checkbox>
</a-checkbox-group>
</a-form-model-item>
<a-form-model-item prop="resource" label="Resources">
<a-radio-group v-model="lb_template_form_form.resource">
<a-radio value="1">Sponsor</a-radio>
<a-radio value="2">Venue</a-radio>
<a-checkbox value="3">Offline</a-checkbox>
</a-radio-group>
</a-form-model-item>
<a-form-model-item prop="desc" label="Activity form">
<a-textarea v-model="lb_template_form_form.desc"></a-textarea>
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 14, offset: 4 }">
<a-row>
<a-button type="primary" @click="onSubmit">Create</a-button>
<a-button style="margin-left:10px" @click="resetForm">Reset</a-button>
</a-row>
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import root from "@/store/root";
import { req, store, actions } from "@lb/j2v-util";
export default {
name: "",
data() {
return {
store,
actions,
lb_template_form_form: {
desc: "",
name: "",
type: [],
date1: null,
region: null,
delivery: false,
resource: "",
},
lb_template_form_formData: [
{
key: "Zone one",
value: "Zone one",
},
{
key: "Zone two",
value: "Zone two",
},
],
rules: {
name: [
{
message: "Please input Activity name",
trigger: "blur",
required: true,
},
{
max: 5,
min: 3,
message: "Length should be 3 to 5",
trigger: "blur",
},
],
region: [
{
message: "Please select Activity zone",
trigger: "change",
required: true,
},
],
date1: [
{
message: "Please pick a date",
trigger: "change",
required: true,
},
],
type: [
{
type: "array",
message: "Please select at least one activity type",
trigger: "change",
required: true,
},
],
desc: [
{
message: "Please input activity form",
trigger: "blur",
required: true,
},
],
},
};
},
async mounted() {},
methods: {
onSubmit() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
return false;
}
});
},
resetForm() {
this.$refs.ruleForm.resetFields();
},
},
computed: {},
watch: {},
components: {},
};
</script>
<style lang="scss" scoped></style>

其实一个 vue 文件,就是分成三块,template,script,style。style 模块从 json 转为代码比较简单(基本属于透传,主要工作量在于界面配置生成 json,这个后面文章分享)

生成 template 的代码字符串

要生成 template,就是要遍历 json 树,从根节点出发,不断遍历节点的子节点,将 type 转化为元素标签,将其他 json 参数转化为标签上的事件,属性等,最终生成一个完整的组件树。

首先我们会定义支持组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
const map = {
page: new Page(),
select: new Select(),
form: new Form(),
table: new Table(),
"form-item": new FormItem(),
number: new Number(),
"radio-group": new RadioGroup(),
radio: new Radio(),
"radio-button": new RadioButton(),
"date-picker": new DatePicker(),
button: new Button(),
card: new Card(),
row: new Row(),
col: new Col(),
tag: new Tag(),
"pop-confirm": new PopConfirm(),
descriptions: new Descriptions(),
"descriptions-item": new DescriptionsItem(),
modal: new Modal(),
"date-range-picker": new DateRangePicker(),
"router-view": new RouterView(),
divider: new Divider(),
textarea: new TextArea(),
"input-search": new InputSearch(),
input: new Input(),
"v-charts": new VCharts(),
switch: new Switch(),
"menu-item-group": new MenuItemGroup(),
menu: new Menu(),
"sub-menu": new SubMenu(),
"menu-item": new MenuItem(),
layout: new Layout(),
"layout-content": new LayoutContent(),
"layout-header": new LayoutHeader(),
"layout-sider": new LayoutSider(),
"layout-footer": new LayoutFooter(),
icon: new Icon(),
steps: new Steps(),
step: new Step(),
dropdown: new Dropdown(),
checkbox: new Checkbox(),
"checkbox-group": new CheckboxGroup(),
"button-group": new ButtonGroup(),
popover: new Popover(),
tooltip: new Tooltip(),
tree: new Tree(),
upload: new Upload(),
"directory-tree": new DirectoryTree(),
"tree-node": new TreeNode(),
"dropdown-button": new DropdownButton(),
"tab-pane": new TabPane(),
tabs: new Tabs(),
breadcrumb: new BreadCrumb(),
"breadcrumb-item": new BreadCrumbItem(),
"breadcrumb-separator": new BreadCrumbSeparator(),
"back-top": new BackTop(),
spin: new Spin(),
result: new Result(),
progress: new Progress(),
"page-header": new PageHeader(),
pagination: new Pagination(),
cascader: new Cascader(),
"time-picker": new TimePicker(),
slider: new Slider(),
collapse: new Collapse(),
"collapse-panel": new CollapsePanel(),
empty: new Empty(),
list: new List(),
"list-item": new ListItem(),
"list-item-meta": new ListItemMeta(),
timeline: new Timeline(),
"timeline-item": new TimelineItem(),
drawer: new Drawer(),
affix: new Affix(),
"auto-complete": new AutoComplete(),
avatar: new Avatar(),
alert: new Alert(),
"select-option": new SelectOption(),
"select-opt-group": new SelectOptionGroup(),
"form-model": new FormModel(),
"form-model-item": new FormModelItem(),
"locale-provider": new LocaleProvider(),
"config-provider": new ConfigProvider(),
};

export default map;

这些是部分支持的组件,都是基于 Antdv 的,对应 json 树的 type 字段

然后开始遍历 json 树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let recursiveRes: RecursiveRes = {
html: "",
data: {
dataSource: [],
dataTarget: [],
},
methods: [],
importMaps: {},
componentMaps: [],
cssConf: {
scoped: true,
lang: "scss",
content: "",
},
};

// 替换 d
recursive(json, recursiveRes, 1);

这里面 json 就是要传入的 json 树,recursiveRes 就是记录转化结果的对象,

recursiveRes 的主要代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
recursive(
item,
resInfo: RecursiveRes,
index: number,
withExtension = false,
forPreview = false // 针对预览去掉事件
) {

let type = item.type;
//定义好的组件
let uiComponent = commonUi[type] || antdv[type] || logicUi[type];
uiComponent.toHtml(item);
let p = parser[type];
if (!p) {
p = parser['unknown'];
}
const body = p.getParserBody(item);
if (body) {
body.forEach((child) => {
recursive(child, resInfo, ++index, withExtension, forPreview);
});
}

resInfo.html += h.end;
} else if (item.constructor.name === "Array") {
item.forEach((child) => {
recursive(child, resInfo, ++index, withExtension, forPreview);
});
}

recursive 主要是接受传入的 json,主要做了两件事

  1. 识别传入的 json 的 type 字段,找到对应的组件然后调用 toHtml 生成 html 字符串
  2. 识别传入的 json 的 BodyKey 字段,通过 getParserBody 获取到该组件下有哪些 slot,遍历 BodyKey 下的数组,然后拼接到该组件下

每个组件都会继承 BaseComponent,里面有 toHtml 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  export class Icon extends BaseComponent {
constructor() {
//对应的tag标签
super('a-icon');
}
}

toHtml(data: any): HtmlRes {
let tag = this.render(data);
tag += this.selfClosing ? "/>" : ">";
if (data.textAdapter && !this.selfClosing) {
tag += replaceVueExpression(data.textAdapter, "");
} else if (data.$textAdapter && !this.selfClosing) {
// 如果本身带有 {{xx}}变量写法,不自动添加{{}}
const p = data.$textAdapter.match(/\{\{.*\}\}/);
if (p) {
tag += `${data.$textAdapter}`;
} else {
tag += `{{ ${data.$textAdapter} }}`;
}
}

return {
start: tag,
end: this.selfClosing ? "" : `</${this.tag}>`,
};
}

protected render(raw: any) {
const { extraProps, extraEvents, ...data } = raw;

let tag = `<${this.tag} `;
const props = Object.assign(
{},
this.defaultConfig.props || {},
data.props,
extraProps || {}
);
if (data.id) {
props.id = data.id;
} else if (data.$id) {
props.$id = data.$id;
}
if (data.name) {
props.name = data.name;
}


tag += getPropsString(props); // render props;
tag += renderValueModule(data);
if (data.event || extraEvents) {
tag += ` ${eventRender({
...(data.event || {}),
...(extraEvents || {}),
})}`;
}
return tag;
}

toHtml 方法就是识别 type 然后找到定义好的组件然后将 html tag,属性,事件(这里面还有很多解析属性,事件的细节) 挂在 标签上。

1
2
3
4
5
6
7
8
9
10
11
{
"path": "/page/body/0/form-model/controls/7/form-model-item/body/0/row/body/0/button",
"type": "button",
"event": {
"click": "onSubmit"
},
"props": {
"type": "primary"
},
"textAdapter": "Create"
}

这里面的 event 对象就是事件的集合,props 就是对应的属性字段。其他的属性类似拼接出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
export function propsRender(props, strCur, isEvent = false) {
console.log("props", props);
for (let k in props) {
const prop = props[k];
// 规则 j 为动态
if (props[k] !== undefined) {
const t = props[k].constructor.name;
if (t === "String" || t === "Boolean") {
if (PropsTransformMap[k]) {
k = PropsTransformMap[k];
}

if (isEvent) {
// 字符串中\r\n代表换行空格,如果希望最终生成\r\n的字符串,必须添加转义为\\r \\n
strCur.s += `@${k}="${parseProps(prop).replace(/\"/g, "'")}" `; // 去掉JSON.stringify处理,避免\r\n等被转换
} else {
const keyName = k.includes("v-slot")
? k.replace(/^\$/, ":")
: camelToKebab(k.replace(/^\$/, ":"));
if (t === "Boolean") {
strCur.s += `${keyName}=${JSON.stringify(prop)} `;
} else if (["v-else"].indexOf(k) !== -1) {
strCur.s += `${keyName} `;
} else {
// 字符串中\r\n代表换行空格,如果希望最终生成\r\n的字符串,必须添加转义为\\r \\n
strCur.s += `${keyName}="${parseProps(prop).replace(/\"/g, "'")}"`; // 去掉JSON.stringify处理,避免\r\n等被转换
}
}
} else {
const keyName = k.includes("v-slot")
? k.replace(/^\$/, ":")
: camelToKebab(k.replace(/^\$/, ":"));
strCur.s += `${keyName}='${JSON.stringify(prop)}' `;
}
} else {
console.warn("props is empty");
}
}
}

通过这样,就能将整个 template 模块拼接出来了。

生成 script 的代码字符串

script 下面其实也可以拆分为几个小模块 data,mounted,function(对应 methods),transform(对应 computed),watch,import,components。

其实在生成 template 的代码字符串的时候,会对部分组件生成 import 等等的东西,加入到 recursiveRes 里面(比如某个组件一定要用到 moment 这个包)。

我们只需要对 recursiveRes 进行解析,然后每个模块的把每个模块的 tpl 字符串生成出来,然后插入到对应的位置上.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const m: DataJsRes = data2Js(
recursiveRes.data.dataSource,
recursiveRes.methods,
recursiveRes.importMaps,
recursiveRes.componentMaps,
recursiveRes.cssConf
);

export function data2Js(
data: DataSource[],
methods: MethodInfo[],
importMaps: Object = {},
componentsMaps: string[],
cssConf: CssConf
): DataJsRes {
const map: { [key: string]: DataSource } = {};
const transform = {};
data.forEach((item) => {
if (!map[item.name]) {
map[item.name] = item;
}
if (item.transform) {
transform[item.name] = item;
}
});
const gDataInfo = getGDataAndString(map, importMaps);

const gFunctionInfo = getRemoteFunctionString(map, "_get_");

const gMethodsFunctionString = getMethodsFunctionString(
methods,
gFunctionInfo.fMap
);

const gImportMapString = getImportString(importMaps);

let gMountedString = getMountedString(gFunctionInfo.fMap);

let gTransformFunctionString = getTransformFunction(transform);

let gDataWatchString = getDataWatchString(map);

let gComponentsString = getComponents(componentsMaps);

const gStyleString = getStyleString(cssConf);
return {
...gDataInfo,
gFunctionString: gFunctionInfo.functionString,
gMountedString,
gTransformFunctionString,
gDataWatchString,
gMethodsFunctionString,
gImportMapString,
gComponentsString,
gStyleString,
};
}

export function getMethodsFunctionString(
methods: MethodInfo[],
fMap: any
): string {
let s = "";
methods.forEach((method) => {
s += generalFunction(
method.name,
method.args,
method.fn,
method.isAsync,
method.comment || ""
);
if (method.callOnMounted) {
fMap[method.name] = {};
}
});
return s;
}

生成 Vue 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  // 构建page 文件
let g = "";
g = replaceTpl(tpl, "nameTpl", `"${json.name || ""}"`);
g = replaceTpl(g, "htmlTpl", recursiveRes.html);
g = replaceTpl(g, "gDataTpl", m.gDataString);
g = replaceTpl(g, "mountedTpl", m.gMountedString);
g = replaceTpl(g, "functionTpl", m.gFunctionString);
g = replaceTpl(g, "functionTpl", m.gMethodsFunctionString);
g = replaceTpl(g, "transformTpl", m.gTransformFunctionString);
g = replaceTpl(g, "watchTpl", m.gDataWatchString);
g = replaceTpl(g, "importTpl", m.gImportMapString);
g = replaceTpl(g, "componentsTpl", m.gComponentsString);
g = replaceTpl(g, "styleTpl", m.gStyleString);
if (json.customSource && json.customSource.unknownCode) { // 存在未可识别的外部代码
g = replaceTpl(g, "unknownCode", json.customSource.unknownCode);
}
g = g.replace(
/\{\{\ (nameTpl|htmlTpl|gDataTpl|mountedTpl|functionTpl|transformTpl|watchTpl|importTpl|componentsTpl|styleTpl|customSourceTpl|customDataTpl|unknownCode)\ \}\}/gim,
""
);
function replaceTpl(globalText, tpl, content) {
return globalText.replace(`{{ ${tpl} }}`, `${content} {{ ${tpl} }}`);
}
return g;
};

把一开始的模版文件引入进来,再替换相应的位置,就能生成整个 vue 文件了.

总结

这里大概说了下整个从 json 到 vue 的一个思路,当然里面还有很多细节。

  1. 有了 json 到 vue 。就可以结合配置平台底盘定义的一整套对 Web 应用的描述 DSL,不仅仅包含视图组件树,还包含数据绑定和交互逻辑,配置出定义好的 json,然后通过 json 转换成 vue 代码(这个后面会发文章讨论下配置相关的技巧(坑))

  2. 因为配置平台永远不可能 100%支持所有需求和用法,所以还需要给用户能够自己编写代码(proCode)去满足,并且写完代码后能够转换为 JSON,使得能够在平台上继续进行进行二次修改和迭代配置(unknownCode&&customSource 记录不可转换成配置的代码),这个后面会写个文章继续讨论.

查看评论