1. 程式人生 > 前端設計 >打造一款適合自己的快速開發框架-前端篇之程式碼生成器

打造一款適合自己的快速開發框架-前端篇之程式碼生成器

前言

在後端篇中已對程式碼生成器的原理進行了詳細介紹,同時也做了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","defaultValue": "undefined"
}
複製程式碼
  • 多行文字

{
    "formtype": "textarea","required": false,"defaultValue": "undefined"
}
複製程式碼
  • 單選

{
    "formtype": "radio","defaultValue": "1","ext": {
        "items": [
            {
                "label": "男","value": "1"
        	},{
                "label": "女","value": "2"
        	}
        ]
    }
}
複製程式碼
  • 多選

{
    "formtype": "checkbox","defaultValue": ["1","2"],"ext": {
        "items": [
            { "label": "蘋果","value": "1" },{ "label": "梨","value": "2" },{ "label": "香蕉","value": "3" },{ "label": "橘子","value": "4" }
        ]
    }
}
複製程式碼
  • 下拉選擇

{
    "formtype": "select","ext": {
        "multiple": false,"items": [
            {
                "label": "蘋果",{
                "label": "梨","value": "2"
        	},{
                "label": "香蕉","value": "3"
        	},{
                "label": "橘子","value": "4"
        	}
        ]
    }
}
複製程式碼
  • 字典元件

    • 使用介面列舉類方式
    {
      "formtype": "dict","defaultValue": 1,"ext": {
          "dictKey": "sys_role_role_type","type": "map"
    }
    複製程式碼
    • 使用介面db儲存方式
    {
      "formtype": "dict","default": 1,"type": "db"
    }
    複製程式碼
    • 使用本地儲存方式
    {
      "formtype": "dict","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","defaultValue": "undefined","ext": {
        "url": "/sys/menu/list"  // 介面地址
    }
}
複製程式碼
  • 檔案上傳

{
    "formtype": "upload","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": "介面模板","templateFile": "service.art","targetPath": "src/api/<%=moduleName%>","targetFileName": "<%=moduleName%>.<%=table.tableCameName.replace(moduleName,'').slice(1)%>.service.js"
    },{
      "name": "新增模板","templateFile": "add.art","targetFileName": "add.vue"
    },{
      "name": "修改模板","templateFile": "edit.art","targetFileName": "edit.vue"
    },{
      "name": "詳情模板","templateFile": "details.art","targetFileName": "details.vue"
    },{
      "name": "表單元件","templateFile": "form.art",'').slice(1)%>/components","targetFileName": "form.vue"
    },{
      "name": "搜尋元件","templateFile": "search.art","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","javaType": "String"
      },{
        "primaryKey": false,"javaProperty": "name","formtype": "text","remark": "角色名稱","searchable": true,"searchType": "LIKE","show": true,"javaProperty": "roleKey","remark": "角色標識","searchable": false,"javaProperty": "roleType","formtype": "dict","remark": "角色型別","ext": {
          "dictKey": "sys_role_role_type"
        },"defaultValue": "10","searchType": "EQ","javaType": "Integer"
      },"javaProperty": "isEnabled","ext": {
          "dictKey": "yes_no"
        },"remark": "是否啟用","defaultValue": 2,"javaProperty": "remark","formtype": "textarea","remark": "備註","javaProperty": "createTime","remark": "建立時間","searchType":"BT","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樣例

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

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

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