使用Node.js写一个代码生成器的方法步骤


Posted in Javascript onMay 10, 2019

 背景

第一次接触代码生成器用的是动软代码生成器,数据库设计好之后,一键生成后端 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 是存放模板以及数据配置的目录。结构如下:

使用Node.js写一个代码生成器的方法步骤

只生成 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 数据转成我们需要的格式即可。

生成前端项目代码的时候,会遇到这种情况:

某个目录结构是这样的:

使用Node.js写一个代码生成器的方法步骤

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 。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery实现图片信息的浮动显示实例代码
Aug 28 Javascript
javascript右下角弹层及自动隐藏(自己编写)
Nov 20 Javascript
node.js中的querystring.stringify方法使用说明
Dec 10 Javascript
JS遍历页面所有对象属性及实现方法
Aug 01 Javascript
jquery对Json的各种遍历方法总结(必看篇)
Sep 29 Javascript
jQuery的三种bind/One/Live/On事件绑定使用方法
Feb 23 Javascript
three.js绘制地球、飞机与轨迹的效果示例
Feb 28 Javascript
form表单数据封装成json格式并提交给服务器的实现方法
Dec 14 Javascript
jquery radio 动态控制选中失效问题的解决方法
Feb 28 jQuery
结合Vue控制字符和字节的显示个数的示例
May 17 Javascript
微信小程序使用map组件实现检索(定位位置)周边的POI功能示例
Jan 23 Javascript
在Vue项目中,防止页面被缩放和放大示例
Oct 28 Javascript
Easyui 去除jquery-easui tab页div自带滚动条的方法
May 10 #jQuery
使用vue脚手架(vue-cli)搭建一个项目详解
May 09 #Javascript
Node.js实现用户评论社区功能(体验前后端开发的乐趣)
May 09 #Javascript
微信小程序中显示倒计时代码实例
May 09 #Javascript
微信小程序日历弹窗选择器代码实例
May 09 #Javascript
vue2 v-model/v-text 中使用过滤器的方法示例
May 09 #Javascript
快速搭建Node.js(Express)用户注册、登录以及授权的方法
May 09 #Javascript
You might like
php中is_null,empty,isset,unset 的区别详细介绍
2013/04/28 PHP
PHP 抽象方法与抽象类abstract关键字介绍及应用
2014/10/16 PHP
PHP+Ajax异步带进度条上传文件实例
2016/11/01 PHP
浅谈htmlentities 、htmlspecialchars、addslashes的使用方法
2016/12/09 PHP
phpstudy默认不支持64位php的解决方法
2017/02/20 PHP
JS JavaScript获取Url参数,src属性参数
2021/03/09 Javascript
jquery多浏览器捕捉回车事件代码
2010/06/22 Javascript
select标签模拟/美化方法采用JS外挂式插件
2013/04/01 Javascript
CSS鼠标响应事件经过、移动、点击示例介绍
2013/09/04 Javascript
获取鼠标在div中的相对位置的实现代码
2013/12/30 Javascript
jQuery判断checkbox(复选框)是否被选中以及全选、反选实现代码
2014/02/21 Javascript
Chrome下ifame父窗口调用子窗口的问题示例探讨
2014/03/17 Javascript
jQuery Easyui Datagrid实现单行的上移下移及保存移动的结果
2016/08/15 Javascript
jQuery实现输入框邮箱内容自动补全与上下翻动显示效果【附demo源码下载】
2016/09/20 Javascript
JS 获取HTML标签内的子节点的方法
2016/09/21 Javascript
d3.js实现简单的网络拓扑图实例代码
2016/11/06 Javascript
jQuery 的 ready()的纯js替代方法
2016/11/20 Javascript
利用js获取下拉框中所选的值
2016/12/01 Javascript
详解vue.js的事件处理器v-on:click
2017/06/27 Javascript
Cpage.js给组件绑定事件的实现代码
2017/08/31 Javascript
零基础写python爬虫之抓取百度贴吧并存储到本地txt文件改进版
2014/11/06 Python
Python批量重命名同一文件夹下文件的方法
2015/05/25 Python
详解Python中的__getitem__方法与slice对象的切片操作
2016/06/27 Python
Python3解释器知识点总结
2019/02/19 Python
Python bytes string相互转换过程解析
2020/03/05 Python
Python 列表中的修改、添加和删除元素的实现
2020/06/11 Python
pycharm专业版远程登录服务器的详细教程
2020/09/15 Python
美国男女折扣服饰百货连锁店:Stein Mart
2017/05/02 全球购物
意大利灯具购物网站:Lampade.it
2018/10/18 全球购物
英国第一独立滑雪板商店:The Snowboard Asylum
2020/01/16 全球购物
装修五一活动策划案
2014/01/23 职场文书
投标诚信承诺书
2014/05/26 职场文书
有限责任公司股东合作协议书
2014/12/02 职场文书
2014年小学辅导员工作总结
2014/12/23 职场文书
基于Python的EasyGUI学习实践
2021/05/07 Python
使用Docker容器部署rocketmq单机的全过程
2022/04/03 Servers