- 作者:老汪软件技巧
- 发表时间:2024-12-30 21:04
- 浏览量:
前言
一套好的框架往往伴随的是一套简单容易理解的规范以及用法,所谓的 egg.js (KOA) 上层框架, 既是约定了一套它的规范,可以更好更便捷的使用 KOA 这篇文章就来实现一下 elpis-core (简易的 egg.js 内核) 并补充说明一下我个人对洋葱圈模型的理解吧。
规范
每一套框架有每一套的规范,往往有框架会指出:实现 xxx 功能可以将 xxx 文件放到 xxx 文件夹下面,快速方便构造使用,elpis-core 也不例外,也会有自己的规范
|app (执行文件)
|-- controller (主要存放处理主要逻辑的类文件 例如:解析接口数据,更加需求调动 service 内部类文件)
|-- extend (主要存放拓张工具 例如:打印日志文件工具)
|-- middleware (主要存放各种各样自定义中间件)
|-- router (主要存放路由文件 例如:路由的定义 而后绑定 controller 类处理对应逻辑)
|-- router-schema (主要存放路由对应的校验规则)
|-- service (主要存放各种服务的类文件 例如:数据库的调用)
|config(环境文件)
|-- config.default.js (运行时的容器,运行时只读取 default 里面的数据)
|-- config.local.js (开发环境的文件,运行生产环境时合并到 default 中)
|-- config.beta.js (测试环境的文件,运行生产环境时合并到 default 中)
|-- config.prod.js (生产环境的文件,运行生产环境时合并到 default 中)
实现
调用方法
const ElpisCore = require("./elpis-core");
//项目启动
ElpisCore.start({
name: "Elpis",
homePage: "/view/page2"
});
上述调用可得知,启动需要提供一个 start 方法,并从规范得知需要创建七个 loader 处理对应的执行文件和环境文件,下述为文件目录
|elpis-core
|-- loader (整合 对应文件夹下的 js 文件并将其挂载到 app 上)
|-- config.js (判断环境,根据环境合并对应的文件)
|-- controller.js
|-- extend.js
|-- middleware.js
|-- router.js
|-- router-schema.js
|-- service.js
|-- index.js (内核启动文件)
项目启动文件
const Koa = require('koa');
const env = require('../env');
//为了兼容不同操作系统上的斜杠
const path = require('path');
const { sep } = path;
// 引入所有 loader
const configLoader = require('./loader/config');
const controllerLoader = require('./loader/controller');
const routerLoader = require('./loader/router');
const routerSchemaLoader = require('./loader/router-schema');
const serviceLoader = require('./loader/service');
const middlewareLoader = require('./loader/middleware');
const extendLoader = require('./loader/extend');
// 值得一提的是 router 必须最后去注册 因为要建立洋葱圈模型 经过中间件层次筛选才进入路由实现功能
module.exports = {
/**
* 项目启动入口
* @param {object} options 项目配置参数
* options = {
* name 名称,
* homePage 项目首页
* }
* */
start (options = {}) {
// 创建 Koa 实例
const app = new Koa();
// 挂载实例配置到 app 对象
app.options = options;
// 基础路径 一般指的项目根目录
app.baseDir = process.cwd();
// 业务文件路径 跟目录下的./app
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
// 初始化环境配置
app.env = env();
console.log(`-- [start] env: ${app.env.get()} --`);
// 加载所有 loader
// 加载 middleware
middlewareLoader(app);
console.log(`-- [start] load middleware done --`);
// 加载 router-schema
routerSchemaLoader(app);
console.log(`-- [start] load router-schema done --`);
// 加载 controller
controllerLoader(app);
console.log(`-- [start] load controller done --`);
// 加载 service
serviceLoader(app);
console.log(`-- [start] load service done --`);
// 加载 config
configLoader(app);
console.log(`-- [start] load config done --`);
// 加载 extend
extendLoader(app);
console.log(`-- [start] load extend done --`);
// 注册全局中间件
try {
require(`${app.businessPath}${sep}middleware.js`)(app);
console.log(`-- [start] load global middleware done --`);
} catch (err) {
console.log(`[exception] there is no global middleware file`);
}
// 注册路由
routerLoader(app);
console.log(`-- [start] load router done --`);
// 启动 Koa 服务
try {
const port = process.env.PORT || 8080;
const host = process.env.IP || '0.0.0.0';
app.listen(port, host);
console.log(`Listening on ${host}:${port}`);
} catch (err) {
console.error(err);
}
}
};
关键代码实现 (除 config.js router.js 外 其余基本类似 以 controller 为例)
config.js 主要功能是判断环境变量合并文件
router.js 大概与实例代码相似但最终是以 use 的方式直接注册挂载而不是单纯的放入 app 对象中
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.customModule.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 controllers = {};
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('.'));
// 将 “-” 的文件名称 改为驼峰式
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 将 controllers 存到 app 对象中
let tempController = controllers;
const names = name.split(sep);
names.forEach((name_each, index, self) => {
if (index < self.length - 1) {
if (!tempController[name_each]) tempController[name_each] = {};
tempController = tempController[name_each];
} else {
// 值得一提的是 除了两个类文件 其他的直接引入即可 无需特意 new 出来
// tempController[name_each] = require(path.resolve(file))(app)
// 而 extend 无需深层循环毕竟只是工具文件 注意不要跟 app 中自带的方法冲突即可
const controllerModule = require(path.resolve(file))(app);
tempController[name_each] = new controllerModule();
}
});
});
app.controller = controllers;
};
应用
就这样一个基础的内核就实现了那么如何使用呢 (利用内核运行一个简单的接口)
1.首先理所应当的是根据规范创建对应的文件夹
2.注册路由并创建对应的 controller 文件和 service 文件
// app/router/project.js
module.exports = (app, router) => {
const { project: projectController } = app.controller;
router.get('/api/projects/list', projectController.getList.bind(projectController));
};
// app/controller/project.js
module.exports = (app) => {
return class ProjectController {
/**
* 获取项目列表
* @param {object} ctx 上下文
* */
async getList (ctx) {
const { project: projectService } = app.service;
const projectList = await projectService.getList();
ctx.status = 200;
ctx.body = {
success: true,
data: projectList
};
}
};
};
// app/service/project.js
module.exports = (app) => {
return class ProjectService {
async getList () {
return [
{
name: 'project1',
desc: 'project 1 description'
},
{
name: 'project2',
desc: 'project 2 description'
},
{
name: 'project3',
desc: 'project 3 description'
},
{
name: 'project4',
desc: 'project 4 description'
}
];
}
};
};
最后启动服务在浏览器中看到该内容就成功啦!
补充 洋葱圈模型
相信很多朋友多多少少都听到过这个词吧 其实也很好理解 下面图所示吧
很好理解吧,每加一层中间件校验都在路由外层包裹,通过各种中间件,无论是自定义的还是 KOA 自带的中间件,不断的去调用,每调用一次就加一层,最后就会得到这么个很像洋葱的玩意,没理解完全没关系,接下来来也用该内核实现一个简单的中间件案例吧。
tips:在 elpis-core 的入口文件中一定要先注册中间件 再注册路由才能达到洋葱圈模型的效果创建两个中间件
// app/middleware/first-middleware-verify.js
moudle.exports = (app) => {
retrun async (_, next) => {
console.log("中间件1");
await next();
console.log("中间件1");
}
}
// app/middleware/second-middleware-verify.js
moudle.exports = (app) => {
retrun async (_, next) => {
console.log("中间件2");
await next();
console.log("中间件2");
}
}
调用中间件
// app/middleware.js
module.exports = (app) => {
app.use(app.middlewares.firstMiddlewareVerify);
app.use(app.middlewares.secondMiddlewareVerify);
}
在路由中打印点东西验证洋葱圈模型
// app/controller/project.js
module.exports = (app) => {
return class ProjectController {
async getList (ctx) {
···
console.log("路由");
···
}
};
};
接下来执行路由可以在控制台看到
由中间件1 -> 中间件2 -> 路由 -> 中间件2 -> 中间件1 完成整个路由的流程或者理解成 KOA 提供的 next 方法可以看成就是下一个中间件或路由
moudle.exports = (app) => {
retrun async () => {
console.log("中间件1");
console.log("中间件2");
···
console.log("路由");
···
console.log("中间件2");
console.log("中间件1");
}
}
总结
以上就是我对上层框架内核和洋葱圈模型的理解和运用了,如有错误请广大网友帮我指正,幸苦咯~~
当然内核也不是特别的完善 像根本不支持 ts 和没有强约束 毕竟规范只是规定,也可以用一些流氓的写法例如不用service 呀不用 controller 呀,直接处理在 router 中也不是不行,就是不符合 elpis-core 的规范罢了,总之有理解不到位的地方请指正吧。
powered by 抖音“哲玄前端”,《全栈实践课》