资讯专栏INFORMATION COLUMN

使用 Node.js 写一个代码生成器

韩冰 / 1737人阅读

摘要:目前市面上也有很多优秀的代码生成器,而且大部分都提供可视化界面操作。原理代码生成器的原理就是数据模板文件。但是如果写一个正儿八经的代码生成器,那肯定是需要根据已经设计好的数据库表来生成代码的。

背景

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

只生成 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
模板开发与安装

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

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

代码生成的相关逻辑都在模板目录的文件中,模板开发没有什么规则限制,只要保证目录名为 templatesgenerate.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(" [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 ");"../", 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 数据转成我们需要的格式即可。

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

某个目录结构是这样的:

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 。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7316.html

相关文章

  • 不会shell的程序员照样是好前端——用Node.JS实现git hooks

    摘要:当退出的错误码不为的时候,表示失败,操作终止,否则操作继续。执行命令进行测试,如果测试全部通过的话,退出,错误码为,否则错误码为,同样退出。这样虽然没有解决不会随着仓库移动的问题,但也提供了一种在项目组里通用一套的方案。 git hooks想必很多攻城狮都不陌生,官方对于hooks有详细的文档,也有站内网友的文章Git Hooks (1):介绍,GIt Hooks (2):脚本分类,说...

    BWrong 评论0 收藏0
  • JavaScript深入浅出第4课:V8引擎是如何工作的?

    摘要:摘要性能彪悍的引擎。深入浅出系列深入浅出第课箭头函数中的究竟是什么鬼深入浅出第课函数是一等公民是什么意思呢深入浅出第课什么是垃圾回收算法深入浅出第课是如何工作的最近,生态系统又多了个非常硬核的项目。 摘要: 性能彪悍的V8引擎。 《JavaScript深入浅出》系列: JavaScript深入浅出第1课:箭头函数中的this究竟是什么鬼? JavaScript深入浅出第2课:函数是一...

    hsluoyz 评论0 收藏0
  • vscode调试node.js c++扩展

    摘要:类似的,如果我们想要调试扩展,我们也需要扩展源文件的调试符号信息。配置安装插件这里我们将用来调试扩展。配置输入配置一个任务,该任务会执行,生成带调试信息的扩展文件。 Debugging NodeJS C++ addons using VS Code 之前笔者写了一篇 用NAN写一个nodejs的c++扩展, 实际开发过程中,肯定是有单步调试的需求。这里简单介绍用如何用vscode调试...

    王伟廷 评论0 收藏0
  • 从 JavaScript 到 TypeScript - 模块化和构建

    摘要:不过,相对于静态类型检查带来的好处,这些代价是值得的。当然少不了的模块化标准,虽然到目前为止和大部分浏览器都还不支持它。本身支持两种模块化方式,一种是对的模块的微小扩展,另一种是在发布之前本身模仿的命名空间。有一种情况例外。 TypeScript 带来的最大好处就是静态类型检查,所以在从 JavaScript 转向 TypeScript 之前,一定要认识到添加类型定义会带来额外的工作量...

    Jonathan Shieber 评论0 收藏0
  • 聊一聊前端自动化测试

    摘要:在真正写了一段时间的基础组件和基础工具后,才发现自动化测试有很多好处。有了自动化测试,开发者会更加信任自己的代码。由于维护测试用例也是一大笔开销毕竟没有多少测试会专门帮前端写业务测试用例,而前端使用的流程自动化工具更是没有测试参与了。 本文转载自 天猫前端博客,更多精彩文章请进入天猫前端博客查看 前言 为何要测试 以前不喜欢写测试,主要是觉得编写和维护测试用例非常的浪费时间。在真正写了...

    wthee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<