溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

使用Node.js寫(xiě)一個(gè)代碼生成器的方法步驟

發(fā)布時(shí)間:2020-09-16 19:03:12 來(lái)源:腳本之家 閱讀:322 作者:若邪 欄目:web開(kāi)發(fā)

 背景

第一次接觸代碼生成器用的是動(dòng)軟代碼生成器,數(shù)據(jù)庫(kù)設(shè)計(jì)好之后,一鍵生成后端 curd代碼。之后也用過(guò) CodeSmith , T4。目前市面上也有很多優(yōu)秀的代碼生成器,而且大部分都提供可視化界面操作。

自己寫(xiě)一個(gè)的原因是因?yàn)橐傻阶约簩?xiě)的一個(gè)小工具中,而且使用 Node.js 這種動(dòng)態(tài)腳本語(yǔ)言進(jìn)行編寫(xiě)更加靈活。

原理

代碼生成器的原理就是: 數(shù)據(jù) + 模板 => 文件 。

數(shù)據(jù) 一般為數(shù)據(jù)庫(kù)的表字段結(jié)構(gòu)。

模板 的語(yǔ)法與使用的模板引擎有關(guān)。

使用模板引擎將 數(shù)據(jù) 和 模板 進(jìn)行編譯,編譯后的內(nèi)容輸出到文件中就得到了一份代碼文件。

功能

因?yàn)檫@個(gè)代碼生成器是要集成到一個(gè)小工具lazy-mock 內(nèi),這個(gè)工具的主要功能是啟動(dòng)一個(gè) mock server 服務(wù),包含curd功能,并且支持?jǐn)?shù)據(jù)的持久化,文件變化的時(shí)候自動(dòng)重啟服務(wù)以最新的代碼提供 api mock 服務(wù)。

代碼生成器的功能就是根據(jù)配置的數(shù)據(jù)和模板,編譯后將內(nèi)容輸出到指定的目錄文件中。因?yàn)樘砑恿诵碌奈募琺ock server 服務(wù)會(huì)自動(dòng)重啟。

還要支持模板的定制與開(kāi)發(fā),以及使用 CLI 安裝模板。

可以開(kāi)發(fā)前端項(xiàng)目的模板,直接將編譯后的內(nèi)容輸出到前端項(xiàng)目的相關(guān)目錄下,webpack 的熱更新功能也會(huì)起作用。

模板引擎

模板引擎使用的是nunjucks。

lazy-mock 使用的構(gòu)建工具是 gulp,使用 gulp-nodemon 實(shí)現(xiàn) mock-server 服務(wù)的自動(dòng)重啟。所以這里使用 gulp-nunjucks-render 配合 gulp 的構(gòu)建流程。

代碼生成

編寫(xiě)一個(gè) 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項(xiàng)目的絕對(duì)路徑
const FrontendFullPath = require('./package.json').FrontendFullPath; //前端項(xiàng)目的絕對(duì)路徑
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)
});

代碼具體結(jié)構(gòu)細(xì)節(jié)可以打開(kāi)lazy-mock 進(jìn)行參照

為了支持模板的開(kāi)發(fā),以及更靈活的配置,我將代碼生成的邏輯全都放在模板目錄中。

templates 是存放模板以及數(shù)據(jù)配置的目錄。結(jié)構(gòu)如下:

使用Node.js寫(xiě)一個(gè)代碼生成器的方法步驟

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

generate.js 的內(nèi)容如下:

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作為數(shù)據(jù)(模板內(nèi)可以獲取到 nunjucksRenderConfig 屬性 data 上的數(shù)據(jù))。編譯后進(jìn)行文件重命名,并保存到指定目錄下。

model.js 的內(nèi)容如下:

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: "書(shū)名"
    },
    {
      key: "author",
      title: "作者"
    },
    {
      key: "press",
      title: "出版社"
    }
  ],
  buildMockData: function () {//不需要生成設(shè)為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
  }
}

模板中使用最多的就是這個(gè)數(shù)據(jù),也是生成新代碼需要配置的地方,比如這里配置的是 book ,生成的就是關(guān)于 book 的curd 的 mock 服務(wù)。要生成別的,修改后執(zhí)行生成命令即可。

buildMockData 函數(shù)的作用是生成 mock 服務(wù)需要的隨機(jī)數(shù)據(jù),在 db.njk 模板中會(huì)使用:

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

這也是 nunjucks 如何在模板中執(zhí)行函數(shù)

config.js 的內(nèi)容如下:

export default {
  //server
  RouteRelativePath: '/src/routes/',
  ControllerRelativePath: '/src/controllers/',
  ServiceRelativePath: '/src/services/',
  ModelRelativePath: '/src/models/',
  DBRelativePath: '/src/db/'
}

配置相應(yīng)的模板編譯后保存的位置。

config/index.js 的內(nèi)容如下:

import model from './model';
import config from './config';
export default {
  model,
  config
}

針對(duì) lazy-mock 的代碼生成的功能就已經(jīng)完成了,要實(shí)現(xiàn)模板的定制直接修改模板文件即可,比如要修改 mock server 服務(wù) 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

模板開(kāi)發(fā)與安裝

不同的項(xiàng)目,代碼結(jié)構(gòu)是不一樣的,每次直接修改模板文件會(huì)很麻煩。

需要提供這樣的功能:針對(duì)不同的項(xiàng)目開(kāi)發(fā)一套獨(dú)立的模板,支持模板的安裝。

代碼生成的相關(guān)邏輯都在模板目錄的文件中,模板開(kāi)發(fā)沒(méi)有什么規(guī)則限制,只要保證目錄名為 templates , generate.js 中導(dǎo)出 generate 函數(shù)即可。

模板的安裝原理就是將模板目錄中的文件全部覆蓋掉即可。不過(guò)具體的安裝分為本地安裝與在線安裝。

之前已經(jīng)說(shuō)了,這個(gè)代碼生成器是集成在 lazy-mock 中的,我的做法是在初始化一個(gè)新 lazy-mock 項(xiàng)目的時(shí)候,指定使用相應(yīng)的模板進(jìn)行初始化,也就是安裝相應(yīng)的模板。

使用 Node.js 寫(xiě)了一個(gè) CLI 工具 lazy-mock-cli ,已發(fā)到 npm ,其功能包含下載指定的遠(yuǎn)程模板來(lái)初始化新的 lazy-mock 項(xiàng)目。代碼參考( copy )了vue-cli2 。代碼不難,說(shuō)下某些關(guān)鍵點(diǎn)。

安裝 CLI 工具:

npm install lazy-mock -g

使用模板初始化項(xiàng)目:

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

d2-admin-pm 是我為一個(gè) 前端項(xiàng)目 已經(jīng)寫(xiě)好的一個(gè)模板。

init 命令調(diào)用的是 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
//項(xiàng)目名稱
const rawName = program.args[1]
//在當(dāng)前文件下創(chuàng)建
const inPlace = !rawName || rawName === '.'
//項(xiàng)目名稱
const name = inPlace ? path.relative('../', process.cwd()) : rawName
//創(chuàng)建項(xiàng)目完整目標(biāo)位置
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
}

//判斷是否當(dāng)前目錄下初始化或者覆蓋已有目錄
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()
}

判斷了是使用本地緩存的模板還是拉取最新的模板,拉取線上模板時(shí)是從官方倉(cāng)庫(kù)拉取還是從別的倉(cāng)庫(kù)拉取。

一些小問(wèn)題

目前代碼生成的相關(guān)數(shù)據(jù)并不是來(lái)源于數(shù)據(jù)庫(kù),而是在 model.js 中簡(jiǎn)單配置的,原因是我認(rèn)為一個(gè) mock server 不需要數(shù)據(jù)庫(kù),lazy-mock 確實(shí)如此。

但是如果寫(xiě)一個(gè)正兒八經(jīng)的代碼生成器,那肯定是需要根據(jù)已經(jīng)設(shè)計(jì)好的數(shù)據(jù)庫(kù)表來(lái)生成代碼的。那么就需要連接數(shù)據(jù)庫(kù),讀取數(shù)據(jù)表的字段信息,比如字段名稱,字段類型,字段描述等。而不同關(guān)系型數(shù)據(jù)庫(kù),讀取表字段信息的 sql 是不一樣的,所以還要寫(xiě)一堆balabala的判斷??梢允褂矛F(xiàn)成的工具 sequelize-auto , 把它讀取的 model 數(shù)據(jù)轉(zhuǎn)成我們需要的格式即可。

生成前端項(xiàng)目代碼的時(shí)候,會(huì)遇到這種情況:

某個(gè)目錄結(jié)構(gòu)是這樣的:

使用Node.js寫(xiě)一個(gè)代碼生成器的方法步驟

index.js 的內(nèi)容:

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')
}

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

這一行內(nèi)容也是可以通過(guò)配置模板來(lái)生成的,比如模板內(nèi)容為:

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

但是生成的內(nèi)容怎么加到 index.js 中呢?

第一種方法:復(fù)制粘貼

第二種方法:

這部分的模板為 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

我目前也是使用了這種方法

第三種方法:

開(kāi)發(fā)模板的時(shí)候,做特殊處理,讀取原有 index.js 的內(nèi)容,按行進(jìn)行分割,在數(shù)組的最后一個(gè)元素之前插入新生成的內(nèi)容,注意逗號(hào)的處理,將新數(shù)組內(nèi)容重新寫(xiě)入 index.js 中,注意換行。

打個(gè)廣告

如果你想要快速的創(chuàng)建一個(gè) mock-server,同時(shí)還支持?jǐn)?shù)據(jù)的持久化,又不需要安裝數(shù)據(jù)庫(kù),還支持代碼生成器的模板開(kāi)發(fā),歡迎試試lazy-mock 。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI