第一次接觸代碼生成器用的是動軟代碼生成器,數據庫設計好以後,一鍵生成後端 curd代碼。以後也用過 CodeSmith , T4。目前市面上也有不少優秀的代碼生成器,並且大部分都提供可視化界面操做。html
本身寫一個的緣由是由於要集成到本身寫的一個小工具中,並且使用 Node.js 這種動態腳本語言進行編寫更加靈活。前端
代碼生成器的原理就是:數據 + 模板 => 文件
。vue
數據
通常爲數據庫的表字段結構。node
模板
的語法與使用的模板引擎有關。webpack
使用模板引擎將數據
和模板
進行編譯,編譯後的內容輸出到文件中就獲得了一份代碼文件。git
由於這個代碼生成器是要集成到一個小工具 lazy-mock 內,這個工具的主要功能是啓動一個 mock server 服務,包含curd功能,而且支持數據的持久化,文件變化的時候自動重啓服務以最新的代碼提供 api mock 服務。github
代碼生成器的功能就是根據配置的數據和模板,編譯後將內容輸出到指定的目錄文件中。由於添加了新的文件,mock server 服務會自動重啓。web
還要支持模板的定製與開發,以及使用 CLI 安裝模板。sql
能夠開發前端項目的模板,直接將編譯後的內容輸出到前端項目的相關目錄下,webpack 的熱更新功能也會起做用。vue-cli
模板引擎使用的是 nunjucks。
lazy-mock 使用的構建工具是 gulp,使用 gulp-nodemon 實現 mock-server 服務的自動重啓。因此這裏使用 gulp-nunjucks-render 配合 gulp 的構建流程。
編寫一個 gulp task :
const rename = require('gulp-rename')
const nunjucksRender = require('gulp-nunjucks-render')
const codeGenerate = require('./templates/generate')
const ServerFullPath = require('./package.json').ServerFullPath; //mock -server項目的絕對路徑
const FrontendFullPath = require('./package.json').FrontendFullPath; //前端項目的絕對路徑
const nunjucksRenderConfig = {
path: 'templates/server',
envOptions: {
tags: {
blockStart: '<%',
blockEnd: '%>',
variableStart: '<$',
variableEnd: '$>',
commentStart: '<#',
commentEnd: '#>'
},
},
ext: '.js',
//以上是 nunjucks 的配置
ServerFullPath,
FrontendFullPath
}
gulp.task('code', function () {
require('events').EventEmitter.defaultMaxListeners = 0
return codeGenerate(gulp, nunjucksRender, rename, nunjucksRenderConfig)
});
複製代碼
代碼具體結構細節能夠打開 lazy-mock 進行參照
爲了支持模板的開發,以及更靈活的配置,我將代碼生成的邏輯全都放在模板目錄中。
templates
是存放模板以及數據配置的目錄。結構以下:
只生成 lazy-mock 代碼的模板中 :
generate.js
的內容以下:
const path = require('path')
const CodeGenerateConfig = require('./config').default;
const Model = CodeGenerateConfig.model;
module.exports = function generate(gulp, nunjucksRender, rename, nunjucksRenderConfig) {
nunjucksRenderConfig.data = {
model: CodeGenerateConfig.model,
config: CodeGenerateConfig.config
}
const ServerProjectRootPath = nunjucksRenderConfig.ServerFullPath;
//server
const serverTemplatePath = 'templates/server/'
gulp.src(`${serverTemplatePath}controller.njk`)
.pipe(nunjucksRender(nunjucksRenderConfig))
.pipe(rename(Model.name + '.js'))
.pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));
gulp.src(`${serverTemplatePath}service.njk`)
.pipe(nunjucksRender(nunjucksRenderConfig))
.pipe(rename(Model.name + 'Service.js'))
.pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ServiceRelativePath));
gulp.src(`${serverTemplatePath}model.njk`)
.pipe(nunjucksRender(nunjucksRenderConfig))
.pipe(rename(Model.name + 'Model.js'))
.pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ModelRelativePath));
gulp.src(`${serverTemplatePath}db.njk`)
.pipe(nunjucksRender(nunjucksRenderConfig))
.pipe(rename(Model.name + '_db.json'))
.pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.DBRelativePath));
return gulp.src(`${serverTemplatePath}route.njk`)
.pipe(nunjucksRender(nunjucksRenderConfig))
.pipe(rename(Model.name + 'Route.js'))
.pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.RouteRelativePath));
}
複製代碼
相似:
gulp.src(`${serverTemplatePath}controller.njk`)
.pipe(nunjucksRender(nunjucksRenderConfig))
.pipe(rename(Model.name + '.js'))
.pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));
複製代碼
表示使用 controller.njk 做爲模板,nunjucksRenderConfig做爲數據(模板內能夠獲取到 nunjucksRenderConfig 屬性 data 上的數據)。編譯後進行文件重命名,並保存到指定目錄下。
model.js
的內容以下:
var shortid = require('shortid')
var Mock = require('mockjs')
var Random = Mock.Random
//必須包含字段id
export default {
name: "book",
Name: "Book",
properties: [
{
key: "id",
title: "id"
},
{
key: "name",
title: "書名"
},
{
key: "author",
title: "做者"
},
{
key: "press",
title: "出版社"
}
],
buildMockData: function () {//不須要生成設爲false
let data = []
for (let i = 0; i < 100; i++) {
data.push({
id: shortid.generate(),
name: Random.cword(5, 7),
author: Random.cname(),
press: Random.cword(5, 7)
})
}
return data
}
}
複製代碼
模板中使用最多的就是這個數據,也是生成新代碼須要配置的地方,好比這裏配置的是 book ,生成的就是關於 book 的curd 的 mock 服務。要生成別的,修改後執行生成命令便可。
buildMockData 函數的做用是生成 mock 服務須要的隨機數據,在 db.njk 模板中會使用:
{
"<$ model.name $>":<% if model.buildMockData %><$ model.buildMockData()|dump|safe $><% else %>[]<% endif %>
}
複製代碼
這也是 nunjucks 如何在模板中執行函數
config.js
的內容以下:
export default {
//server
RouteRelativePath: '/src/routes/',
ControllerRelativePath: '/src/controllers/',
ServiceRelativePath: '/src/services/',
ModelRelativePath: '/src/models/',
DBRelativePath: '/src/db/'
}
複製代碼
配置相應的模板編譯後保存的位置。
config/index.js
的內容以下:
import model from './model';
import config from './config';
export default {
model,
config
}
複製代碼
針對 lazy-mock 的代碼生成的功能就已經完成了,要實現模板的定製直接修改模板文件便可,好比要修改 mock server 服務 api 的接口定義,直接修改 route.njk 文件:
import KoaRouter from 'koa-router'
import controllers from '../controllers/index.js'
import PermissionCheck from '../middleware/PermissionCheck'
const router = new KoaRouter()
router
.get('/<$ model.name $>/paged', controllers.<$model.name $>.get<$ model.Name $>PagedList)
.get('/<$ model.name $>/:id', controllers.<$ model.name $>.get<$ model.Name $>)
.del('/<$ model.name $>/del', controllers.<$ model.name $>.del<$ model.Name $>)
.del('/<$ model.name $>/batchdel', controllers.<$ model.name $>.del<$ model.Name $>s)
.post('/<$ model.name $>/save', controllers.<$ model.name $>.save<$ model.Name $>)
module.exports = router
複製代碼
不一樣的項目,代碼結構是不同的,每次直接修改模板文件會很麻煩。
須要提供這樣的功能:針對不一樣的項目開發一套獨立的模板,支持模板的安裝。
代碼生成的相關邏輯都在模板目錄的文件中,模板開發沒有什麼規則限制,只要保證目錄名爲 templates
,generate.js
中導出generate
函數便可。
模板的安裝原理就是將模板目錄中的文件所有覆蓋掉便可。不過具體的安裝分爲本地安裝與在線安裝。
以前已經說了,這個代碼生成器是集成在 lazy-mock 中的,個人作法是在初始化一個新 lazy-mock 項目的時候,指定使用相應的模板進行初始化,也就是安裝相應的模板。
使用 Node.js 寫了一個 CLI 工具 lazy-mock-cli,已發到 npm ,其功能包含下載指定的遠程模板來初始化新的 lazy-mock 項目。代碼參考( copy )了 vue-cli2。代碼不難,說下某些關鍵點。
安裝 CLI 工具:
npm install lazy-mock -g
複製代碼
使用模板初始化項目:
lazy-mock init d2-admin-pm my-project
複製代碼
d2-admin-pm 是我爲一個前端項目已經寫好的一個模板。
init
命令調用的是 lazy-mock-init.js 中的邏輯:
#!/usr/bin/env node
const download = require('download-git-repo')
const program = require('commander')
const ora = require('ora')
const exists = require('fs').existsSync
const rm = require('rimraf').sync
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const home = require('user-home')
const fse = require('fs-extra')
const tildify = require('tildify')
const cliSpinners = require('cli-spinners');
const logger = require('../lib/logger')
const localPath = require('../lib/local-path')
const isLocalPath = localPath.isLocalPath
const getTemplatePath = localPath.getTemplatePath
program.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')
program.on('--help', () => {
console.log(' Examples:')
console.log()
console.log(chalk.gray(' # create a new project with an official template'))
console.log(' $ lazy-mock init d2-admin-pm my-project')
console.log()
console.log(chalk.gray(' # create a new project straight from a github template'))
console.log(' $ vue init username/repo my-project')
console.log()
})
function help() {
program.parse(process.argv)
if (program.args.length < 1) return program.help()
}
help()
//模板
let template = program.args[0]
//判斷是否使用官方模板
const hasSlash = template.indexOf('/') > -1
//項目名稱
const rawName = program.args[1]
//在當前文件下建立
const inPlace = !rawName || rawName === '.'
//項目名稱
const name = inPlace ? path.relative('../', process.cwd()) : rawName
//建立項目完整目標位置
const to = path.resolve(rawName || '.')
const clone = program.clone || false
//緩存位置
const serverTmp = path.join(home, '.lazy-mock', 'sever')
const tmp = path.join(home, '.lazy-mock', 'templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
template = tmp
}
//判斷是否當前目錄下初始化或者覆蓋已有目錄
if (inPlace || exists(to)) {
inquirer.prompt([{
type: 'confirm',
message: inPlace
? 'Generate project in current directory?'
: 'Target directory exists. Continue?',
name: 'ok'
}]).then(answers => {
if (answers.ok) {
run()
}
}).catch(logger.fatal)
} else {
run()
}
function run() {
//使用本地緩存
if (isLocalPath(template)) {
const templatePath = getTemplatePath(template)
if (exists(templatePath)) {
generate(name, templatePath, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s"', name)
})
} else {
logger.fatal('Local template "%s" not found.', template)
}
} else {
if (!hasSlash) {
//使用官方模板
const officialTemplate = 'lazy-mock-templates/' + template
downloadAndGenerate(officialTemplate)
} else {
downloadAndGenerate(template)
}
}
}
function downloadAndGenerate(template) {
downloadServer(() => {
downloadTemplate(template)
})
}
function downloadServer(done) {
const spinner = ora('downloading server')
spinner.spinner = cliSpinners.bouncingBall
spinner.start()
if (exists(serverTmp)) rm(serverTmp)
download('wjkang/lazy-mock', serverTmp, { clone }, err => {
spinner.stop()
if (err) logger.fatal('Failed to download server ' + template + ': ' + err.message.trim())
done()
})
}
function downloadTemplate(template) {
const spinner = ora('downloading template')
spinner.spinner = cliSpinners.bouncingBall
spinner.start()
if (exists(tmp)) rm(tmp)
download(template, tmp, { clone }, err => {
spinner.stop()
if (err) logger.fatal('Failed to download template ' + template + ': ' + err.message.trim())
generate(name, tmp, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s"', name)
})
})
}
function generate(name, src, dest, done) {
try {
fse.removeSync(path.join(serverTmp, 'templates'))
const packageObj = fse.readJsonSync(path.join(serverTmp, 'package.json'))
packageObj.name = name
packageObj.author = ""
packageObj.description = ""
packageObj.ServerFullPath = path.join(dest)
packageObj.FrontendFullPath = path.join(dest, "front-page")
fse.writeJsonSync(path.join(serverTmp, 'package.json'), packageObj, { spaces: 2 })
fse.copySync(serverTmp, dest)
fse.copySync(path.join(src, 'templates'), path.join(dest, 'templates'))
} catch (err) {
done(err)
return
}
done()
}
複製代碼
判斷了是使用本地緩存的模板仍是拉取最新的模板,拉取線上模板時是從官方倉庫拉取仍是從別的倉庫拉取。
目前代碼生成的相關數據並非來源於數據庫,而是在 model.js
中簡單配置的,緣由是我認爲一個 mock server 不須要數據庫,lazy-mock 確實如此。
可是若是寫一個正兒八經的代碼生成器,那確定是須要根據已經設計好的數據庫表來生成代碼的。那麼就須要鏈接數據庫,讀取數據表的字段信息,好比字段名稱,字段類型,字段描述等。而不一樣關係型數據庫,讀取表字段信息的 sql 是不同的,因此還要寫一堆balabala的判斷。可使用現成的工具 sequelize-auto , 把它讀取的 model 數據轉成咱們須要的格式便可。
生成前端項目代碼的時候,會遇到這種狀況:
某個目錄結構是這樣的:
index.js
的內容:
import layoutHeaderAside from '@/layout/header-aside'
export default {
"layoutHeaderAside": layoutHeaderAside,
"menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
"route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
"role": () => import(/* webpackChunkName: "role" */'@/pages/sys/role'),
"user": () => import(/* webpackChunkName: "user" */'@/pages/sys/user'),
"interface": () => import(/* webpackChunkName: "interface" */'@/pages/sys/interface')
}
複製代碼
若是添加一個 book 就須要在這裏加上"book": () => import(/* webpackChunkName: "book" */'@/pages/sys/book')
這一行內容也是能夠經過配置模板來生成的,好比模板內容爲:
"<$ model.name $>": () => import(/* webpackChunkName: "<$ model.name $>" */'@/pages<$ model.module $><$ model.name $>')
複製代碼
可是生成的內容怎麼加到index.js
中呢?
第一種方法:複製粘貼
第二種方法:
這部分的模板爲 routerMapComponent.njk :
export default {
"<$ model.name $>": () => import(/* webpackChunkName: "<$ model.name $>" */'@/pages<$ model.module $><$ model.name $>')
}
複製代碼
編譯後文件保存到 routerMapComponents 目錄下,好比 book.js
修改 index.js :
const files = require.context('./', true, /\.js$/);
import layoutHeaderAside from '@/layout/header-aside'
let componentMaps = {
"layoutHeaderAside": layoutHeaderAside,
"menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
"route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
"role": () => import(/* webpackChunkName: "role" */'@/pages/sys/role'),
"user": () => import(/* webpackChunkName: "user" */'@/pages/sys/user'),
"interface": () => import(/* webpackChunkName: "interface" */'@/pages/sys/interface'),
}
files.keys().forEach((key) => {
if (key === './index.js') return
Object.assign(componentMaps, files(key).default)
})
export default componentMaps
複製代碼
使用了 require.context
我目前也是使用了這種方法
第三種方法:
開發模板的時候,作特殊處理,讀取原有 index.js 的內容,按行進行分割,在數組的最後一個元素以前插入新生成的內容,注意逗號的處理,將新數組內容從新寫入 index.js 中,注意換行。
若是你想要快速的建立一個 mock-server,同時還支持數據的持久化,又不須要安裝數據庫,還支持代碼生成器的模板開發,歡迎試試 lazy-mock 。