基于koa服务端脚手架搭建(文件加载器) --【elpis全栈项目笔记】

news/2024/12/23 19:46:51/

基于koa服务端脚手架(文件加载器) --【elpis-core】


前言: elpis-core 是一个项目文件加载器。基于一定的约定,将功能不同的代码分类放置到不同的目录下管理。适用于项目代码规范化、减少维护成本、沟通成本,易于扩展。(简易版的 egg-core )

请添加图片描述
其目的,就是将各类约定好的文件夹下的js方法,自动挂载到全局的app的实例上。(这只是其中一类实例,更多请看后文)

|-- app|-- controller|-- project.js|-- ...// 在别的文件下 每次使用都需要手动引入 project.js 文件
const projController = require('app/controller/project.js');
projController.getList()// ==> 接入 elpis-core 之后// 只需将 project.js 放入 controller 文件夹下,会自动挂载到app实例上
const projController = app.controller.project
projController.getList()

一、 elpis-core 目录结构


在这里插入图片描述

|-- app
|-- ...
|-- elpis-core|-- loader|-- config.js 					解析环境配置|-- controller.js				解析公共业务逻辑|-- extend.js 					解析加载外部工具类|-- middleware.js 				解析中间件|-- router-schema.js 			解析路由校验规则|-- router.js 					解析注册路由|-- service.js					解析服务模块|-- env.js							判断环境|-- index.js 						引擎入口文件
|-- index.js 项目入口文件

项目入口文件,引入/启动 elpis-core:

const ElpisCore = require('./elpis-core');
// 启动项目
ElpisCore.start({name: 'Elpis',homePage: '/'});

引擎入口文件, 加载各个处理器:

const Koa = require('koa');
const env = require('./env')
const configLoader = require('./loader/config'); 
const middlewareLoader = require('./loader/middleware');
...
module.exports = {start(options = {}) {const app = new Koa(); // koa 实例app.baseDir = process.cwd() // 基本路径app.env = env();  // 初始化环境配置controllerLoader(app) // 加载contorller处理器configLoader(app) // 加载config处理器... // 启动服务try {const port = process.env.PORTB || 8080;const host = process.env.IP || '0.0.0.0';app.listen(port, host);console.log(`\n --- 🚀 Server running at http://localhost:${port} 🚀 --- \n`);} catch (e) {console.error(e);}}
}

二、解析环境配置 – config.js


要实现的功能:基于当前环境,读取当前环境对象的config配置,可通过app.config读取当前环境配置。

|-- app
|-- config|-- config.default.js|-- config.local.js|-- config.beta.js|-- config.prod.js// config.prod.js
module.exports = {name:'生产', ...}// 生产环境调用时:
const prodName = app.config.name

处理器实现:

const path = require('path');
const { sep } = path/*** config loader* @param {object} app koa实例 * * 配置区分 本地/测试/生产 环境, 通过 env 环境读取不同文件配置* 通过 env.config 覆盖 default.config 加载到 app.config 中* * 目录下对应的 config 配置* 默认配置 config/config.default.js* 本地环境配置 config/config.local.js* 测试环境配置 config/config.beta.js* 生产环境配置 config/config.prod.js*/module.exports = (app) => {// 获取 config/ 目录const configPath = path.resolve(app.baseDir, `.${sep}config`);// 获取 default.configlet defaultConfig = {};try {defaultConfig = require(path.resolve(configPath, `.${sep}config.default.js`));} catch (e) {console.log('[exceprion] there is no default.config file')}// 获取 env.configlet envConfig = {};try {if (app.env.isLocal()) { // 本地环境envConfig = require(path.resolve(configPath, `.${sep}config.local.js`))} else if (app.env.isBeta()) {  // 测试环境envConfig = require(path.resolve(configPath, `.${sep}config.beta.js`))} else if (app.env.isProd()) {  // 生产环境envConfig = require(path.resolve(configPath, `.${sep}config.prod.js`))}} catch (e) {console.log('[exceprion] there is no default.envConfig file')}// 覆盖并加载 config 配置app.config = { ...defaultConfig, ...envConfig }
}

三、 解析业务逻辑-- controller.js


要实现的功能:读取controller文件夹,挂载到app实例上,使其中的函数可通过app.controller.xxx.getxxx()调用

|-- app|-- controller|-- base.js|-- project.js|-- view.js// project.js
module.exports = (app) => {const BaseController = require('./base')(app)return class ProjectController extends BaseController {async getList(ctx) {}}
}// 被router文件调用时:
module.exports = (app, router) => {const { project: projectController } = app.controllerrouter.get('/api/project/list', projectController.getList.bind(projectController))
}

处理器实现:

const glob = require('glob');
const path = require('path')
const { sep } = path/***  controller loader* @param {object} app koa 实例* * 加载所有 controller,,可通过`app.controller.${目录}.${文件}`访问 * * 例子:* app/controller*   |*   | -- custom-module*           |*           | -- custom-controller.js* * => app.controller.customname.customController*/module.exports = (app) => {// 读取app/controller/**/**.js 所有文件const controllerPath = path.resolve(app.businessPath, `.${sep}controller`);const fileList = glob.sync(path.resolve(controllerPath, `.${sep}**${sep}**.js`));// 遍历所有文件目录,把内容加载到 app.controller 下const controller = {}fileList?.forEach(file => {//提取文件名let name = path.resolve(file)// 截取路径 app/controller/custom-module/custom-controller.js => custom-module/custom-controllername = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length, name.lastIndexOf('.'));// 把'-'统一改为驼峰式, custom-module/custom-controller.js => customModule/customControllername = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());//挂载 controller 到内存 app 对象中;  tempController === { customModule:{ customController:{ } } }let tempController = controller;const names = name.split(sep)for (let i = 0, len = names.length; i < len; ++i) {if (i === len - 1) {const ControllerModule = require(path.resolve(file))(app)tempController[names[i]] = new ControllerModule()return} else {if (!tempController[names[i]]) {tempController[names[i]] = {}}tempController = tempController[names[i]]}}})app.controller = controller
}

四、解析外部工具类 – extend.js


要实现的功能:读取extend文件夹,挂载到app实例上,使其中的函数可通过app.xxx.xxx()调用

|-- app|-- extend|-- logger.js// logger.js
module.exports = (app) => {let logger...logger = log4js.getLogger();...return logger
}// 被其他文件调用时:
module.exports = (app, router) => {...app.logger.info('info');app.logger.error('error');
}

处理器实现:

const glob = require('glob');
const path = require('path')
const { sep } = path/***  extend loader* @param {object} app koa 实例* * 加载所有 extend,,可通过`app.extend.${文件}`访问 * * 例子:* app/extend*   |*   | -- custom-extend.js* * => app.extend.customExtend 访问*/module.exports = (app) => {// 读取app/extend/**/**.js 所有文件const extendPath = path.resolve(app.businessPath, `.${sep}extend`);const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`));// 遍历所有文件目录,把内容加载到 app.extend 下fileList?.forEach(file => {//提取文件名let name = path.resolve(file)// 截取路径 app/extend/custom-extend.js => custom-extendname = name.substring(name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length, name.lastIndexOf('.'));// 把'-'统一改为驼峰式, custom-extend.js => customExtendname = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());// 过滤 app 已经存在的keyfor (const key in app) {if (key === name) {console.warn(`[extend load error] ${name} is already in app`)return}}//挂载 extend 到内存 app 对象中;  app[name] = require(path.resolve(file))(app)})
}

五、解析中间件 – middleware.js


要实现的功能:读取middleware文件夹,挂载到app实例上,使其可通过app.middlewares.xxx调用

|-- app|-- middleware|-- error-handler.js// error-handler.js
module.exports = (app) => {return async (ctx, next) => {try {await next()} catch (err) {   // 异常处理...const resBody = { success: false, code: 5000, message: '网络异常,请稍后再试' }ctx.status = 200;ctx.body = resBody;}}
}// 使用全局 middlewares.js 统一维护引入时:
module.exports = (app, router) => {app.use(app.middlewares.errorHandler);  // 引入异常捕获中间件app.use(xxx)...
}

处理器实现:

const glob = require('glob');
const path = require('path')
const { sep } = path/***  middleware loader* @param {object} app koa 实例* * 加载所有 middleware,,可通过`app.middleware.${目录}.${文件}`访问 * * 例子:* app/middleware*   |*   | -- custom-module*           |*           | -- custom-middleware.js* * => app.middlleware.customname.customMiddleware*/module.exports = (app) => {// 读取app/middleware/**/**.js 所有文件 const middlewarePath = path.resolve(app.businessPath, `.${sep}middleware`);const fileList = glob.sync(path.resolve(middlewarePath, `.${sep}**${sep}**.js`));// 遍历所有文件目录,把内容加载到 app.middlewares 下const middleware = {}fileList?.forEach(file => {//提取文件名let name = path.resolve(file)// 截取路径 app/middleware/custom-module/custom-middleware.js => custom-module/custom-middlewarename = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));// 把'-'统一改为驼峰式, custom-module/custom-middleware.js => customModule/customMiddlewarename = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());//挂载 middleware 到内存 app 对象中;  tempMiddleware === { customModule:{ customMiddleware:{ } } }let tempMiddleware = middleware;const names = name.split(sep)for (let i = 0, len = names.length; i < len; ++i) {if (i === len - 1) {tempMiddleware[names[i]] = require(path.resolve(file))(app)return} else {if (!tempMiddleware[names[i]]) {tempMiddleware[names[i]] = {}}tempMiddleware = tempMiddleware[names[i]]}}})app.middlewares = middleware
}

koa中间件有一个需要特别关注的地方:洋葱圈模型; 有时间需要单独写一篇;
网友的: 浅谈 Koa 和 Express 的中间件设计模式

六、解析路由校验规则 – router-schema.js


要实现的功能:读取router-schema文件夹,挂载到app实例上,使其可通过app.routerSchema.xxx调用

|-- app|-- router-schema|-- project.js// project.js 是 '/api/project/list' 接口的参数校验规则
module.exports = {'/api/project/list': {get: {query: {type: 'object',... required: ['id']}}}
}// 在API参数校验的中间件 api-params-verify.js 中使用时:
module.exports = (app, router) => {return async (ctx, next) => {const { query } = ctx.request;const { path } = ctx;const schema = app.routerSchema[path]?.[method.toLowerCase()];...validate = ajv.compile(schema.query)valid = validate(query)...}
}

处理器实现:

const glob = require('glob');
const path = require('path')
const { sep } = path/*** router-schema loader* @param {object} app koa实例 * * 通过 'json-schema' & 'ajv' 对 api规则进行约束,配合 api-params-verify 中间件使用* * app/router-schema/api1.js  // { 'api1/data/getDetail' :{} } * app/router-schema/api2.js  // { 'api2/data/getDetail' :{} } * ...* * 输出:*   app.routerSchema = {*      'api1/data/getDetail':{},*      'api2/data/getDetail':{},*     ...*   }*/module.exports = (app) => {// 读取app/router-schema/**/**.js 所有文件 const middlewarePath = path.resolve(app.businessPath, `.${sep}router-schema`);const fileList = glob.sync(path.resolve(middlewarePath, `.${sep}**${sep}**.js`));// 注册所有 routerSchema, 使得 app.routerSchema 可以访问let routerSchema = {}fileList.forEach(file => {routerSchema = {...routerSchema,...require(path.resolve(file))}});app.routerSchema = routerSchema
}

七、解析注册路由 – router.js


要实现的功能:读取router文件夹,引入每个文件的’/xxx/xxx/...‘接口,将其直接注册到koaRouter

|-- app|-- router|-- project.js// project.js 
module.exports = (app, router) => {const { project: projectController } = app.controllerrouter.get('/api/project/list', projectController.getList.bind(projectController))
}// rouer.js 处理器会遍历所有路由,引入并注册
module.exports = (app) => {...fileList.forEach(file => {require(path.resolve(file))(app, router) }); // 将遍历到的所有路由引入...app.use(router.routes()); // 注册app.use(router.allowedMethods()); // 自动响应不支持的 HTTP 方法
}

处理器实现:

const KoaRouter = require('koa-router');
const glob = require('glob');
const path = require('path');
const { sep } = path;/*** router loader* @param {object} app koa实例* * 解析所有 app/router/ 下所有 js 文件, 加载到 KoaRouter 下* */module.exports = (app) => {// 找到路由文件路径const routerPath = path.resolve(app.businessPath, `.${sep}router`);// 实例化所有路由const router = new KoaRouter();// 注册所有路由const fileList = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`));fileList.forEach(file => {require(path.resolve(file))(app, router);});// 路由兜底(健壮性)router.get('*', async (ctx, next) => {ctx.status = 302; // 临时重定向ctx.redirect(`${app?.options?.homePath ?? '/'}`);})// 路由注册到 app 上app.use(router.routes());app.use(router.allowedMethods()); 
}

八、解析服务模块-- service.js


要实现的功能:读取service文件夹,挂载到app实例上,使其可通过app.service.xxx调用

|-- app|-- service|-- base.js|-- project.js// project.js 
module.exports = (app) => {const BaseService = require('./base')(app)return class ProjectService extends BaseService {async getList() {return '数据库拿到的数据'}}
}// 在 controller 中处理接口请求的业务逻辑时: 直接调用app.service
module.exports = (app) => {...return class ProjectController extends BaseController {async getList(ctx) {const { project: projectService } = app.service;const projectList = await projectService.getList();this.success(ctx, projectList);}}
}

处理器实现:

const glob = require('glob');
const path = require('path')
const { sep } = path/***  service loader* @param {object} app koa 实例* * 加载所有 service,,可通过`app.service.${目录}.${文件}`访问 * * 例子:* app/service*   |*   | -- custom-module*           |*           | -- custom-service.js* * => app.service.customname.customService*/module.exports = (app) => {// 读取app/service/**/**.js 所有文件const servicePath = path.resolve(app.businessPath, `.${sep}service`);const fileList = glob.sync(path.resolve(servicePath, `.${sep}**${sep}**.js`));// 遍历所有文件目录,把内容加载到 app.service 下const service = {}fileList?.forEach(file => {//提取文件名let name = path.resolve(file)// 截取路径 app/service/custom-module/custom-service.js => custom-module/custom-servicename = name.substring(name.lastIndexOf(`service${sep}`) + `service${sep}`.length, name.lastIndexOf('.'));// 把'-'统一改为驼峰式, custom-module/custom-service.js => customModule/customServicename = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());//挂载 service 到内存 app 对象中;  tempService === { customModule:{ customService:{ } } }let tempService = service;const names = name.split(sep)for (let i = 0, len = names.length; i < len; ++i) {if (i === len - 1) {const SerivceModule = require(path.resolve(file))(app)tempService[names[i]] = new SerivceModule()return} else {if (!tempService[names[i]]) {tempService[names[i]] = {}}tempService = tempService[names[i]]}}})app.service = service
}

了解更多:
核心体系egg-core
阮一峰Koa 框架教程


至此elpis-core的核心功能已基本实现

全文特别鸣谢: 抖音“哲玄前端”,《全栈实践课》


http://www.ppmy.cn/news/1557546.html

相关文章

C# 动态组合判断条件对数据进行筛选

一、设计背景 工作上需求开发一个文本处理软件&#xff0c;我要在界面上编辑文本筛选条件&#xff0c;这就需要动态判断每一行文本数据。我首先是将单行文本根据空格分割成了几十个子串&#xff0c;然后对子串进行条件判断。一开始设想的动态组合判断条件&#xff0c;然后一行一…

php面对对象的基础知识

php面对对象的基础知识 程序开发&#xff1a;面向过程vs面向对象 面向过程面向过程是一种以“整体事件”为中心的编程思想&#xff0c;编程的时候把解决问题的步骤分析出来&#xff0c;然后用函数把这些步骤实现&#xff0c;在一步一步的具体步骤中再按顺序调用函数。 面向对…

Unity Apple Vision Pro 开发教程:物体识别跟踪

Spatial XR 开发者社区官网&#xff1a;SpatialXR 社区 开发流程与原理&#xff1a;Apple Vision Pro 物体识别跟踪原理与开发流程【Unity Apple Vision Pro 开发系列教程】 PolySpatial 物体跟踪官方样例讲解&#xff1a;Unity Apple Vision Pro 开发教程&#xff1a;物体识别…

SQL注入(SQL lnjection Base)21

SQL注入&#xff08;SQL lnjection Base&#xff09; sql-labs靶场的搭建 GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based.SQLI labs to test error based, Blind boolean based, Time based. - Audi-1/sqli-labshttps://githu…

MFC/C++学习系列之简单记录12——文件操作

MFC/C学习系列之简单记录12——文件操作 前言文件操作处理具体使用CFileDialog类 CFile类和CStdioFile类错误总结 前言 学习MFC的文件处理操作&#xff01; 文件操作处理 CFileDialog类&#xff1a;对话框类&#xff0c;获取选择的文件信息。CFile类&#xff1a;抽象类&#…

Redis生产实践中相关疑问记录

1. Redis相关疑问 1.1. redis内存使用率100% 就等同于redis不可用吗&#xff1f; 正常使用情况下&#xff0c;不是。 redis有【缓存淘汰机制】&#xff0c;Redis 在内存使用率达到 100% 时不会直接崩溃。相反&#xff0c;它依赖内存淘汰策略来释放内存&#xff0c;确保系统的…

数据结构---------二叉树前序遍历中序遍历后序遍历

以下是用C语言实现二叉树的前序遍历、中序遍历和后序遍历的代码示例&#xff0c;包括递归和非递归&#xff08;借助栈实现&#xff09;两种方式&#xff1a; 1. 二叉树节点结构体定义 #include <stdio.h> #include <stdlib.h>// 二叉树节点结构体 typedef struct…

了解 SpringMVC 请求流程

文章目录 1. Spring 基础 - SpringMVC 请求流程1.1 引入1.2 什么是 MVC1.3 什么是 Spring MVC1.4 请求流程核心架构的具体流程步骤补充 1.5 案例**Maven 包引入****业务代码的编写**DaoServiceControllerwebapp 下的 web.xmlspringmvc.xmlJSP 视图 2. Spring 进阶 - Dispatcher…