在後端篇中已對代碼生成器的原理進行了詳細介紹,同時也作了java和python版的實現。可是對於前端來講,僅靠後端提供的數據庫元數據仍是不足以知足代碼生成的要求的,並且先後端分離後,我的仍是想把代碼生成的活獨自交給前端維護,所以也爲前端單獨開發一個代碼生成器。前端
其實前端代碼生成的原理和後端的差很少,惟一區別可能就是關於元數據的來源上,這裏提供三個方案:vue
前端直接鏈接數據庫獲取元數據java
該方案並非很建議,由於這樣前端小哥的權限過大,很差把控node
前端經過後端開放的接口獲取數據庫元數據python
該方案能夠考慮,可是由於須要擴展元數據,僅該方式獲取的元數據也不全。git
前端本身定義元數據(基於數據庫元數據進行擴展)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"
}
複製代碼
{
"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的模板引擎了
ejs
優勢:ejs在使用vue-cli腳手架時自帶的模板引擎,若是使用該模板引擎,能夠再也不安裝其餘依賴
缺點:其模板語法並非很優雅,在模板製做中有點不是很方便
art-template
優勢: art-template 支持標準語法與原始語法。標準語法可讓模板易讀寫。
缺點:無
經過對比,本框架選擇後者,模板易讀纔是關鍵。
編碼以前先介紹兩個依賴庫
上述說的nodejs模板引擎
npm install art-template --save-dev
複製代碼
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; %>
複製代碼