打造一款適合自己的快速開發框架-前端篇之程式碼生成器
前言
在後端篇中已對程式碼生成器的原理進行了詳細介紹,同時也做了java和python版的實現。但是對於前端來說,僅靠後端提供的資料庫元資料還是不足以滿足程式碼生成的要求的,而且前後端分離後,個人還是想把程式碼生成的活獨自交給前端維護,因此也為前端單獨開發一個程式碼生成器。
前端程式碼生成原理
其實前端程式碼生成的原理和後端的差不多,唯一區別可能就是關於元資料的來源上,這裡提供三個方案:
-
前端直接連線資料庫獲取元資料
該方案並不是很建議,因為這樣前端小哥的許可權過大,不好把控
-
前端通過後端開放的介面獲取資料庫元資料
該方案可以考慮,但是因為需要擴充套件元資料,僅該方式獲取的元資料也不全。
-
前端自己定義元資料(基於資料庫元資料進行擴充套件)
本文並沒有採用方案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的模板引擎了
-
ejs
優點:ejs在使用vue-cli腳手架時自帶的模板引擎,如果使用該模板引擎,可以不再安裝其他依賴
缺點:其模板語法並不是很優雅,在模板製作中有點不是很方便
-
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; %>
複製程式碼
專案原始碼地址
- 後端
- 前端