使用Node.js写一个代码生成器的方法步骤
背景
第一次接触代码生成器用的是动软代码生成器,数据库设计好之后,一键生成后端 curd代码。之后也用过 codesmith , t4。目前市面上也有很多优秀的代码生成器,而且大部分都提供可视化界面操作。
自己写一个的原因是因为要集成到自己写的一个小工具中,而且使用 node.js 这种动态脚本语言进行编写更加灵活。
原理
代码生成器的原理就是: 数据 + 模板 => 文件 。
数据 一般为数据库的表字段结构。
模板 的语法与使用的模板引擎有关。
使用模板引擎将 数据 和 模板 进行编译,编译后的内容输出到文件中就得到了一份代码文件。
功能
因为这个代码生成器是要集成到一个小工具内,这个工具的主要功能是启动一个 mock server 服务,包含curd功能,并且支持数据的持久化,文件变化的时候自动重启服务以最新的代码提供 api mock 服务。
代码生成器的功能就是根据配置的数据和模板,编译后将内容输出到指定的目录文件中。因为添加了新的文件,mock server 服务会自动重启。
还要支持模板的定制与开发,以及使用 cli 安装模板。
可以开发前端项目的模板,直接将编译后的内容输出到前端项目的相关目录下,webpack 的热更新功能也会起作用。
模板引擎
模板引擎使用的是。
使用的构建工具是 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) });
代码具体结构细节可以打开进行参照
为了支持模板的开发,以及更灵活的配置,我将代码生成的逻辑全都放在模板目录中。
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 项目的时候,指定使用相应的模板进行初始化,也就是安装相应的模板。
使用 node.js 写了一个 cli 工具 ,已发到 npm ,其功能包含下载指定的远程模板来初始化新的 lazy-mock 项目。代码参考( copy )了。代码不难,说下某些关键点。
安装 cli 工具:
npm install lazy-mock -g
使用模板初始化项目:
lazy-mock init d2-admin-pm my-project
是我为一个 已经写好的一个模板。
init 命令调用的是 中的逻辑:
#!/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的判断。可以使用现成的工具 , 把它读取的 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,同时还支持数据的持久化,又不需要安装数据库,还支持代码生成器的模板开发,欢迎试试。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。