基于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.config
let defaultConfig = {};
try {
defaultConfig = require(path.resolve(configPath, `.${sep}config.default.js`));
} catch (e) {
console.log('[exceprion] there is no default.config file')
}
// 获取 env.config
let 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.controller
router.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-controller
name = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length, name.lastIndexOf('.'));
// 把'-'统一改为驼峰式, custom-module/custom-controller.js => customModule/customController
name = 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-extend
name = name.substring(name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length, name.lastIndexOf('.'));
// 把'-'统一改为驼峰式, custom-extend.js => customExtend
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 过滤 app 已经存在的key
for (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-middleware
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));
// 把'-'统一改为驼峰式, custom-module/custom-middleware.js => customModule/customMiddleware
name = 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.controller
router.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-service
name = name.substring(name.lastIndexOf(`service${sep}`) + `service${sep}`.length, name.lastIndexOf('.'));
// 把'-'统一改为驼峰式, custom-module/custom-service.js => customModule/customService
name = 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的核心功能已基本实现
全文特别鸣谢: 抖音“哲玄前端”,《全栈实践课》