经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Node.js » 查看文章
使用Node.js写一个代码生成器的方法步骤
来源:jb51  时间:2019/5/10 10:18:25  对本文有异议

 背景

第一次接触代码生成器用的是动软代码生成器,数据库设计好之后,一键生成后端 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 :

  1. const rename = require('gulp-rename')
  2. const nunjucksRender = require('gulp-nunjucks-render')
  3. const codeGenerate = require('./templates/generate')
  4. const ServerFullPath = require('./package.json').ServerFullPath; //mock -server项目的绝对路径
  5. const FrontendFullPath = require('./package.json').FrontendFullPath; //前端项目的绝对路径
  6. const nunjucksRenderConfig = {
  7. path: 'templates/server',
  8. envOptions: {
  9. tags: {
  10. blockStart: '<%',
  11. blockEnd: '%>',
  12. variableStart: '<$',
  13. variableEnd: '$>',
  14. commentStart: '<#',
  15. commentEnd: '#>'
  16. },
  17. },
  18. ext: '.js',
  19. //以上是 nunjucks 的配置
  20. ServerFullPath,
  21. FrontendFullPath
  22. }
  23. gulp.task('code', function () {
  24. require('events').EventEmitter.defaultMaxListeners = 0
  25. return codeGenerate(gulp, nunjucksRender, rename, nunjucksRenderConfig)
  26. });

代码具体结构细节可以打开lazy-mock 进行参照

为了支持模板的开发,以及更灵活的配置,我将代码生成的逻辑全都放在模板目录中。

templates 是存放模板以及数据配置的目录。结构如下:

只生成 lazy-mock 代码的模板中 :

generate.js 的内容如下:

  1. const path = require('path')
  2. const CodeGenerateConfig = require('./config').default;
  3. const Model = CodeGenerateConfig.model;
  4.  
  5. module.exports = function generate(gulp, nunjucksRender, rename, nunjucksRenderConfig) {
  6. nunjucksRenderConfig.data = {
  7. model: CodeGenerateConfig.model,
  8. config: CodeGenerateConfig.config
  9. }
  10. const ServerProjectRootPath = nunjucksRenderConfig.ServerFullPath;
  11. //server
  12. const serverTemplatePath = 'templates/server/'
  13. gulp.src(`${serverTemplatePath}controller.njk`)
  14. .pipe(nunjucksRender(nunjucksRenderConfig))
  15. .pipe(rename(Model.name + '.js'))
  16. .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));
  17.  
  18. gulp.src(`${serverTemplatePath}service.njk`)
  19. .pipe(nunjucksRender(nunjucksRenderConfig))
  20. .pipe(rename(Model.name + 'Service.js'))
  21. .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ServiceRelativePath));
  22.  
  23. gulp.src(`${serverTemplatePath}model.njk`)
  24. .pipe(nunjucksRender(nunjucksRenderConfig))
  25. .pipe(rename(Model.name + 'Model.js'))
  26. .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ModelRelativePath));
  27.  
  28. gulp.src(`${serverTemplatePath}db.njk`)
  29. .pipe(nunjucksRender(nunjucksRenderConfig))
  30. .pipe(rename(Model.name + '_db.json'))
  31. .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.DBRelativePath));
  32.  
  33. return gulp.src(`${serverTemplatePath}route.njk`)
  34. .pipe(nunjucksRender(nunjucksRenderConfig))
  35. .pipe(rename(Model.name + 'Route.js'))
  36. .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.RouteRelativePath));
  37. }
  38.  

类似:

  1. gulp.src(`${serverTemplatePath}controller.njk`)
  2. .pipe(nunjucksRender(nunjucksRenderConfig))
  3. .pipe(rename(Model.name + '.js'))
  4. .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));

表示使用 controller.njk 作为模板,nunjucksRenderConfig作为数据(模板内可以获取到 nunjucksRenderConfig 属性 data 上的数据)。编译后进行文件重命名,并保存到指定目录下。

model.js 的内容如下:

  1. var shortid = require('shortid')
  2. var Mock = require('mockjs')
  3. var Random = Mock.Random
  4.  
  5. //必须包含字段id
  6. export default {
  7. name: "book",
  8. Name: "Book",
  9. properties: [
  10. {
  11. key: "id",
  12. title: "id"
  13. },
  14. {
  15. key: "name",
  16. title: "书名"
  17. },
  18. {
  19. key: "author",
  20. title: "作者"
  21. },
  22. {
  23. key: "press",
  24. title: "出版社"
  25. }
  26. ],
  27. buildMockData: function () {//不需要生成设为false
  28. let data = []
  29. for (let i = 0; i < 100; i++) {
  30. data.push({
  31. id: shortid.generate(),
  32. name: Random.cword(5, 7),
  33. author: Random.cname(),
  34. press: Random.cword(5, 7)
  35. })
  36. }
  37. return data
  38. }
  39. }
  40.  

模板中使用最多的就是这个数据,也是生成新代码需要配置的地方,比如这里配置的是 book ,生成的就是关于 book 的curd 的 mock 服务。要生成别的,修改后执行生成命令即可。

buildMockData 函数的作用是生成 mock 服务需要的随机数据,在 db.njk 模板中会使用:

  1. {
  2. "<$ model.name $>":<% if model.buildMockData %><$ model.buildMockData()|dump|safe $><% else %>[]<% endif %>
  3. }

这也是 nunjucks 如何在模板中执行函数

config.js 的内容如下:

  1. export default {
  2. //server
  3. RouteRelativePath: '/src/routes/',
  4. ControllerRelativePath: '/src/controllers/',
  5. ServiceRelativePath: '/src/services/',
  6. ModelRelativePath: '/src/models/',
  7. DBRelativePath: '/src/db/'
  8. }

配置相应的模板编译后保存的位置。

config/index.js 的内容如下:

  1. import model from './model';
  2. import config from './config';
  3. export default {
  4. model,
  5. config
  6. }

针对 lazy-mock 的代码生成的功能就已经完成了,要实现模板的定制直接修改模板文件即可,比如要修改 mock server 服务 api 的接口定义,直接修改 route.njk 文件:

  1. import KoaRouter from 'koa-router'
  2. import controllers from '../controllers/index.js'
  3. import PermissionCheck from '../middleware/PermissionCheck'
  4.  
  5. const router = new KoaRouter()
  6. router
  7. .get('/<$ model.name $>/paged', controllers.<$model.name $>.get<$ model.Name $>PagedList)
  8. .get('/<$ model.name $>/:id', controllers.<$ model.name $>.get<$ model.Name $>)
  9. .del('/<$ model.name $>/del', controllers.<$ model.name $>.del<$ model.Name $>)
  10. .del('/<$ model.name $>/batchdel', controllers.<$ model.name $>.del<$ model.Name $>s)
  11. .post('/<$ model.name $>/save', controllers.<$ model.name $>.save<$ model.Name $>)
  12.  
  13. module.exports = router
  14.  

模板开发与安装

不同的项目,代码结构是不一样的,每次直接修改模板文件会很麻烦。

需要提供这样的功能:针对不同的项目开发一套独立的模板,支持模板的安装。

代码生成的相关逻辑都在模板目录的文件中,模板开发没有什么规则限制,只要保证目录名为 templates , generate.js 中导出 generate 函数即可。

模板的安装原理就是将模板目录中的文件全部覆盖掉即可。不过具体的安装分为本地安装与在线安装。

之前已经说了,这个代码生成器是集成在 lazy-mock 中的,我的做法是在初始化一个新 lazy-mock 项目的时候,指定使用相应的模板进行初始化,也就是安装相应的模板。

使用 Node.js 写了一个 CLI 工具 lazy-mock-cli ,已发到 npm ,其功能包含下载指定的远程模板来初始化新的 lazy-mock 项目。代码参考( copy )了vue-cli2 。代码不难,说下某些关键点。

安装 CLI 工具:

  1. npm install lazy-mock -g

使用模板初始化项目:

  1. lazy-mock init d2-admin-pm my-project

d2-admin-pm 是我为一个 前端项目 已经写好的一个模板。

init 命令调用的是 lazy-mock-init.js 中的逻辑:

  1. #!/usr/bin/env node
  2. const download = require('download-git-repo')
  3. const program = require('commander')
  4. const ora = require('ora')
  5. const exists = require('fs').existsSync
  6. const rm = require('rimraf').sync
  7. const path = require('path')
  8. const chalk = require('chalk')
  9. const inquirer = require('inquirer')
  10. const home = require('user-home')
  11. const fse = require('fs-extra')
  12. const tildify = require('tildify')
  13. const cliSpinners = require('cli-spinners');
  14. const logger = require('../lib/logger')
  15. const localPath = require('../lib/local-path')
  16.  
  17. const isLocalPath = localPath.isLocalPath
  18. const getTemplatePath = localPath.getTemplatePath
  19.  
  20. program.usage('<template-name> [project-name]')
  21. .option('-c, --clone', 'use git clone')
  22. .option('--offline', 'use cached template')
  23.  
  24. program.on('--help', () => {
  25. console.log(' Examples:')
  26. console.log()
  27. console.log(chalk.gray(' # create a new project with an official template'))
  28. console.log(' $ lazy-mock init d2-admin-pm my-project')
  29. console.log()
  30. console.log(chalk.gray(' # create a new project straight from a github template'))
  31. console.log(' $ vue init username/repo my-project')
  32. console.log()
  33. })
  34.  
  35. function help() {
  36. program.parse(process.argv)
  37. if (program.args.length < 1) return program.help()
  38. }
  39. help()
  40. //模板
  41. let template = program.args[0]
  42. //判断是否使用官方模板
  43. const hasSlash = template.indexOf('/') > -1
  44. //项目名称
  45. const rawName = program.args[1]
  46. //在当前文件下创建
  47. const inPlace = !rawName || rawName === '.'
  48. //项目名称
  49. const name = inPlace ? path.relative('../', process.cwd()) : rawName
  50. //创建项目完整目标位置
  51. const to = path.resolve(rawName || '.')
  52. const clone = program.clone || false
  53.  
  54. //缓存位置
  55. const serverTmp = path.join(home, '.lazy-mock', 'sever')
  56. const tmp = path.join(home, '.lazy-mock', 'templates', template.replace(/[\/:]/g, '-'))
  57. if (program.offline) {
  58. console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  59. template = tmp
  60. }
  61.  
  62. //判断是否当前目录下初始化或者覆盖已有目录
  63. if (inPlace || exists(to)) {
  64. inquirer.prompt([{
  65. type: 'confirm',
  66. message: inPlace
  67. ? 'Generate project in current directory?'
  68. : 'Target directory exists. Continue?',
  69. name: 'ok'
  70. }]).then(answers => {
  71. if (answers.ok) {
  72. run()
  73. }
  74. }).catch(logger.fatal)
  75. } else {
  76. run()
  77. }
  78.  
  79. function run() {
  80. //使用本地缓存
  81. if (isLocalPath(template)) {
  82. const templatePath = getTemplatePath(template)
  83. if (exists(templatePath)) {
  84. generate(name, templatePath, to, err => {
  85. if (err) logger.fatal(err)
  86. console.log()
  87. logger.success('Generated "%s"', name)
  88. })
  89. } else {
  90. logger.fatal('Local template "%s" not found.', template)
  91. }
  92. } else {
  93. if (!hasSlash) {
  94. //使用官方模板
  95. const officialTemplate = 'lazy-mock-templates/' + template
  96. downloadAndGenerate(officialTemplate)
  97. } else {
  98. downloadAndGenerate(template)
  99. }
  100. }
  101. }
  102.  
  103. function downloadAndGenerate(template) {
  104. downloadServer(() => {
  105. downloadTemplate(template)
  106. })
  107. }
  108.  
  109. function downloadServer(done) {
  110. const spinner = ora('downloading server')
  111. spinner.spinner = cliSpinners.bouncingBall
  112. spinner.start()
  113. if (exists(serverTmp)) rm(serverTmp)
  114. download('wjkang/lazy-mock', serverTmp, { clone }, err => {
  115. spinner.stop()
  116. if (err) logger.fatal('Failed to download server ' + template + ': ' + err.message.trim())
  117. done()
  118. })
  119. }
  120.  
  121. function downloadTemplate(template) {
  122. const spinner = ora('downloading template')
  123. spinner.spinner = cliSpinners.bouncingBall
  124. spinner.start()
  125. if (exists(tmp)) rm(tmp)
  126. download(template, tmp, { clone }, err => {
  127. spinner.stop()
  128. if (err) logger.fatal('Failed to download template ' + template + ': ' + err.message.trim())
  129. generate(name, tmp, to, err => {
  130. if (err) logger.fatal(err)
  131. console.log()
  132. logger.success('Generated "%s"', name)
  133. })
  134. })
  135. }
  136.  
  137. function generate(name, src, dest, done) {
  138. try {
  139. fse.removeSync(path.join(serverTmp, 'templates'))
  140. const packageObj = fse.readJsonSync(path.join(serverTmp, 'package.json'))
  141. packageObj.name = name
  142. packageObj.author = ""
  143. packageObj.description = ""
  144. packageObj.ServerFullPath = path.join(dest)
  145. packageObj.FrontendFullPath = path.join(dest, "front-page")
  146. fse.writeJsonSync(path.join(serverTmp, 'package.json'), packageObj, { spaces: 2 })
  147. fse.copySync(serverTmp, dest)
  148. fse.copySync(path.join(src, 'templates'), path.join(dest, 'templates'))
  149. } catch (err) {
  150. done(err)
  151. return
  152. }
  153. done()
  154. }
  155.  

判断了是使用本地缓存的模板还是拉取最新的模板,拉取线上模板时是从官方仓库拉取还是从别的仓库拉取。

一些小问题

目前代码生成的相关数据并不是来源于数据库,而是在 model.js 中简单配置的,原因是我认为一个 mock server 不需要数据库,lazy-mock 确实如此。

但是如果写一个正儿八经的代码生成器,那肯定是需要根据已经设计好的数据库表来生成代码的。那么就需要连接数据库,读取数据表的字段信息,比如字段名称,字段类型,字段描述等。而不同关系型数据库,读取表字段信息的 sql 是不一样的,所以还要写一堆balabala的判断。可以使用现成的工具 sequelize-auto , 把它读取的 model 数据转成我们需要的格式即可。

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

某个目录结构是这样的:

index.js 的内容:

  1. import layoutHeaderAside from '@/layout/header-aside'
  2. export default {
  3. "layoutHeaderAside": layoutHeaderAside,
  4. "menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
  5. "route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
  6. "role": () => import(/* webpackChunkName: "role" */'@/pages/sys/role'),
  7. "user": () => import(/* webpackChunkName: "user" */'@/pages/sys/user'),
  8. "interface": () => import(/* webpackChunkName: "interface" */'@/pages/sys/interface')
  9. }

如果添加一个 book 就需要在这里加上 "book": () => import(/* webpackChunkName: "book" */'@/pages/sys/book')

这一行内容也是可以通过配置模板来生成的,比如模板内容为:

  1. "<$ model.name $>": () => import(/* webpackChunkName: "<$ model.name $>" */'@/pages<$ model.module $><$ model.name $>')

但是生成的内容怎么加到 index.js 中呢?

第一种方法:复制粘贴

第二种方法:

这部分的模板为 routerMapComponent.njk

  1. export default {
  2. "<$ model.name $>": () => import(/* webpackChunkName: "<$ model.name $>" */'@/pages<$ model.module $><$ model.name $>')
  3. }

编译后文件保存到 routerMapComponents 目录下,比如 book.js

修改 index.js :

  1. const files = require.context('./', true, /\.js$/);
  2. import layoutHeaderAside from '@/layout/header-aside'
  3.  
  4. let componentMaps = {
  5. "layoutHeaderAside": layoutHeaderAside,
  6. "menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
  7. "route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
  8. "role": () => import(/* webpackChunkName: "role" */'@/pages/sys/role'),
  9. "user": () => import(/* webpackChunkName: "user" */'@/pages/sys/user'),
  10. "interface": () => import(/* webpackChunkName: "interface" */'@/pages/sys/interface'),
  11. }
  12. files.keys().forEach((key) => {
  13. if (key === './index.js') return
  14. Object.assign(componentMaps, files(key).default)
  15. })
  16. export default componentMaps
  17.  

使用了 require.context

我目前也是使用了这种方法

第三种方法:

开发模板的时候,做特殊处理,读取原有 index.js 的内容,按行进行分割,在数组的最后一个元素之前插入新生成的内容,注意逗号的处理,将新数组内容重新写入 index.js 中,注意换行。

打个广告

如果你想要快速的创建一个 mock-server,同时还支持数据的持久化,又不需要安装数据库,还支持代码生成器的模板开发,欢迎试试lazy-mock

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号