在设计企业级中后台框架时,约定优先于配置和模块化的理念至关重要。良好的设计能够促进多人协作,提高开发一致性,减少重复配置代码,进而提升开发效率,降低人为错误及复杂度。此外,引入自动化机制可以进一步提升开发效率。
一、约定项目结构
│ app
│ -- controller # 控制器层,处理业务逻辑和请求响应
│ -- extend # 自定义公共组件或一些通用方法
│ -- middleware # 各种中间件
│ -- router # 路由配置
│ -- router-schema # API接口的路由和参数规则
│ -- service # 服务层,负责数据库与外部API的交互
│ -- router-schema # 页面组件,每个页面一个文件夹,包含该页面的逻辑和UI
│ -- middleware.js # 中间件配置文件
│ config
│ -- config.default.js 默认系统配置
│ -- config.local.js 开发环境系统配置
│ -- config.beta.js 测试环境系统配置
│ -- config.prod.js 生产环境系统配置
│ elpis-core
│ -- loader
│ -- config.js # 通过 env 读取环境配置
│ -- controller.js # 加载所有在 app/controller 目录下的controller
│ -- extend.js # 加载所有在 app/extend 目录下的 extend
│ -- middleware.js # 加载所有在 app/middleware 目录下的 middleware
│ -- router-schema.js # 加载所有在 app/router-schema 目录下的 router-schema
│ -- router.js # 解析所有在 app/router/ 目录下所有的js文件,加载到koaRouter中
│ -- service.js # 加载所有在 app/service 目录下的 service
│ -- env.js # Koa应用测试环境配置
│ -- index.js # Koa应用生产环境配置
│ 略
二、基于约定项目结构实现自动挂载机制
1、例controller.js
const glob = require("glob");
const path = require("path");
const { sep } = path;
/**
* controller
* @param {*} 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 下
let 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(".")
);
// 把 '_' 统一改为驼峰式,custome_module/custome_controller.js => customeModule/customecontroller
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 挂载 controller 到内存 app 对象中
const names = name.split(sep); // [ customModule(目录), customController(文件) ]
// 挂载 controller 到内存 app 对象中去
let tempController = controller;
for (let i = 0, len = names.length; i < len; ++i) {
if (i === len - 1) {
// 文件
const tempControllerModule = require(path.resolve(file))(app);
tempController[names[i]] = new tempControllerModule();
} else {
// 文件夹
if (!tempController[names[i]]) {
tempController[names[i]] = {};
}
tempController = tempController[names[i]];
}
}
});
app.controller = controller;
};
2、例router.js
const koaRouter = require("koa-router");
const glob = require("glob");
const path = require("path");
const { sep } = path;
/**
* router loader
* @param {*} app Koa实例
* 解析所有 app/router/ 下所有js文件,加载到koaRouter下
*
*/
module.exports = (app) => {
// 找到路由文件路径
const routerPath = path.resolve(app.businessPath, `.${sep}router`);
// 实例化 koaRouter
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?.homePage ?? "/"}`);
});
// 路由注册到 app 上
app.use(router.routes());
app.use(router.allowedMethods());
};
3、挂载loader
const Koa = require("koa");
const path = require("path");
// 兼容不同操作系统上的斜杠
const { sep } = path;
const env = require("./env");
const middleWareLoader = require("./loader/middleware.js");
const routerSchemaLoader = require("./loader/router-schema.js");
const routerLoader = require("./loader/router.js");
const controllerLoader = require("./loader/controller.js");
const serviceLoader = require("./loader/service.js");
const configLoader = require("./loader/config.js");
const extendLoader = require("./loader/extend.js");
module.exports = {
/**
* 启动项目
* @param {*} options
* @param {string} options.name 项目名称
* @param {string} options.homePath 项目首页
*
*/
start(options = {}) {
// Koa实例
const app = new Koa();
app.options = options;
// 基础路径
app.baseDir = process.cwd();
// 业务文件路径
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
// 初始化环境配置
app.env = env();
console.log(`-- [start] env: ${app.env.get()} --`);
// 加载 middleware
middleWareLoader(app);
console.log("-- [start] load middleware done --");
// 加载 routerSchema
routerSchemaLoader(app);
console.log("-- [start] load routerSchema 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 (e) {
console.log("[expection] there is no global middleware file");
}
// 加载 router
routerLoader(app);
console.log("-- [start] load router done --");
// 启动服务
try {
const port = process.env.PORT || 9527;
const host = process.env.IP || "0.0.0.0";
app.listen(port, host);
console.info(`Server running on port: ${port}`);
} catch (e) {
console.error(e);
}
},
};
4、总结
通过约定目录结构这样的挂载机制,使得开发者可以更加专注于业务逻辑,而无需手动去配置各个模块或组件,提高开发效率,并使项目结构更加清晰。这种约定优于配置的设计理念,有助于开发团队遵循一致的惯例,促进代码的可维护性和可扩展性。
三、洋葱圈模型解析
图1
洋葱圈模型是 Koa 框架(Egg.js)中间件执行的核心概念,用于处理请求和响应的顺序。它将中间件的执行过程形象地比作洋葱圈,通过不同层级分层处理请求和响应,体现了单向的执行过程。
中间件执行顺序:
-
请求处理阶段(从外向内):
- 当请求到达时,中间件的执行是从外向内的。最外层的 middleware1 首先执行,完成后调用
await next()
将控制权传递给下一个中间件 middleware2,依此类推,直到 middleware3。当所有的中间件执行完成后,请求会被发送到路由处理的controller控制器。
- 当请求到达时,中间件的执行是从外向内的。最外层的 middleware1 首先执行,完成后调用
-
响应处理阶段(从内向外):
- 当控制器处理完请求并返回结果后,响应的处理则是从内层向外层进行的。相应会首先在最内层的中间件,上层的中间件可以访问到响应数据,进行处理,最终将响应返回给客户端。
原因:
这种设计方式的原因在于:
-
灵活性:
- 在请求处理阶段,外层的中间件可以负责全局的操作,比如api的参数、签名校验等等,如果请求不成立,则可以直接返回响应。
-
控制权:
- 在响应处理阶段,开发者可以针对响应数据进行修改,最终返回给客户端,实现更好的控制。
四、总结
工欲善其事,必先利其器。一个良好的架构就如同一台高效的发动机,为系统提供稳定而强大的动力输出。同时,一个优秀的架构应具备应对高负载和灵活扩展的能力,以便为团队创造相对更好的工作条件,使得每个人能够专注于业务,尽可能地避免回头处理基础设施问题。
powered by 抖音“哲玄前端”,《全栈实践课》