使用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 相关文章推荐
编写兼容IE和FireFox的脚本
May 18 Javascript
JavaScript 自动分号插入(JavaScript synat:auto semicolon insertion)
Nov 04 Javascript
iframe窗口高度自适应的实现方法
Jan 08 Javascript
table对象中的insertRow与deleteRow使用示例
Jan 26 Javascript
JavaScript闭包详解
Feb 02 Javascript
javascript 动态创建表格的2种方法总结
Mar 04 Javascript
javascript实现数独解法
Mar 14 Javascript
javascript实现随机显示星星特效
Jan 28 Javascript
分享javascript实现的冒泡排序代码并优化
Jun 05 Javascript
bootstrap模态框示例代码分享
May 17 Javascript
JavaScript订单操作小程序完整版
Jun 23 Javascript
如何在wxml中直接写js代码(wxs)
Nov 14 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编写大型网站问题集
2007/03/06 PHP
基于PHP服务端图片生成缩略图的方法详解
2013/06/20 PHP
解析php5配置使用pdo
2013/07/03 PHP
汇总PHPmailer群发Gmail的常见问题
2016/02/24 PHP
PHP使用Nginx实现反向代理
2017/09/20 PHP
setTimeout 不断吐食CPU的问题分析
2009/04/01 Javascript
Google AJAX 搜索 API实现代码
2010/11/17 Javascript
js 字符串转换成数字的三种方法
2013/03/23 Javascript
ajax请求乱码的解决方法(中文乱码)
2014/04/10 Javascript
JavaScript保留两位小数的2个自定义函数
2014/05/05 Javascript
node.js中的fs.readSync方法使用说明
2014/12/17 Javascript
js点击选择文本的方法
2015/02/09 Javascript
jquery简单实现带渐显效果的选项卡菜单代码
2015/09/01 Javascript
深入探究AngularJS框架中Scope对象的超级教程
2016/01/04 Javascript
jQuery的文档处理程序详解
2016/05/10 Javascript
深入浅析JS Function()构造函数
2016/08/22 Javascript
Vuejs第十篇之vuejs父子组件通信
2016/09/06 Javascript
微信小程序 form组件详解及简单实例
2017/01/10 Javascript
nodeJS实现简单网页爬虫功能的实例(分享)
2017/06/08 NodeJs
Vue 组件间的样式冲突污染
2017/08/31 Javascript
javascript数组定义的几种方法
2017/10/06 Javascript
浅谈ajax在jquery中的请求和servlet中的响应
2018/01/22 jQuery
JS随机数产生代码分享
2018/02/24 Javascript
小程序开发基础之view视图容器
2018/08/21 Javascript
vue如何使用外部特殊字体的操作
2020/07/30 Javascript
python3.5仿微软计算器程序
2020/03/30 Python
python如何让类支持比较运算
2018/03/20 Python
python中的decimal类型转换实例详解
2019/06/26 Python
Django 源码WSGI剖析过程详解
2019/08/05 Python
Django中使用session保持用户登陆连接的例子
2019/08/06 Python
python实现贪吃蛇游戏源码
2020/03/21 Python
YSL圣罗兰美妆英国官网:Yves Saint Laurent Beauty UK
2019/08/03 全球购物
高一化学教学反思
2014/02/05 职场文书
学习雷锋精神演讲稿
2014/05/10 职场文书
旷课检讨书
2015/01/26 职场文书
优秀学生主要事迹怎么写
2015/11/04 职场文书