您好,登錄后才能下訂單哦!
背景
第一次接觸代碼生成器用的是動軟代碼生成器,數據庫設計好之后,一鍵生成后端 curd代碼。之后也用過 CodeSmith , T4。目前市面上也有很多優秀的代碼生成器,而且大部分都提供可視化界面操作。
自己寫一個的原因是因為要集成到自己寫的一個小工具中,而且使用 Node.js 這種動態腳本語言進行編寫更加靈活。
原理
代碼生成器的原理就是: 數據 + 模板 => 文件 。
數據 一般為數據庫的表字段結構。
模板 的語法與使用的模板引擎有關。
使用模板引擎將 數據 和 模板 進行編譯,編譯后的內容輸出到文件中就得到了一份代碼文件。
功能
因為這個代碼生成器是要集成到一個小工具lazy-mock 內,這個工具的主要功能是啟動一個 mock server 服務,包含curd功能,并且支持數據的持久化,文件變化的時候自動重啟服務以最新的代碼提供 api mock 服務。
代碼生成器的功能就是根據配置的數據和模板,編譯后將內容輸出到指定的目錄文件中。因為添加了新的文件,mock server 服務會自動重啟。
還要支持模板的定制與開發,以及使用 CLI 安裝模板。
可以開發前端項目的模板,直接將編譯后的內容輸出到前端項目的相關目錄下,webpack 的熱更新功能也會起作用。
模板引擎
模板引擎使用的是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 。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。