vue页面反转成json schema

很多低代码平台适用场景有限,只能无代码编辑,或者部分代码编辑,无法直接修改源代码,难以支撑复杂业务,限制了开发效率。

只有提供 low code <=> pro code 互转的能力,才能保证灵活性。

前面一篇文章我们已经说了如何将 json schema 转为 vue 源码,这篇文章我们讲下如何将 vue 源码转为 json schema

实现方案

转化的 json schema 结果格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
const info: DataJson = {
type: "page",
path: "/page",
body: [],
methods: [],
importMaps: {},
dataSource: [],
cssConf: {
scoped: true,
lang: "scss",
content: "",
},
};

更多字段参考json schema 生成 vue 页面

首先,vue 官方提供了
Vue Template Compiler,其中的
compiler.parseComponent(file, [options]) 可以解析一个 SFC (单文件组件,或 *.vue 文件) 解析为一个描述器 (参考 flow 声明 中的 SFCDescriptor 类型)。

也就是分为 template、script,styles。
我们对传入的 vue 文件直接用 Vue Template Compiler 进行解析

1
2
3
4
import { parseComponent, compile } from "vue-template-compiler/build";
//codeStr就是文件的源码字符串
const sfc = parseComponent(codeStr);
console.log("sfc", sfc);

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
<template>
<div>
<a-button type="primary" @click="click">{{ test }}</a-button>
</div>
</template>
<script>
import root from "@/store/root";
import { req, store, actions } from "@lb/j2v-util";
export default {
name: "",
data() {
return {
store,
actions,
test: "按钮",
};
},
async mounted() {},
methods: {
click() {
this.test1 = this.test1 + "222";
},
},
computed: {},
watch: {
test: {
immediate: true,
deep: true,
async handler(value, oldValue) {
this.$message.info(value, oldValue);
},
},
},
components: {},
};
</script>
<style lang="scss" scoped>
.test {
width: 100%;
}
</style>
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
{
"template": {
"type": "template",
"content":
"\n" +
"<div>\n" +
" <a-button type=\"primary\" @click=\"click\">{{ test }}</a-button>\n" +
"</div>\n",
"start": 10,
"attrs": {},
"end": 94
},
"script": {
"type": "script",
"content":
"\n" +
"import root from \"@/store/root\";\n" +
"import { req, store, actions } from \"@lb/j2v-util\";\n" +
"export default {\n" +
" name: \"\",\n" +
" data() {\n" +
" return {\n" +
" store,\n" +
" actions,\n" +
" test: \"按钮\",\n" +
" };\n" +
" },\n" +
" async mounted() {},\n" +
" methods: {\n" +
" click() {\n" +
" this.test1 = this.test1 + \"222\";\n" +
" },\n" +
" },\n" +
" computed: {},\n" +
" watch: {\n" +
" test: {\n" +
" immediate: true,\n" +
" deep: true,\n" +
" async handler(value, oldValue) {\n" +
" this.$message.info(value, oldValue);\n" +
" },\n" +
" },\n" +
" },\n" +
" components: {},\n" +
"};\n",
"start": 114,
"attrs": {},
"end": 617
},
"styles": [
{
"type": "style",
"content": "\n.test {\n width: 100%;\n}\n",
"start": 653,
"attrs": [Object],
"lang": "scss",
"scoped": true,
"end": 679
}
],
"customBlocks": [],
"errors": []
}

分别分析 template,script,styles 模块

分析 scripts

一般一份 vue 的 js 代码就是 import 以及 export 模块,当然也有可能有其他代码,这些代码会被放进 unknownCode 透穿
所以我们会这样的顺序遍历 ast

首先将 script 代码用@babel/parser转为 ast 树,然后用@babel/traverse分别分析 import,export,以及其他代码 3 个部分。

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
Object.assign(info, jsCompiler(sfc.script.content));

//jsCompiler里面做的事情
import { parse } from "@babel/parser";
import traverse, { NodePath } from "@babel/traverse";
import analyse from "./analyse";
import generate from "@babel/generator";
const jsAst = parse(jsStr, {
sourceType: "module",
plugins: ["classProperties", "jsx"],
});

traverse(jsAst, {
ImportDeclaration(rootPath) {
//分析import的ast块
...
}
ExportDefaultDeclaration(rootPath) {
// 分析export default
...
}
jsAst.program.body.forEach(item => {
// 过滤import和export
if (!bt.isImportDeclaration(item) && !bt.isExportDefaultDeclaration(item)) {
//分析其他的代码
}
})
})

上面的步骤首先将 js 内容转化为 jsAst,可参考AST Explorer
,对着这个 ast 树一步步进行分析

解析 ImportDeclaration

import 的内容最终会放到 importMaps 里面,规则如下:

  1. import “xxx” => importMaps[“xxx”] = null
  2. import a from “xxx” => importMaps[“xxx”] = a
  3. import * as a from “xxx” => importMaps[“xxx”] = * as a
  4. import a, { b } from “xxx” => importMaps[“xxx”] = a, { b }
  5. import { a, b } from “xxx” => importMaps[“xxx”] = [ a, b ]

然后遇到重复的做合并。

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
traverse(jsAst, {
// 提取import
ImportDeclaration(rootPath) {
const specifiers = rootPath.node.specifiers;
let params: any;
if (
// import "xxx"
specifiers.length === 0
) {
params = null;
} else if (
// import a from "xxx"
specifiers.length === 1 &&
bt.isImportDefaultSpecifier(specifiers[0])
) {
params = specifiers[0].local.name;
} else if (
// import * as a from "xxx"
specifiers.length === 1 &&
bt.isImportNamespaceSpecifier(specifiers[0])
) {
params = generate(specifiers[0]).code;
} else {
const importDefaultSpecifier = specifiers.find((item) => {
return bt.isImportDefaultSpecifier(item);
});
if (importDefaultSpecifier) {
// import a, { b } from "xxx"
params =
importDefaultSpecifier.local.name +
", { " +
specifiers
.filter((item) => {
return item.local.name !== importDefaultSpecifier.local.name;
})
.map((item) => {
return generate(item).code;
})
.join(", ") +
" }";
} else {
// import { a, b } from "xxx"
params = specifiers.map((item) => {
return generate(item).code;
});
}
}
if (!result.importMaps[rootPath.node.source.value]) {
result.importMaps[rootPath.node.source.value] = params;
} else {
// 已存在同一个导入包,需要进行合并
let value = result.importMaps[rootPath.node.source.value];
if (Array.isArray(value)) {
value = `{ ${value.join(",")} }`;
}
value += `, ${
Array.isArray(params) ? "{" + params.join(",") + "}" : params
}`;
result.importMaps[rootPath.node.source.value] = value;
}
},
});
解析 ExportDefaultDeclaration

export 模块存在两种情况:

  1. export default {}
  2. export default {}()
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
traverse(jsAst, {
// 分析export default
ExportDefaultDeclaration(rootPath) {
// 兼容export default a 和 export default a()的情况
if (bt.isIdentifier(rootPath.node.declaration)) {
exportDefaultKey = rootPath.node.declaration.name;
} else if (bt.isCallExpression(rootPath.node.declaration)) {
exportDefaultKey = rootPath.node.declaration.callee.name;
}
if (exportDefaultKey) {
const bindings = rootPath.scope.bindings;
rootPath = bindings[exportDefaultKey].path;
if (bt.isFunctionDeclaration(rootPath)) {
(rootPath as NodePath).traverse({
ReturnStatement(path) {
rootPath = path;
path.skip();
},
});
}
}

// 分析默认导出的Object
rootPath.traverse({
ObjectExpression: {
enter: (path) => {
componentLevel++;
// 只遍历第一层

if (componentLevel === 1) {
path.node.properties.forEach((element) => {
// 处理分析各个vue属性
const optionsAnalyse = element.key && analyse[element.key.name] || null;
if (optionsAnalyse) {
Object.assign(analyseResult, optionsAnalyse(element));
} else {
rootCustomSource.push(generate(element).code);
}
});
}
},
exit: () => {
componentLevel--;
},
},
});
},
});

然后分别分析对象里面的 methods,data,mounted,computed,watch,name,components 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import methodsAnalyse from "./methodsAnalyse";
import dataAnalyse from "./dataAnalyse";
import mountedAnalyse from "./mountedAnalyse";
import computedAnalyse from "./computedAnalyse";
import watchAnalyse from "./watchAnalyse";
import componentsAnalyse from "./componentsAnalyse";

export default {
methods: methodsAnalyse,
data: dataAnalyse,
mounted: mountedAnalyse,
computed: computedAnalyse,
watch: watchAnalyse,
name: (element) => {
return {
rootName: element.value.value,
};
},
components: componentsAnalyse,
};

这里面就是对每个属性进行分析,然后写回到 analyseResult 对象里面去

解析其他代码

除了 import 和 export,其他代码都丢进 unknownCode 里面去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jsAst.program.body.forEach((item) => {
// 过滤import和export
if (!bt.isImportDeclaration(item) && !bt.isExportDefaultDeclaration(item)) {
// 如果存在exportDefaultKey,对应的export变量也过滤
if (
!(
bt.isVariableDeclaration(item) &&
bt.isVariableDeclarator(item.declarations[0]) &&
bt.isIdentifier(item.declarations[0].id) &&
item.declarations[0].id.name === exportDefaultKey
)
) {
item.leadingComments = handleComments(item.leadingComments);
item.trailingComments = handleComments(item.trailingComments);
unknownCode.push(item);
}
}
});
整合 analyseResult 和 unknownCode

最终就是将 analyseResult 和 unknownCode 整合起来,放回到 result 中.此时 script 模块完全分析完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (unknownCode.length) {
result.customSource.unknownCode = unknownCode.map(item => {
return generate(item).code;
}).join("\n");
}
// 处理methods
if (analyseResult.methods) {
// 处理callOnMounted
const callOnMounteds = analyseResult.callOnMountedFn && Object.keys(analyseResult.callOnMountedFn) || [];
if (callOnMounteds.length) {
analyseResult.methods.forEach((item) => {
const index = callOnMounteds.indexOf(item.name);
if (index !== -1) {
item.callOnMounted = true;
delete analyseResult.callOnMountedFn[callOnMounteds[index]];
}
});
}
result.methods = analyseResult.methods;
}
...

分析 template

compiler.compile(template, [options])) 编译一个模板字符串并返回编译好的 JavaScript 代码。返回结果的格式如下:

1
2
3
4
5
6
{
ast: ?ASTElement, // 由模板元素解析为的抽象语法树 (AST)
render: string, // 主渲染函数的代码
staticRenderFns: Array<string>, // 可能存在的静态子树的渲染代码
errors: Array<string> // 可能存在的模板语法错误
}

我们先把 template 部分 compile 成 ast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (sfc.template) {
let templateAst = compile(sfc.template.content, {
comments: true,
outputSourceRange: true,
}).ast;

if (templateAst.if && templateAst.ifConditions) {
// 根元素不可以有if逻辑,需要添加一个div包裹
templateAst = compile(`<div>${sfc.template.content}</div>`, {
comments: true,
outputSourceRange: true,
}).ast;
}
// 解析模版时需要当前js中的部分变量,因此需要将info透传进去
const result: DataJson = templateAstRecursive(
templateAst,
"",
info,
keepTagType
);
Object.assign(info, result);
}

接下来就是递归遍历 ast 树,重新组装成 json schema,整个流程大概是这样子的

1.分析传入整个 ast

可参考AST Explorer
,对着这个 ast 树一步步进行分析

2.根据 ast 的 tag 获取组件

这里的组件是指上篇提到的系统支持的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function templateAstRecursive(
templateAst: any,
currentPath: string = "",
curJson: DataJson,
keepTagType: boolean = false
): DataJson {
// 根据ast的tag,获取对应的组件tag
let tagType = getComponent(templateAst);

// 仅第一层div识别为page
if (tagType === "page" && currentPath !== "") {
tagType = "div";
}
// 获取类型编译器
const typeCompiler = Compilers[tagType];
//如果没找到就设置tagType为custom-source
if (!typeCompiler) {
tagType = "custom-source";
}
}

其中 Compilers 是定义好的组件集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 无特殊解析需求直接使用BaseCompiler导出
* BaseCompiler —— 参数1:组件名,参数2:props属性,参数3:event属性
*/
export default {
"button": new BaseCompiler("button",
["type", "loading", "icon", "disabled", "htmlType", "ghost", "shape", "size", "block"],
["click"]),

"button-group": new BaseCompiler("button-group"),

"card": new BaseCompiler("card",
["title", "activeTabKey", "defaultActiveTabKey", "tabList", "type", "size", "loading", "bordered", "hoverable", "headStyle", "bodyStyle"],
["tabChange"]),
.....
}
3.要是组件不是系统支持的(也就是 tagType 为 custom-source)

直接源码反转(没有解析 importMaps,dataSource,body 等),不能解析出 json 树的结构,就只有 source

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
//注释组件
if (templateAst.isComment) {
result.source = templateAst.text;
result.type = "remark";
} else {
result.source = CustomSourceCompiler(templateAst);
}
//实现源码反转
export default function customSourceAstRecursive(templateAst: any): string {
const selfClosingTag = [
"br",
"hr",
"img",
"link",
"base",
"area",
"input",
"source",
];
let source: string = "";
if (templateAst.type === 1 && !templateAst.textAdapter) {
source += "<" + templateAst.tag;

// 追加attrs
for (const key of Object.keys(templateAst.attrsMap)) {
source += templateAst.attrsMap[key]
? ` ${key}="${templateAst.attrsMap[key]}"`
: ` ${key}`;
}
if (selfClosingTag.includes(templateAst.tag)) {
source += " />";
} else {
source += ">";
}

// 处理children
templateAst.children.forEach((item) => {
source += customSourceAstRecursive(item);
});

// 处理scopedSlots
if (templateAst.scopedSlots) {
for (const key of Object.keys(templateAst.scopedSlots)) {
source += customSourceAstRecursive(templateAst.scopedSlots[key]);
}
}
if (!selfClosingTag.includes(templateAst.tag)) {
source += `</${templateAst.tag}>`;
}
} else {
source += templateAst.text;
}
return source;
}
4. 组件是系统支持的,解析出 json 结构

typeCompiler 是对应的组件,组件里面有 parser 方法,parser 方法分析出当前 ast 的 attrs,attrsList,转回 props,events

1
2
3
4
if (typeCompiler) {
const info = typeCompiler.parser(templateAst, curJson);
result = Object.assign(result, info);
}
5. 处理 children,scopedSlots,以及 ifCondition

就是递归的遍历每个字节点,生成整个 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
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
const bodyContainer = info.bodyContainer || "body";
delete info.bodyContainer;
if (templateAst.children && templateAst.children.length && bodyContainer) {
const body = [];
let bodyItemIndex = 0;
// 递归处理标签节点
templateAst.children
.filter((item) => (item.type === 1 && !item.slotTarget) || item.isComment)
.forEach((item) => {
const child = templateAstRecursive(
item,
`${path}/${bodyContainer}/${bodyItemIndex++}`,
curJson,
keepTagType
);
body.push(child);
if (child.elseBody) {
body.push(...child.elseBody);
delete child.elseBody;
}
});
if (body.length) {
result[bodyContainer] = body;
}

// 处理slot节点
templateAst.children
.filter((item) => item.type === 1 && item.slotTarget)
.forEach((item) => {
const slotBodyContainer = `${JSON.parse(item.slotTarget)}Body`;
const slotBody = [];
let slotBodyItemIndex = 0;
if (item.tag === "template") {
// template类型取下面的children
item.children.forEach((subItem) => {
if (subItem.type === 1 || subItem.isComment) {
const child = templateAstRecursive(
subItem,
`${path}/${slotBodyContainer}/${slotBodyItemIndex++}`,
curJson,
keepTagType
);
slotBody.push(child);
if (child.elseBody) {
slotBody.push(...child.elseBody);
delete child.elseBody;
}
} else {
// 文本节点增加span套住
const textAdapter = parseExpressTextApdater(subItem);
if (textAdapter) {
const p = textAdapter.match(/^\{\{([^{}]*)\}\}$/);
const spanItem: any = {
type: "span",
path: `${path}/${slotBodyContainer}/${slotBodyItemIndex++}/span`,
};
if (p) {
spanItem.$textAdapter = p[1];
} else {
spanItem.textAdapter = textAdapter;
}
if (!spanItem.props) {
spanItem.props = {};
}
slotBody.push(spanItem);
}
}
});
} else {
// 非template类型取自身
const child = templateAstRecursive(
item,
`${path}/${slotBodyContainer}/${slotBodyItemIndex++}`,
curJson,
keepTagType
);
slotBody.push(child);
if (child.elseBody) {
slotBody.push(...child.elseBody);
delete child.elseBody;
}
}
if (slotBody.length) {
result[slotBodyContainer] = slotBody;
}
});
}

// 作用域slot需要插入到特定的xxxBody中
if (templateAst.scopedSlots) {
for (const key of Object.keys(templateAst.scopedSlots)) {
const extraBodyContainer =
JSON.parse(key) === "default" ? bodyContainer : `${JSON.parse(key)}Body`;
const extraBody = [];
let extraBodyItemIndex = 0;
const scopedSlots = templateAst.scopedSlots[key];

if (scopedSlots.tag === "template") {
// template类型取下面的children
scopedSlots.children.forEach((item) => {
if (item.type === 1 || item.isComment) {
const child = templateAstRecursive(
item,
`${path}/${extraBodyContainer}/${extraBodyItemIndex++}`,
curJson,
keepTagType
);
if (!child.props) {
child.props = {};
}
if (
!Object.is("_empty_", scopedSlots.slotScope) &&
scopedSlots.slotScope
) {
child.props["slot-scope"] = scopedSlots.slotScope;
}
extraBody.push(child);
if (child.elseBody) {
extraBody.push(...child.elseBody);
delete child.elseBody;
}
} else {
// 文本节点增加span套住
const textAdapter = parseExpressTextApdater(item);
if (textAdapter) {
const p = textAdapter.match(/^\{\{([^{}]*)\}\}$/);
const spanItem: any = {
type: "span",
path: `${path}/${extraBodyContainer}/${extraBodyItemIndex++}/span`,
};
if (p) {
spanItem.$textAdapter = p[1];
} else {
spanItem.textAdapter = textAdapter;
}
if (!spanItem.props) {
spanItem.props = {};
}
spanItem.props["slot-scope"] = scopedSlots.slotScope;
extraBody.push(spanItem);
}
}
});
} else {
// 非template类型取自身
const child = templateAstRecursive(
scopedSlots,
`${path}/${extraBodyContainer}/${extraBodyItemIndex++}`,
curJson,
keepTagType
);
extraBody.push(child);
if (child.elseBody) {
extraBody.push(...child.elseBody);
delete child.elseBody;
}
}
if (extraBody.length) {
result[extraBodyContainer] = [
...(result[extraBodyContainer] || []),
...extraBody,
];
}
}
}

// v-else和v-else-if的特殊处理
if (templateAst.if && templateAst.ifConditions) {
delete templateAst.ifConditions[0]; // 删除v-if自身元素
const elsePath = currentPath.split("/");
let elseIndex = parseInt(elsePath.pop(), 10) + 1;
const elseBody = [];
templateAst.ifConditions.forEach((item) => {
const elseItem = templateAstRecursive(
item.block,
`${elsePath.join("/")}/${elseIndex++}`,
curJson,
keepTagType
);
elseBody.push(elseItem);
});
if (elseBody.length) {
result.elseBody = elseBody;
}
}

分析 css

直接识别 lang,content,scope 然后返回.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 解析css,目前仅支持单个styles
if (sfc.styles && sfc.styles[0]) {
const { lang = "css", content, scoped = false } = sfc.styles[0];
info.cssConf = {
lang,
content: content.trim(),
scoped,
};
sfc.styles.splice(0, 1);
}
// 存在多个style
if (sfc.styles && sfc.styles.length) {
info.customSource.styles = sfc.styles.map((item) => {
const { lang = "css", content, scoped = false } = item;
return {
lang,
content: content.trim(),
scoped,
};
});
}

总结

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

有了 vue 反转回 json,就可以让用户在配置完后,在当前低代码编辑器 Pro code 直接写部分源码甚至在外部编辑器直接编写源码后再次回到代码平台的 Low code 模式二次配置,提供了灵活性。

参考文章和工具

  1. 你不知道的 Babel(7000 字,详解原理并手写插件)
  2. Vue 模板 AST 详解
  3. AST Explorer
  4. Vue Template Compiler
  5. Babel Plugin Handbook
查看评论