打造一款适合自己的快速开发框架-前端篇之代码生成器

5,376

前言

在后端篇中已对代码生成器的原理进行了详细介绍,同时也做了java和python版的实现。但是对于前端来说,仅靠后端提供的数据库元数据还是不足以满足代码生成的要求的,而且前后端分离后,个人还是想把代码生成的活独自交给前端维护,因此也为前端单独开发一个代码生成器。

前端代码生成原理

其实前端代码生成的原理和后端的差不多,唯一区别可能就是关于元数据的来源上,这里提供三个方案:

  1. 前端直接连接数据库获取元数据

    该方案并不是很建议,因为这样前端小哥的权限过大,不好把控

  2. 前端通过后端开放的接口获取数据库元数据

    该方案可以考虑,但是因为需要扩展元数据,仅该方式获取的元数据也不全。

  3. 前端自己定义元数据(基于数据库元数据进行扩展)

本文并没有采用方案1和方案2,原因是单独使用该两种方案获取到的元数据都是不全的,不过后续做到页面收集元数据时会考虑由方案2获取最基础的元数据,然后再基于基础的元数据进行扩展。

页面元数据

页面元数据,比如:

属性 类型 默认值 说明
isTree Boolean false 是否为树型列表
dialogWidth String 50% 弹框宽度
labelWidth String 100px 表单域标签的宽度
hasDelete Boolean true 是否有删除
hasAdd Boolean true 是否有添加
hasEdit Boolean true 是否有修改
formLayout String 1r1c 表单布局(1r1c->一行一列,1r2c->一行两列)

表单元数据

表单的基础元数据

属性 类型 默认值 说明
formtype String text 表单类型(详见下表)
required Boolean false 是否必填
defaultValue String undefined 默认值
labelWidth String 100px 表单域标签的宽度
show Boolean true 是否在列表中显示
searchable Boolean false 是否可搜索属性
searchType String EQ EQ/LIKE/BT等
ext Object 根据表单类型扩展的属性

表单类型:

表单类型 是否自定义组件 组件 说明
text el-input 单行文本
password el-input 密码输入框
textarea el-input 多行文本
radio el-radio 单选
checkbox el-checkbox 多选
select select 下拉组件
dict m-dect 字典组件
mselect m-select 自定义下拉组件
selectTree m-select-tree 选择关联树
upload m-upload 上传组件
ricttext m-rict-text 富文本组件
  • 单行文本

{
    "formtype": "text",
    "required": true,
    "defaultValue": "undefined"
}
  • 密码输入框

{
    "formtype": "password",
    "required": true,
    "defaultValue": "undefined"
}
  • 多行文本

{
    "formtype": "textarea",
    "required": false,
    "defaultValue": "undefined"
}
  • 单选

{
    "formtype": "radio",
    "required": false,
    "defaultValue": "1",
    "ext": {
        "items": [
            {
                "label": "男",
                "value": "1"
        	},
            {
                "label": "女",
                "value": "2"
        	}
        ]
    }
}
  • 多选

{
    "formtype": "checkbox",
    "required": false,
    "defaultValue": ["1","2"],
    "ext": {
        "items": [
            { "label": "苹果", "value": "1" },
            { "label": "梨", "value": "2" },
            { "label": "香蕉", "value": "3" },
            { "label": "橘子", "value": "4" }
        ]
    }
}
  • 下拉选择

{
    "formtype": "select",
    "required": false,
    "defaultValue": "1",
    "ext": {
        "multiple": false,
        "items": [
            {
                "label": "苹果",
                "value": "1"
        	},
            {
                "label": "梨",
                "value": "2"
        	},
            {
                "label": "香蕉",
                "value": "3"
        	},
            {
                "label": "橘子",
                "value": "4"
        	}
        ]
    }
}
  • 字典组件

    • 使用接口枚举类方式
    {
      "formtype": "dict",
      "required": false,
      "defaultValue": 1,
      "ext": {
          "dictKey": "sys_role_role_type",
          "type": "map"
    }
    
    • 使用接口db存储方式
    {
      "formtype": "dict",
      "required": false,
      "default": 1,
      "ext": {
          "dictKey": "sys_role_role_type",
          "type": "db"
    }
    
    • 使用本地存储方式
    {
      "formtype": "dict",
      "required": false,
      "defaultValue": 1,
      "ext": {
          "dictKey": "sys_role_role_type",
          "type": "local"
    }
    
  • 自定义下拉组件

{
    "formtype": "mselect",
    "required": false,
    "defaultValue": "undefined",
    "ext": {
        "valueKey": "id",               // 列表中选项的值对应的key
        "labelKey": "companyName",    // 列表中选项的值对应的key
        "searchKey": "name",
        "url": "/sys/company/list",  // 接口地址
        "placeholder": "请选择",		
        "multiple": false,           // 是否多选
    }
}
  • 选择树

{
    "formtype": "selectTree",
    "required": false,
    "defaultValue": "undefined",
    "ext": {
        "url": "/sys/menu/list"  // 接口地址
    }
}
  • 文件上传

{
    "formtype": "upload",
    "required": false,
    "defaultValue": "undefined",
    "ext": {
        "bizType": "业务类型"  // 业务类型
    }
}
  • 富文本

{
    "formType": "richtext"
}

关于模板引擎

前端肯定是使用nodejs的模板引擎了

  1. ejs

    优点:ejs在使用vue-cli脚手架时自带的模板引擎,如果使用该模板引擎,可以不再安装其他依赖

    缺点:其模板语法并不是很优雅,在模板制作中有点不是很方便

  2. art-template

    优点: art-template 支持标准语法与原始语法。标准语法可以让模板易读写。

    缺点:无

通过对比,本框架选择后者,模板易读才是关键。

开始编码

编码之前先介绍两个依赖库

  • art-template

上述说的nodejs模板引擎

npm install art-template --save-dev
  • commander

nodejs的命令行解析工具

npm install commander --save-dev

目录结构

├── generate
	├──	data	# 定义的元数据
		├──	sys_role.json
		└── ...
	├── templates	# 模板目录
		├──	add.art
		├──	details.art
		├──	edit.art
		├──	form.art
		├──	index.art
		├──	search.art
		└── service.js
	├── config.json	# 配置文件
	└── index.js	# 代码生成主函数

文件详解

  • generate/index.js

代码生成

const { program } = require('commander')
const template = require('art-template')
const path = require('path')
const fs = require('fs')
program
  .version('1.0.0')
  .requiredOption('-f, --file <type>', '数据文件')
  .option('-d, --debug <type>', '开启调试模式', 1)
  .option('-c, --config <type>', '配置文件', 'config.json')
  .option('-co, --covered <type>', '是否覆盖(1->覆盖,0->不覆盖)', 0)
  .parse(process.argv)

// 原始语法的界定符规则
template.defaults.rules[0].test = /<%(#?)((?:==|=#|[=-])?)[ \t]*([\w\W]*?)[ \t]*(-?)%>/
// 标准语法的界定符规则(默认的开始结束标签为{{和}},与vue的模板语法有冲突,所以修改一下<{ }>)
template.defaults.rules[1].test = /<{([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*}>/
// 设置模板引擎调试模式
template.defaults.debug = program.debug === 1
// 禁止压缩
template.defaults.minimize = false
/**
 * 主函数
 */
function main() {
  var dataFile = program.file
  if (!fs.existsSync(dataFile)) {
    dataFile = path.join(__dirname, `data/${dataFile}`)
    if (!fs.existsSync(dataFile)) {
      log(`${program.file}元数据文件不存在`)
      process.exit(1)
    }
  }
  var configFile = program.config
  if (!fs.existsSync(program.config)) {
    configFile = path.join(__dirname, configFile)
    if (!fs.existsSync(configFile)) {
      log(`${program.config}元数据文件不存在`)
      process.exit(1)
    }
  }
  var data = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
  var config = JSON.parse(fs.readFileSync(configFile, 'utf-8'))
  genCode(config, data)
}
/**
 * 生成代码
 * @param config 配置文件
 * @param {*} data 元数据
 */
function genCode(config, data) {
  config.templates.forEach(item => {
    if (item.selected) {
      var templateFile = item.templateFile
      var targetPath = template.render(item.targetPath, data)
      var targetFileName = template.render(item.targetFileName, data)
      log(`模板名称:${item.name}`)
      log(`模板文件:${templateFile}`)
      var content = template(path.join(__dirname, `templates/${templateFile}`), data)
      targetPath = path.join(path.resolve(__dirname, '..'), `${targetPath}`)
      if (!fs.existsSync(targetPath)) {
        mkdirs(targetPath)
      }
      var targetFile = path.join(targetPath, targetFileName)
      if (fs.existsSync(targetFile)) {
        if (program.covered === 1 || program.covered === '1') {
          log(`目标文件-被覆盖:${targetFile}`)
          writeFile(content, targetFile)
        } else {
          log(`目标文件-已存在:${targetFile}`)
        }
      } else {
        log(`目标文件-新生成:${targetFile}`)
        writeFile(content, targetFile)
      }
    }
  })
}
/**
 * 写文件
 * @param {*} content
 * @param {*} targetFile
 */
function writeFile(content, targetFile) {
  fs.writeFile(targetFile, content, {}, (err) => {
    if (err) {
      log(err)
    }
  })
}
/**
 * 创建多级目录
 * @param {} dirpath
 */
function mkdirs(dirpath) {
  if (!fs.existsSync(path.dirname(dirpath))) {
    mkdirs(path.dirname(dirpath))
  }
  fs.mkdirSync(dirpath)
}
/**
 * 日志打印
 * @param {} msg 打印的消息
 */
function log(msg) {
  if (program.debug === 1 || program.debug === '1') {
    console.log(msg)
  }
}
// 入口函数
main()

  • generate/config.json

配置文件,目前主要是配置模板

{
  "templates": [
    {
      "name": "首页模板",
      "selected": true,
      "templateFile": "index.art",
      "targetPath": "src/views/modules/<%=moduleName%>/<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>",
      "targetFileName": "index.vue"
    },
    {
      "name": "接口模板",
      "selected": true,
      "templateFile": "service.art",
      "targetPath": "src/api/<%=moduleName%>",
      "targetFileName": "<%=moduleName%>.<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>.service.js"
    },
    {
      "name": "添加模板",
      "selected": true,
      "templateFile": "add.art",
      "targetPath": "src/views/modules/<%=moduleName%>/<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>",
      "targetFileName": "add.vue"
    },
    {
      "name": "修改模板",
      "selected": true,
      "templateFile": "edit.art",
      "targetPath": "src/views/modules/<%=moduleName%>/<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>",
      "targetFileName": "edit.vue"
    },
    {
      "name": "详情模板",
      "selected": true,
      "templateFile": "details.art",
      "targetPath": "src/views/modules/<%=moduleName%>/<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>",
      "targetFileName": "details.vue"
    },
    {
      "name": "表单组件",
      "selected": true,
      "templateFile": "form.art",
      "targetPath": "src/views/modules/<%=moduleName%>/<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>/components",
      "targetFileName": "form.vue"
    },
    {
      "name": "搜索组件",
      "selected": true,
      "templateFile": "search.art",
      "targetPath": "src/views/modules/<%=moduleName%>/<%=table.tableCameName.replace(moduleName,'').charAt(0).toLowerCase()+table.tableCameName.replace(moduleName,'').slice(1)%>/components",
      "targetFileName": "search.vue"
    }
  ]
}
  • generate/sys_role.json

角色表的元数据,样例

{
  "moduleName": "sys",
  "table": {
    "fullscreen": false,
    "remark": "角色",
    "isTree": false,
    "dialogWidth": "50%",
    "labelWidth": 100,
    "hasDelete": true,
    "hasAdd": true,
    "hasEdit": true,
    "hasExport": false,
    "tableName": "sys_role",
    "className": "SysRole",
    "tableCameName": "sysRole",
    "columns": [
      {
        "primaryKey": true,
        "javaProperty": "id",
        "formtype": "none",
        "defaultValue": "undefined",
        "javaType": "String"
      },
      {
        "primaryKey": false,
        "javaProperty": "name",
        "formtype": "text",
        "remark": "角色名称",
        "defaultValue": "undefined",
        "searchable": true,
        "searchType": "LIKE",
        "required": true,
        "show": true,
        "javaType": "String"
      },
      {
        "primaryKey": false,
        "javaProperty": "roleKey",
        "formtype": "text",
        "remark": "角色标识",
        "defaultValue": "undefined",
        "searchable": false,
        "required": true,
        "show": true,
        "javaType": "String"
      },
      {
        "primaryKey": false,
        "javaProperty": "roleType",
        "formtype": "dict",
        "remark": "角色类型",
        "ext": {
          "dictKey": "sys_role_role_type"
        },
        "defaultValue": "10",
        "searchable": true,
        "searchType": "EQ",
        "required": true,
        "show": true,
        "javaType": "Integer"
      },
      {
        "primaryKey": false,
        "javaProperty": "isEnabled",
        "formtype": "dict",
        "ext": {
          "dictKey": "yes_no"
        },
        "remark": "是否启用",
        "defaultValue": 2,
        "searchable": true,
        "required": true,
        "show": true,
        "javaType": "Integer"
      },
      {
        "primaryKey": false,
        "javaProperty": "remark",
        "formtype": "textarea",
        "remark": "备注",
        "defaultValue": "undefined",
        "searchable": false,
        "required": false,
        "show": true,
        "javaType": "String"
      },
      {
        "primaryKey": false,
        "javaProperty": "createTime",
        "formtype": "none",
        "remark": "创建时间",
        "defaultValue": "undefined",
        "searchable": true,
        "searchType":"BT",
        "required": false,
        "show": true,
        "javaType": "Date"
      }
    ]
  }
}

运行说明

查看帮助

node generate/index.js -h
Usage: index [options]
Options:
  -V, --version          output the version number
  -f, --file <type>      数据文件
  -d, --debug <type>     开启调试模式 (default: 1)
  -c, --config <type>    配置文件 (default: "config.json")
  -co, --covered <type>  是否覆盖(1->覆盖,0->不覆盖) (default: 0)
  -h, --help             display help for command

指定某个元数据生成代码

node generate/index.js -f sys_role.json

指定某个元数据生成代码-覆盖式

node generate/index.js -f sys_role.json -co 1

小结

本文通过自定义元数据的方式来做代码生成器,对于一些基础的CURD需求,基本上可以做到生成一次,无需再修改。当然,对于复杂的需求还是需要手工去调整,不过这其实也大大的提高了开发效率。如果想尽可能的少修改,那么可以继续去补充元数据和完善模板。

最后附上模板语法

输出

标准语法

<{value}>
<{data.key}>
<{data['key']}>
<{a ? b : c}>
<{a || b}>
<{a + b}>

原始语法

<%= value %>
<%= data.key %>
<%= data['key'] %>
<%= a ? b : c %>
<%= a || b %>
<%= a + b %>

原文输出,不转义

标准语法

<{@ value }>

原始语法

<%- value %>

条件

标准语法

<{if value}> ... <{/if}>
<{if value}> ... <{else}> ... <{/if}>
<{if v1}> ... <{else if v2}> ... <{/if>}
<{if v1}> ... <{else if v2}> ... <{else}> ... <{/if}>

原始语法

<% if (value) { %> ... <% } %>
<% if (value) { %> ... <% } else { %>... <% } %>
<% if (v1) { %> ... <% } else if (v2) { %> ... <% } %>
<% if (v1) { %> ... <% } else if (v2) { %> ... <% }  else { %>... <% } %>

循环

标准语法

隐式定义,默认$value/$index
<{each target}>
    <{$index}} <{$value>}>
<{/each}>
显示定义
<{each target val index}>
    <{index}> <{val>}>
<{/each}>

原始语法

<% for(var i = 0; i < target.length; i++){ %>
    <%= i %> <%= target[i] %>
<% } %>

变量

标准语法

<{set temp = data.sub.content}>

原始语法

<% var temp = data.sub.content; %>

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-前端脚手架搭建

打造一款适合自己的快速开发框架-前端篇之登录与路由模块化

打造一款适合自己的快速开发框架-前端篇之框架分层及CURD样例

打造一款适合自己的快速开发框架-前端篇之字典组件设计与实现

打造一款适合自己的快速开发框架-前端篇之下拉组件设计与实现

打造一款适合自己的快速开发框架-前端篇之选择树组件设计与实现