elpis-core学习记录
前言
Elpis是一个js全栈开发的企业级应用,其目的是解决业务开发中重复的CRUD劳动。共有五个阶段:
- 基于 node + koa 实现一个服务端开发框架
- 基于 webpack5 + express 完成前端工程化建设
- 基于“领域模型方案”完成系统架构设计
- 基于 vue3 + elementPlus 完成前端动态组件
- 抽象封装 elpis 并发布到 NPM
此文是完成了第一个阶段elpis-core后记录一下自己的学习理解。
简介
elpis-core最大的特点是模块的自动加载,通过不同的loader,将各个功能模块注入到koa的实例app上:将app作为入参,在不同的loader里加载对应模块的所有文件,然后将文件的返回值作为一个属性挂载到app下。这种处理,方便各个模块的拆分,却同时又通过app关联起来,是一种高内聚低耦合的设计。
简单贴一下代码,详细的代码讲解可搜索站内其他elpis文章
入口文件index.js
const Koa = require("koa");
const path = require("path");
const env = require("./env");
const configLoader = require("./loader/config");
const routerLoader = require("./loader/router");
const routerSchemaLoader = require("./loader/router-schema");
const controllerLoader = require("./loader/controller");
const serviceLoader = require("./loader/service");
const extendLoader = require("./loader/extend");
const middlewareLoader = require("./loader/middleware");
const { sep } = path; // 兼容不同操作系统下的斜杠
module.exports = {
/**
* 启动项目
* @param {object} options 项目配置
* options = {
* name // 项目名称
* homePage // 项目首页
* }
*/
start(options = {}) {
// koa 实例
const app = new Koa();
// 应用配置
app.options = options;
// 基础路径
app.baseDir = process.cwd();
// 业务文件路径
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
console.log(
`基础路径app.baseDir:${app.baseDir} 业务文件路径 app.businessPath:${app.businessPath}`
);
// 初始化环境配置
app.env = env();
console.log(`-- [start] env is: ${app.env.get()} --`);
// 加载middleware
middlewareLoader(app);
// console.log(`${app.middlewares}`);
console.log(`-- [start] load middleware done --`);
// 加载路由schema
routerSchemaLoader(app);
// console.log(`${app.routerSchema}`);
console.log(`-- [start] load router schema done --`);
// 加载controller
controllerLoader(app);
// console.log(`${app.controller}`);
console.log(`-- [start] load controller done --`);
// 加载service
serviceLoader(app);
// console.log(`${app.service}`);
console.log(`-- [start] load service done --`);
// 加载config
configLoader(app);
// console.log(`${app.config}`);
console.log(`-- [start] load config done --`);
// 加载extend
extendLoader(app);
// console.log(`${app.extend}`);
console.log(`-- [start] load extend done --`);
// 注册全局中间件,即用户自定义loader(只会在app/middleware.js下)
try {
require(`${app.businessPath}${sep}middleware.js`)(app);
console.log(`-- [start] load global middleware done --`);
} catch (error) {
console.log(`-- [exception] global middleware file error --`);
}
// 注册路由
routerLoader(app);
console.log(`-- [start] load router done --`);
// 启动服务
try {
const port = process.env.port || "8080";
const host = process.env.host || "0.0.0.0";
app.listen(port, host);
console.log("server running on port:", port);
} catch (error) {
console.error(error);
}
},
};
业务逻辑处理controller.js
const glob = require("glob");
const path = require("path");
const { sep } = path;
/**
* controller loader
* @param {object} app Koa 实例
*
* 加载所有 middleware 可通过 `app.controller.${目录}.${文件}` 访问
*
* 示例:
* app/controller
* |
* | -- custom-module
* |
* | -- custom-controller.js
* 输出 => app.middlewares.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 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 => customModule/customController
name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
// 挂在 controller 到内存app对象中
let tempController = controller;
const names = name.split(sep); // ['customModule', 'customController']
for (let i = 0, len = names.length; i < names.length; ++i) {
if (i === len - 1) {
const ControllerModule = require(path.resolve(file))(app);
tempController[names[i]] = new ControllerModule();
} else {
if (!tempController[names[i]]) {
tempController[names[i]] = {};
}
tempController = tempController[names[i]];
}
}
});
app.controller = controller;
};
- 环境配置:configLoader
- 功能扩展:extendLoader + middlewareLoader
- 逻辑分层:serviceLoader + controllerLoader
- 路由管理:routerSchemaLoader + routerLoader
收获
-
这个阶段最大的收获是使用js完成了一个服务端开发框架。之前是纯前端业务开发,对于服务端的理解还停留在大学期间。从前端发送请求,到服务端接受到请求以后,通过controller处理业务,service处理数据,完成了整个流程的开发。
-
对于koa洋葱圈模型的理解加深。刚开始讲解elpis-core架构的时候,看这张图是挺懵的,现在清楚了。一个请求会经过一层层的处理到达核心(拿数据或静态资源),在核心拿到数据资料后再一层层的返回。
- 对于api请求。elpis-core中第一层是异常捕获,第二层是请求的签名合法性校验,第三层时api参数校验。经过这三层以后才会到controller中进行业务处理,controller中处理完毕后,到service中进行数据处理。处理好的数据再一层层的返回。
-
约定优于配置,开发流程标准。目录结构十分清晰,只需按照约定,在不同的目录下添加自己的业务代码,即可完成自己的定制化需求。
-
逆向思维的建立。课程中先写loader,再写app(loader中就已经用到app下的文件)。先处理的接口健壮性,再由前端发起验证(不是前端发现接口有问题的时候再处理接口错误)。对于顺向思维比较强的前端开发者,逆向思维的抽象十分利于建立自己的架构思维
课程优点
- 一个个文件建,一行行代码敲;
- 可见编码时的错误及错误解决,注重代码健壮性;
- 不讲解api,更多的是思想体系的建立
- 亲自代码评审且有注解
- 十分适用于有一定经验的我
引用: 抖音“哲玄前端”《大前端全栈实践》