基于vue3的低代码平台-源码生成(Java freemarker)
前言
源码生成是采用的数据+模板的形式,这就需要通过模板引擎将数据放入到事先写好的模板中去,本文使用的是java的freemarker模板引擎,作为一款老牌的模板引擎,在网上有诸多学习资料,再次就不赘述了,本文着重讲解如何通过json+freemarker的形式生成源码文件。
传入的json文件
其中包含普通组件button,容器组件layout
格式简介:
**“pageName”😗*页面名称
**“children”😗*页面或容器组件包含的组件列表
**”component"😗*组件名称(用于匹配组件的模板文件如ScButton.ftl)
**“attributes”😗*组件的属性列表
**“events”😗*组件的事件列表
**“ecVueInfo”😗*vue的
**“css”😗*vue的
[
{
"type": "page",
"pageName": "page1",
"label": "测试页面",
"children": [
{
"component": "ScButton",
"label": "按钮",
"animations": [],
"events": {
"click": {
"enable": true,
"method": "to"
}
},
"attributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"autofocus": false,
"native-type": "button",
"color": "",
"dark": false
},
"styles": {
"display": "inline-flex"
},
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"type": "common",
"bindClass": "",
"id": "de076629-485d-4a95-af40-c3d2b09fdc91",
"featherId": "editor"
},
{
"component": "ScLayout",
"label": "行",
"event": {},
"attributes": {
"justify": "start",
"align": "start",
"gutter": 0,
"col": [
{
"span": 24,
"offset": 0,
"push": 0,
"pull": 0,
"xs": null,
"sm": null,
"md": null,
"lg": null,
"xl": null
},
{
"span": 24,
"offset": 0,
"push": 0,
"pull": 0,
"xs": null,
"sm": null,
"md": null,
"lg": null,
"xl": null
}
]
},
"styles": {},
"children": [
{
"component": "container",
"attribute": "col",
"label": "列",
"id": "8535e009-de5b-4f4a-a631-91cd5b2cb948",
"event": {},
"attributes": {},
"styles": {},
"children": [
{
"component": "ScButton",
"label": "按钮",
"animations": [],
"events": {
"click": {
"enable": true,
"method": "test"
}
},
"attributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"autofocus": false,
"native-type": "button",
"color": "",
"dark": false
},
"styles": {
"display": "inline-flex"
},
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"type": "common",
"bindClass": "hello",
"id": "85a509c6-49b5-4de1-8a8b-b34dfa13cd62",
"featherId": "8535e009-de5b-4f4a-a631-91cd5b2cb948"
}
],
"featherId": "0f866713-bad6-40b2-bcfc-214fd32125c7",
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
}
},
{
"component": "container",
"attribute": "col",
"label": "列",
"id": "e92f60af-747f-4f61-8894-0109757f9068",
"event": {},
"attributes": {},
"styles": {},
"children": [
{
"component": "ScButton",
"label": "按钮",
"animations": [],
"events": {
"click": {
"enable": true,
"method": "test"
}
},
"attributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"autofocus": false,
"native-type": "button",
"color": "",
"dark": false
},
"styles": {
"display": "inline-flex"
},
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"type": "common",
"bindClass": "hello",
"id": "fffe3e81-3918-4e86-adcb-8a61d2e5c9c1",
"featherId": "e92f60af-747f-4f61-8894-0109757f9068"
}
],
"featherId": "0f866713-bad6-40b2-bcfc-214fd32125c7",
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
}
}
],
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"bindClass": "",
"id": "0f866713-bad6-40b2-bcfc-214fd32125c7",
"featherId": "editor"
}
],
"status": {
"active": false
},
"data": {},
"ecVueInfo": "export default{\nmounted(){\n},\ndata(){\nreturn{\n \n inputValue:\"hello code\"\n}},\nmethods:{\n test(){\n console.log('hello world')\n },\n to(){\n this.router('routerPage')\n }\n}}\n",
"EcVue": {
"$options": {
"methods": {}
},
"_data": {
"inputValue": "hello code"
},
"$refs": {}
},
"css": ".hello{\n height: 100px;\n}",
"id": "4a33d447-1f92-4ad7-8f9e-1e724dec147b"
},
]
解析json文件
由于json文件解析后是一个数组,需要先通过 JSONArray.parseArray()将json文件解析为List类型的对象,
freemarker 用于生成模板时需要传入Map<String,Object>类型的参数,故通过JSONObject.parseObject()将列表中的每一项转换为Map<String,Object>类型,
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class analysisJson {
static String path="";
private String JsonString;
public analysisJson(String path){
this.path = path;
}
public void readJson(String path){
//从给定位置获取文件
File file = new File(path);
BufferedReader reader = null;
//返回值,使用StringBuffer
StringBuffer data = new StringBuffer();
//
try {
reader = new BufferedReader(new FileReader(file));
//每次读取文件的缓存
String temp = null;
while((temp = reader.readLine()) != null){
data.append(temp);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
//关闭文件流
if (reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
this.JsonString =data.toString();
}
public List<Map<String,Object>> JsonToMap(){
this.readJson(path);
String JsonString=this.JsonString;
List<JSONObject> list = null;
List<Map<String,Object>> mapList = new ArrayList<Map<String,Object>>();
try{
// 将json字符串转换成jsonObject
//字符串转map
list = JSONArray.parseArray(JsonString,JSONObject.class);
list.forEach(item->{
mapList.add(JSONObject.parseObject(item.toJSONString(), new TypeReference<Map<String, Object>>(){}));
});
}catch (Exception e) {
e.printStackTrace();
}finally {
return mapList;
}
}
}
用到的freemarker指令介绍
if:if指令效果通java中的if一致,不过当判定条件为false时,对应位置会产生一个空白区域,这时就需要通过compress指令将空白强制删除掉(同时也会删除首行缩进),当需要判断value值的类型时,通过**?is_~~~**的形式来进行判断,
?is_boolean判断是否为boolean
?is_string判断是否为字符串,
?is_number判断是否为数字
更多类型判断可看:很少使用的和专家级的内建函数 - FreeMarker 中文官方参考手册 (foofun.cn)
示例:
<#if condition>
...
<#elseif condition2>
...
<#elseif condition3>
...
...
<#else>
...
</#if>
**include:**在include指令的位置引入其他模板文件
**${属性名}😗*获取对应属性的值
**list:*list指令可以类比为for循环,其中当循环的对象为Map(json {})类型时可以通过?keys as k的方式来调用对应的key值,当循环遍历时想要知道对应项的下标值可以通过_index*的方式。
示例:
<#list children as item>
${item_index}
<#include "./"+item.component+".ftl">
</#list>
<#list attributes?keys as k>
${k}
</#list>
**?string(‘true’, ‘false’)😗*当属性值为boolean类型时,无法直接通过 属性名进行引用的需要通过 ∗ {属性名}进行引用的需要通过* 属性名进行引用的需要通过∗{属性名?string(‘true’, ‘false’)}*的方式进行使用
示例:
boolenaValue:true
${boolenaValue?string('true', 'false')}
核心模板
首先我们会创建一个核心模板,也就是一个.vue文件的架子,通过include指令引入组件的模板,同时freemarker会将item作为参数给到组件模板中去,并通过item.属性的方式进行属性的调用。
<template>
<div>
<#list children as item>
<#include "./"+item.component+".ftl">
</#list>
</div>
</template>
<script>
${ecVueInfo}
</script>
<style scoped>
${css}
</style>
button模板
通过list循环将默认值为false,“”的属性过滤掉,这样生成的组件上将不会有过多的属性
<el-button
<#compress>
<#if item.bindClass!=''>class="${item.bindClass}"</#if>
<#list item.attributes?keys as k>
<#if item.attributes[k]?is_boolean && item.attributes[k] >:${k}=${item.attributes[k]?string('true', 'false')}<#elseif item.attributes[k]?is_string>${k}="${item.attributes[k]}"</#if>
</#list>
<#list item.events?keys as k>
<#if item.events[k].enable>@${k}="${item.events[k].method}"</#if>
</#list>
</#compress>
>
${item.attributes.label}</el-button>
Layout布局模板
layout模板是一个典型的容器模板,其中可以包含多个col,每个col中也包含多个组件
<el-row
<#compress>
<#if item.bindClass!=''>class="${item.bindClass}"</#if>
<#list item.attributes?keys as k>
<#if k!='col'>
<#if item.attributes[k]?is_boolean && item.attributes[k] >:${k}=${item.attributes[k]?string('true', 'false')}<#elseif item.attributes[k]?is_string>${k}="${item.attributes[k]}"</#if>
</#if>
</#list>
</#compress>
>
<#list item.children as ctem>
<el-col
<#compress>
<#list item.attributes['col'][ctem_index]?keys as k>
<#if k!='col'>
<#if item.attributes['col'][ctem_index][k]?is_boolean && item.attributes['col'][ctem_index][k] >
:${k}=${item.attributes['col'][ctem_index][k]?string('true', 'false')}
<#elseif item.attributes['col'][ctem_index][k]?is_string && item.attributes['col'][ctem_index][k] != ''>
${k}="${item.attributes['col'][ctem_index][k]}"
<#elseif item.attributes['col'][ctem_index][k]?is_number && item.attributes['col'][ctem_index][k] != 0>
:${k}="${item.attributes['col'][ctem_index][k]}"
</#if>
</#if>
</#list>
</#compress>
>
<#list ctem.children as item>
<#include "./"+item.component+".ftl">
</#list>
</el-col>
</#list>
</el-row>
调用
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.IOException;
public class createFile {
public static void main(String[] args) throws Exception {
BuildConfig buildConfig = new BuildConfig();
Template template = buildConfig.setTemplate();
analysisJson analysisJson=new analysisJson("src/main/resources/static/template/Json/page.json");
analysisJson.JsonToMap().forEach(item->{
//将数据填充到模板对象中生成文件
try {
buildConfig.setFileName(item.get("pageName").toString());
buildConfig.setFilenameSuffix(".vue");
template.process(item, buildConfig.setFileDemo());
} catch (TemplateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
结果
<template>
<div>
<el-button
color=""
icon=""
label="按钮"
loading-icon="Loading"
type="primary"
native-type="button"
size="default"
@click="to">
按钮
</el-button>
<el-row
justify="start"
align="start">
<el-col
:span="24">
<el-button
class="hello"
color=""
icon=""
label="按钮"
loading-icon="Loading"
type="primary"
native-type="button"
size="default"
@click="test">
按钮
</el-button>
</el-col>
<el-col
:span="24">
<el-button
class="hello"
color=""
icon=""
label="按钮"
loading-icon="Loading"
type="primary"
native-type="button"
size="default"
@click="test">
按钮
</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
mounted() {
},
data() {
return {
inputValue: "hello code"
}
},
methods: {
test() {
console.log('hello world')
},
to() {
// this.router('routerPage')
}
}
}
</script>
<style scoped>
.hello {
height: 100px;
}
</style>