基于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的核心功能已基本实现
全文特别鸣谢: 抖音“哲玄前端”,《全栈实践课》