今天给大家分享一下egg的代码生成器,这个其实原理很简单,说白了就是用到了nodejs的一个文件io的操作,通过一系列配置参数解析然后生成一个很长的字符串,写入到文件中,最后导出到我们指定的文件夹。
前提概要
为什么我要写这个代码生成器呢?原因主要还是为了提高自己平时开发的效率和时间,比如说什么呢,其实现在单表单业务的管理系统页面完全是可以使用代码生成来做的,可以极大程度的去简化我们开发的一个效率,也是我们一个摸鱼神器。话不多说,讲讲我的大致的一个思路把
大致思路
这里我希望每次生成代码都是根据配置进行生成,我希望我每次在egg-sequelize设计好一个实体之后,然后自动关联到数据表,自动获取数据表中的字段,这个字段我可以进行配置也可以不进行配置,然后这些字段最终都是要加入到我后端的crud接口中的。
我这里使用的是egg-swagger-doc框架,还需要根据它那边的规则去生成对应的swagger文档,所以我写了一个大致的配置
这样的话我就可以根据具体的配置去生成对应的controller,对应的service,对应的router以及vue文件,生成完成之后的str,我就根据工具类的创建文件夹,创建对应的controller,service,router,vue等文件夹,再把文件内容写入到文件,放入到对应文件夹里面,这就是我分享的代码生成器的大致的一个实现思路
工具类
// 移除文件夹,移除output目录
function clearOutputFolder() {fs.readdirSync(outputPath).forEach((file) => {if (file !== 'index.js') {const filePath = path.join(outputPath, file);if (fs.lstatSync(filePath).isDirectory()) {deleteFolderRecursive(filePath);} else {fs.unlinkSync(filePath);}}});
}// 创建文件夹
function createFolder(folderName) {return new Promise((resolve, reject) => {fs.mkdir(folderName, (err) => {if (err) {reject(err);} else {resolve();}});});
}// 删除文件夹
function deleteFolderRecursive(folderPath) {// 文件夹存在if (fs.existsSync(folderPath)) {// 读取文件夹里面所有文件fs.readdirSync(folderPath).forEach((file) => {const currentFilePath = path.join(folderPath, file);// 如果是文件夹,递归删除if (fs.lstatSync(currentFilePath).isDirectory()) {deleteFolderRecursive(currentFilePath); // 递归删除子文件夹} else {// 如果是文件删除文件fs.unlinkSync(currentFilePath); // 删除文件}});fs.rmdirSync(folderPath); // 删除空文件夹}
}//生成controller代码
function generateController(){//...
}//生成service代码
function generateService(){//...
}//生成swagger的contract代码,用于放dto和vo的对象
function generateContract(){//...
}//生成路由代码
function generateRouter(){//...
}//生成swagger api字符串
function generateSwaggerApiStr(){}
入口
const str = `请选择要使用的功能1.代码生成器2.根据表格生成所有字段基本信息请输入对应的数字序号`;let number = await getInput(str);if (Number(number) === 1) {const config = require('./config');let isUseFileConfig = await getInput('你是否使用文件配置(yes/no)','select');let author,parentMenu,moduleEName,moduleName,moduleDescription,isGenerateAnnotaion;// 是否使用文件配置if (!isUseFileConfig) {author = await getInput('请输入作者');parentMenu = await getInput('请输入模块所在上层目录');moduleName = await getInput('请输入模块中文名称');moduleEName = await getInput('请输入模块英文名称');moduleDescription = await getInput('请输入模块描述信息');isGenerateAnnotaion = await getInput('你是否想要生成注释信息(yes/no)','select');} else {author = config.author;parentMenu = config.parentMenu;moduleName = config.moduleName;moduleEName = config.moduleEName;moduleDescription = config.moduleDescription;isGenerateAnnotaion = config.isGenerateAnnotaion;}clearOutputFolder();const date = timeFormat(new Date());const upperCaseModuleEName = capitalizeFirstLetter(moduleEName);const upperCaseParentMenu = capitalizeFirstLetter(parentMenu);let generateConfig = {author,parentMenu,moduleName,moduleEName,moduleDescription,isGenerateAnnotaion,date,upperCaseModuleEName,upperCaseParentMenu};if (isUseFileConfig) {generateConfig = {...generateConfig,...config};}// 生产控制器代码generateController(generateConfig);// 生产路由代码generateRouter(generateConfig);// 生产服务端代码generateService(generateConfig);// 生成swagger的contractgenerateContract(generateConfig);// 生成前端的文件generateVueFolder(generateConfig);console.log('代码生成完成,生成的文件在codeGenerator/output文件夹下');rl.close();} else if (Number(number) === 2) {let table = await getInput('请输入表名称');const str = await getVoByTb(table);const folderPath = path.join(__dirname, 'output');// console.log(folderPath);// await createFolder(folderPath);const filePath = path.join(folderPath, 'dto.js');fs.writeFileSync(filePath, str);console.log('vo生成完成,生成的文件在codeGenerator/output文件夹下,名字为dto.js');rl.close();} else {console.log('输入的值出错!!');rl.close();}// 控制台输入
async function getInput(question, type) {return new Promise((resolve) => {rl.question(question, (input) => {input = input.trim();if (!input) {console.log('输入不能为空,请再次输入');resolve(getInput(question));} else {if (type === 'select') {if (input.toLocaleLowerCase() === 'yes') {resolve(true);} else if (input.toLocaleLowerCase() === 'no') {resolve(false);} else {console.log('请输入yes或者no');resolve(getInput(question));}}resolve(input);}});});
}
我这里还使用了一个控制台输入的,做了两种配置,可以通过配置指定,也可以通过控制台输入,这里控制台输入的有一些还没有完善好,后面觉得配置形式的好用一点,就是每次需要重新改。
生成controller
我这里随机用一个来展示,代码有点多,主要就是来展示一下,大概的一个实现,本质上其实就是模版 + 配置的动态数据
const {author,moduleName,moduleEName,moduleDescription,date,groupName,parentMenu} = opts;const listAnnotationStr = generateSwaggerApiStr({ ...opts, type: 'list' });const detailAnnotationStr = generateSwaggerApiStr({...opts,type: 'detail'});const pageAnnotationStr = generateSwaggerApiStr({ ...opts, type: 'page' });const addAnnotationStr = generateSwaggerApiStr({ ...opts, type: 'add' });const editAnnotationStr = generateSwaggerApiStr({ ...opts, type: 'edit' });const deleteAnnotationStr = generateSwaggerApiStr({...opts,type: 'delete'});const exportAnnotationStr = generateSwaggerApiStr({...opts,type: 'export'});const controllerStr = `/**
* @description ${moduleDescription}
* @controller ${moduleName}管理 ${moduleName}管理 ${groupName ? groupName : ''}
* @author ${author}
* @date : ${date}
*/`;const upperCaseModuleEname = capitalizeFirstLetter(moduleEName);const parentModuleGroupStr = parentMenu ? `${parentMenu}.${moduleEName}` : moduleEName;const content = `
const Controller = ${parentMenu ? "require('../../core/baseController')" : "require('../core/baseController')"};${controllerStr}
class ${moduleEName}Controller extends Controller {${listAnnotationStr}async list() {const { ctx, service } = this;ctx.body = await service.${parentModuleGroupStr}.find${upperCaseModuleEname}List();}${detailAnnotationStr}async detail() {const { ctx, service } = this;const {id} = ctx.params;ctx.body = await service.${parentModuleGroupStr}.find${upperCaseModuleEname}Detail(id);}${pageAnnotationStr}async page(){const { ctx, service } = this;const opts = ctx.params;ctx.body = await service.${parentModuleGroupStr}.find${upperCaseModuleEname}Page(opts);}${addAnnotationStr}async create() {const { ctx, service } = this;await service.${parentModuleGroupStr}.add${upperCaseModuleEname}(ctx.request.body);}${editAnnotationStr}async update() {const { ctx, service } = this;await service.${parentModuleGroupStr}.edit${upperCaseModuleEname}(ctx.request.body);}${deleteAnnotationStr}async destroy() {const { ctx, service } = this;ctx.validate({ ids: 'string' }, ctx.request.query);await service.${parentModuleGroupStr}.delete${upperCaseModuleEname}(ctx.request.query.ids);}${exportAnnotationStr}async export() {await this.exportExcel('${parentModuleGroupStr}', 'export${upperCaseModuleEname}', '${moduleName}');}
}
module.exports = ${moduleEName}Controller;
最终效果
运行这个index,在控制台可以进行输入,输入完成回车
最后根据配置生成的一个结果
可以直接进入引入,引入到自己项目中,重新编译就可以看到一个效果
可拓展
我目前这个代码生成器还有可拓展的空间
- 比如关于一些字段的配置,我这里最好可以传递一个数组,然后在后台进行接收,数组里面包含每一个属性对象,属性对象里面有各种属性来区分这些字段是否要展示,是否要加入查询,是否要加入添加,是否要加入编辑,等等
- 我这个代码生成器可以做成可视化的形式,把配置文件通过在前端界面进行配置,配置完成之后生成文件出来,通过浏览器去下载对应的资源压缩包,然后一键导入到项目中
- 等之后有时间我会去做一个可视化的代码生成器的版本,功能更加细致,更加方便快捷进行开发
结语
每天进步一点点,每天进行学习以及进行技术分享,加油,只要一直坚持总能越来越好