打造一款適合本身的快速開發框架-前端篇之代碼生成器

前言

在後端篇中已對代碼生成器的原理進行了詳細介紹,同時也作了java和python版的實現。可是對於前端來講,僅靠後端提供的數據庫元數據仍是不足以知足代碼生成的要求的,並且先後端分離後,我的仍是想把代碼生成的活獨自交給前端維護,所以也爲前端單獨開發一個代碼生成器。前端

前端代碼生成原理

其實前端代碼生成的原理和後端的差很少,惟一區別可能就是關於元數據的來源上,這裏提供三個方案:vue

  1. 前端直接鏈接數據庫獲取元數據java

    該方案並非很建議,由於這樣前端小哥的權限過大,很差把控node

  2. 前端經過後端開放的接口獲取數據庫元數據python

    該方案能夠考慮,可是由於須要擴展元數據,僅該方式獲取的元數據也不全。git

  3. 前端本身定義元數據(基於數據庫元數據進行擴展)vue-cli

本文並無採用方案1和方案2,緣由是單獨使用該兩種方案獲取到的元數據都是不全的,不事後續作到頁面收集元數據時會考慮由方案2獲取最基礎的元數據,而後再基於基礎的元數據進行擴展。shell

頁面元數據

頁面元數據,好比:數據庫

屬性 類型 默認值 說明
isTree Boolean false 是否爲樹型列表
dialogWidth String 50% 彈框寬度
labelWidth String 100px 表單域標籤的寬度
hasDelete Boolean true 是否有刪除
hasAdd Boolean true 是否有添加
hasEdit Boolean true 是否有修改
formLayout String 1r1c 表單佈局(1r1c->一行一列,1r2c->一行兩列)

表單元數據

表單的基礎元數據npm

屬性 類型 默認值 說明
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樣例

打造一款適合本身的快速開發框架-前端篇之字典組件設計與實現

打造一款適合本身的快速開發框架-前端篇之下拉組件設計與實現

打造一款適合本身的快速開發框架-前端篇之選擇樹組件設計與實現

相關文章
相關標籤/搜索