摘要:目前市面上也有很多優秀的代碼生成器,而且大部分都提供可視化界面操作。原理代碼生成器的原理就是數據模板文件。但是如果寫一個正兒八經的代碼生成器,那肯定是需要根據已經設計好的數據庫表來生成代碼的。
背景第一次接觸代碼生成器用的是動軟代碼生成器,數據庫設計好之后,一鍵生成后端 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
模板開發與安裝
不同的項目,代碼結構是不一樣的,每次直接修改模板文件會很麻煩。
需要提供這樣的功能:針對不同的項目開發一套獨立的模板,支持模板的安裝。
代碼生成的相關邏輯都在模板目錄的文件中,模板開發沒有什么規則限制,只要保證目錄名為 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(" [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 。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7316.html
摘要:當退出的錯誤碼不為的時候,表示失敗,操作終止,否則操作繼續。執行命令進行測試,如果測試全部通過的話,退出,錯誤碼為,否則錯誤碼為,同樣退出。這樣雖然沒有解決不會隨著倉庫移動的問題,但也提供了一種在項目組里通用一套的方案。 git hooks想必很多攻城獅都不陌生,官方對于hooks有詳細的文檔,也有站內網友的文章Git Hooks (1):介紹,GIt Hooks (2):腳本分類,說...
摘要:摘要性能彪悍的引擎。深入淺出系列深入淺出第課箭頭函數中的究竟是什么鬼深入淺出第課函數是一等公民是什么意思呢深入淺出第課什么是垃圾回收算法深入淺出第課是如何工作的最近,生態系統又多了個非常硬核的項目。 摘要: 性能彪悍的V8引擎。 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數中的this究竟是什么鬼? JavaScript深入淺出第2課:函數是一...
摘要:類似的,如果我們想要調試擴展,我們也需要擴展源文件的調試符號信息。配置安裝插件這里我們將用來調試擴展。配置輸入配置一個任務,該任務會執行,生成帶調試信息的擴展文件。 Debugging NodeJS C++ addons using VS Code 之前筆者寫了一篇 用NAN寫一個nodejs的c++擴展, 實際開發過程中,肯定是有單步調試的需求。這里簡單介紹用如何用vscode調試...
摘要:不過,相對于靜態類型檢查帶來的好處,這些代價是值得的。當然少不了的模塊化標準,雖然到目前為止和大部分瀏覽器都還不支持它。本身支持兩種模塊化方式,一種是對的模塊的微小擴展,另一種是在發布之前本身模仿的命名空間。有一種情況例外。 TypeScript 帶來的最大好處就是靜態類型檢查,所以在從 JavaScript 轉向 TypeScript 之前,一定要認識到添加類型定義會帶來額外的工作量...
摘要:在真正寫了一段時間的基礎組件和基礎工具后,才發現自動化測試有很多好處。有了自動化測試,開發者會更加信任自己的代碼。由于維護測試用例也是一大筆開銷畢竟沒有多少測試會專門幫前端寫業務測試用例,而前端使用的流程自動化工具更是沒有測試參與了。 本文轉載自 天貓前端博客,更多精彩文章請進入天貓前端博客查看 前言 為何要測試 以前不喜歡寫測試,主要是覺得編寫和維護測試用例非常的浪費時間。在真正寫了...
閱讀 3138·2021-11-24 10:24
閱讀 2930·2021-11-11 16:54
閱讀 3066·2021-09-22 15:55
閱讀 2027·2019-08-30 15:44
閱讀 1901·2019-08-29 18:41
閱讀 2761·2019-08-29 13:43
閱讀 3053·2019-08-29 12:51
閱讀 1172·2019-08-26 12:19