前言
在上一篇基于freemarker生成源码后,考虑到还需要起一个后端服务才能生成代码并下载,增加的不少麻烦,便想着试试通过前端生成源码并导出。前端的源码导出不止可以用到低代码平台上,当前端项目有一些简易的导出工作也可以通过这个方式实现对应的功能,项目开源地址:https://gitee.com/xxt2286621910/easy-code要是对项目感兴趣,欢迎加入到项目的开发中来
简介
导出代码需要用到file-saver(用于生成下载的文件),jszip(用于将多个页面压缩使用),prettier(格式化前端文本代码),ecTemplate(用于装配json数据生成对应的文件)ecTemplate是由于没找到合适的便自己动手实现了一个简易的模板引擎,若有比较好的解决方案,希望大家可以在评论区给出。
功能实现
模板编辑
模板功能实现是基于 `` ,${}可以调用方法,或者直接使用三元表达式进行判断。自定义一个for循环的方法用于向模板中插入对应的属性值。includeTemplate 用于查询其他的模板用作模板嵌套使用,ComponentListStore.componentTemplates 用于保存组件的模板
**ecTemplateFor:**当传入的list为数组类型时向回调函数中传入value和index,若为Object则传递value和key
**includeTemplate:**当需要嵌套使用模板时便可传入对应的模板名,查询ComponentListStore.componentTemplates 并返回对应模板内容
import {coreTemplate} from "./coreTempalte/coreTemplate";
import {ComponentListStore} from "../stores/counter";
// 核心模板
export function ecTemplate(data){
return coreTemplate(data)
}
// 循环功能list 循环的列表、method回调函数
export function ecTemplateFor(list,method){
let result = ''
if(Array.isArray(list)){
let length = list.length
for (let i = 0; i < length; i++) {
if(method(list[i],i)!==''){
result = result+method(list[i],i)+'\n'
}
}
}else{
for(let key in list){
if(method(list[key],key)!==''){
result = result+method(list[key],key)+'\n'
}
}
}
return result
}
// 用于查询子模版。用作子模版嵌套使用
export function includeTemplate(template,param){
const componentListStore = ComponentListStore()
if(componentListStore.componentTemplates.hasOwnProperty(template)){
return componentListStore.componentTemplates[template](param)
}
return ""
}
// 生成通用模板 加载事件、属性、ref等
export function generalTemplate(param,filtrate){
let filtrates = {attr:[]}
if(filtrate){
filtrates = filtrate
}
return `\n
${param.styles && JSON.stringify(param.styles) !== "{}"?`:style='${JSON.stringify(param.styles)}'`:''}\n
${param.bindClass && param.bindClass !==""?`class="${param.bindClass}"`:''}\n
${generateAttribute(param.attributes,param.defaultAttributes,filtrates.attr)}\n
${ecTemplateFor(param.events, (item,k) => {
if (item.enable) {
return `@${k}="${item.method}"`
}
return ''
})}\n
`
}
// 属性加载方法 attributes 属性列表、defaultAttributes 默认属性值列表、filtrate 过滤不展示的属性
export function generateAttribute(attributes,defaultAttributes,filtrate){
return `\n
${ecTemplateFor(attributes, (item,k) => {
if(filtrate && filtrate.indexOf(k)!==-1){
return ''
}
if ((typeof item === "boolean" && defaultAttributes[k] !== item)) {
return `:${k}="${item}"`
}
if ((typeof item === "number" && item !== 0) && defaultAttributes[k] !== item) {
return `:${k}="${item}"`
}
if ((typeof item === "string") && item !== '' && defaultAttributes[k] !== item) {
if(k.includes("-slot")){
return ""
}
if(k.includes("Ref")){
return `ref="${item}"`
}else if(k === "modelValue"){
return `v-model="${item}"`
}else if(k.includes("-bindValue")){
return `:${k.replace("-bindValue","")}="${item}"`
}else{
return `${k}="${item}"`
}
}
return ''
})}`
}
ScButton模板
generalTemplate 加载通用的模板方法,用于传入属性、事件等配置,{attr:[“label”]}过滤掉attributes中的label属性
template: (param)=>{
return `
<el-button
${generalTemplate(param,{attr:["label"]})}
>
${param.attributes.label !==""?`${param.attributes.label }`:''}
</el-button>
`
}
ScLayout模板
template:(param)=>{
return `
<el-row
${generalTemplate(param)}
>
${ecTemplateFor(param.children, (citem,k) => {
return `<el-col ${generateAttribute(param.attributes['col'][k],param.defaultAttributes['col'][k],['span'])}
:span="${param.attributes['col'][k]["span"]}">
${ecTemplateFor(param.children[k].children,(item2)=>{
return includeTemplate(item2.component,item2)})}
</el-col>`
})}
</el-row>
`
}
格式化文本
由于模板生成的问题可能会导致输出的源码看起来乱糟糟,变将根据模板引擎生成的文本通过formatText方法格式化之后在进行输出。格式化代码使用的是prettier,prettier.format()返回的是promise函数,由于生成页面时需要同时生成多个页面,并将多个页面压缩到一个zip中便增加了同步方法。prettier官网Prettier 中文网 · Prettier 是一个“有态度”的代码格式化工具
安装prettier
npm install prettier
import prettier from "prettier/standalone"
import parserHtml from "prettier/plugins/html" // 格式化html
import parserCss from "prettier/plugins/postcss" // 格式化css文本
// 格式化文本text文本、type语言类型
export async function formatText(text, type) {
try {
if (!type || type === "javascript" ) return text
let result = ""
let plugin = null
let vueIndentScriptAndStyle = false
if (type === "css") {
plugin = parserCss
type = "css"
}
if (type === "html") {
type = "html"
plugin = parserHtml
vueIndentScriptAndStyle = true
}
if (plugin !== null) {
await prettier.format(text, {
parser: type,
plugins: [plugin],
vueIndentScriptAndStyle: vueIndentScriptAndStyle
}).then(data => {
result = data
})
}
return result
}catch (e){
console.log(e)
ElMessage({message: "有错误请检查后在进行保存", type: 'warning', duration: 2000, showClose: true,})
}
}
出码
通过循环遍历需要导出的数据,并根绝pageName参数作为文件名,页面数据通过模板引擎处理后,将结果通过formatText格式化最后将文本转为二进制对象,并放入zip.file中并导出
安装jszip
npm install jszip
// 输出源码
export async function generateCode(exportPage) {
// 加载zip模块用于压缩多个页面
let zip = new JSZip()
for (const item of exportPage) {
let data = await formatText(ecTemplate(item),"html")
let blob = new Blob([data], {type: "text/json;charset=utf-8"});
zip.file(item.pageName + ".vue", blob, {binary: true})
}
zip.generateAsync({type: "blob"}).then(content => {
// 生成二进制流
saveAs(content, "easyCode源码"); // 利用file-saver保存文件 自定义文件名
});
}
exportPage格式
[
{
"type": "page",
"pageName": "1",
"label": "1",
"children": [
{
"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": "e03373e1-d61b-421c-ad50-73ad0c905cd4",
"event": {},
"attributes": {},
"styles": {},
"children": [
{
"component": "ScButton",
"label": "按钮",
"animations": [],
"events": {
"click": {
"enable": false,
"method": ""
}
},
"attributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"color": "",
"dark": false
},
"styles": {},
"shapeStyles": {
"display": "inline-flex"
},
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"type": "common",
"bindClass": "",
"defaultAttributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"color": "",
"dark": false
},
"id": "1991ebc8-ed13-4a45-a631-13c96e915a06",
"featherId": "e03373e1-d61b-421c-ad50-73ad0c905cd4"
}
],
"featherId": "cff86dbe-8598-40d4-aca6-3e05c9dd3ccf",
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
}
},
{
"component": "container",
"attribute": "col",
"label": "列",
"id": "3212d0d4-4de0-462c-822d-68d740defa6c",
"event": {},
"attributes": {},
"styles": {},
"children": [],
"featherId": "cff86dbe-8598-40d4-aca6-3e05c9dd3ccf",
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
}
}
],
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"bindClass": "",
"defaultAttributes": {
"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
}
]
},
"id": "cff86dbe-8598-40d4-aca6-3e05c9dd3ccf",
"featherId": "editor"
}
],
"status": {
"active": false
},
"data": {},
"ecVueInfo": "export default{ \n name:\"1\", \n mounted(){ \n}, \n data(){\n return{ \n colorValue:\"yellow\",\n inputValue:\"hello\",\n data:[\n {value:\"name\",label:\"name\"}\n ]\n }}, \n methods:{\n} \n}",
"EcVue": {
"$options": {
"name": "1",
"methods": {}
},
"_data": {
"colorValue": "yellow",
"inputValue": "hello",
"data": [
{
"value": "name",
"label": "name"
}
]
},
"$refs": {}
},
"css": "",
"id": "c29ec373-1b4d-458b-bd08-b0565f72c30e"
},
{
"type": "page",
"pageName": "2",
"label": "2",
"children": [
{
"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": "9e766ea8-202a-4c9c-ab36-079c14784240",
"event": {},
"attributes": {},
"styles": {},
"children": [],
"featherId": "4f82046c-8379-45af-b1b9-9dbb52563923",
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
}
},
{
"component": "container",
"attribute": "col",
"label": "列",
"id": "daa4fb26-df81-4cf1-a09b-6b678d0e71cb",
"event": {},
"attributes": {},
"styles": {},
"children": [],
"featherId": "4f82046c-8379-45af-b1b9-9dbb52563923",
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
}
}
],
"type": "container",
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"bindClass": "",
"defaultAttributes": {
"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
}
]
},
"id": "4f82046c-8379-45af-b1b9-9dbb52563923",
"featherId": "editor"
},
{
"component": "ScButton",
"label": "按钮",
"animations": [],
"events": {
"click": {
"enable": false,
"method": ""
}
},
"attributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"color": "",
"dark": false
},
"styles": {},
"shapeStyles": {
"display": "inline-flex"
},
"status": {
"active": false,
"activeContainer": false,
"isHidden": false,
"lock": false
},
"type": "common",
"bindClass": "",
"defaultAttributes": {
"type": "primary",
"size": "default",
"label": "按钮",
"plain": false,
"round": false,
"disabled": false,
"circle": false,
"loading": false,
"loading-icon": "Loading",
"icon": "",
"autoInsertSpace": false,
"color": "",
"dark": false
},
"id": "eb016967-e3ca-44c3-8afe-3f46ae19e463",
"featherId": "editor"
}
],
"status": {
"active": false
},
"data": {},
"ecVueInfo": "export default{ \n name:\"2\", \n mounted(){ \n}, \n data(){\n return{ \n }}, \n methods:{\n} \n}",
"EcVue": {
"$options": {
"methods": {}
},
"_data": {},
"$refs": {}
},
"css": "",
"id": "7df39bbe-edaf-4bb8-a04f-7dc1b7d70b89"
}
]
结果
<template>
<div>
<el-row>
<el-col :span="24"> </el-col>
<el-col :span="24"> </el-col>
</el-row>
<el-button> 按钮 </el-button>
</div>
</template>
<script>
export default{
name:"2",
mounted(){
},
data(){
return{
}},
methods:{
}
}
</script>
<style scoped></style>