前言
本文为个人学习笔记,跟着哲哥(抖音:哲玄前端)学全栈之 elpis-core 内核实现和理解使用,该内核主要奉行约定优于配置的设计理念,通过一系列的load来加载约定目录下 config、middleware、service、extend、routerSchema、router、controller,可以减少开发人员的学习成本,无需考虑目录结构,只需要把对应的功能写到对应的文件中,引入 elpis-core ,调用start方法就能启动项目,接下来说一下load的实现和应用中各个目录的功能。
应用中各目录功能和elpis-core中load实现
- 应用目录结构图
Elpis
├─ .DS_Store
├─ app
│ ├─ controller
│ ├─ extend
│ ├─ middleware
│ ├─ middleware.js //全局中间件
│ ├─ public
│ ├─ router
│ ├─ router-schema
│ └─ service
├─ config
├─ elpis-core
config
该目录下放置不同环境的配置文件和默认配置文件
如: config.default.js(默认)、config.prod.js(生产)、config.local.js(开发)
elpis-core可以根据你运行的环境去加载对应的配置文件,且合并默认配置文件。
- configLoader 核心代码:
// 找到 config/ 目录
const configPath = path.resolve(process.cwd(), `./config`);
// 获取 default.config
let defaultConfig = {};
try {
defaultConfig = require(path.resolve(
configPath,
`./config.default.js`
));
} catch (e) {
console.log("[exception] there is no default.config file");
console.log(e);
}
// 获取 env.config
let envConfig = {};
try {
if (app.env.isLocal()) {
// 本地环境
envConfig = require(path.resolve(configPath, `./config.local.js`));
} else if (app.env.isProduction()) {
// 生产环境
envConfig = require(path.resolve(configPath, `./config.prod.js`));
}
} catch (e) {
console.log("[exception] there is no default.config file");
console.log(e);
}
// 覆盖并加载 config 配置
app.config = Object.assign({}, defaultConfig, envConfig);
controller
该目录下放置控制器文件,控制器是处理请求并返回响应的模块,它们会调用服务来获取数据并将其返回给客户端,如接口,页面。
elpis-core中对controller的处理是获取所有controller目录下的控制器,以文件的名字为key挂载到 koa实例的controller 上,在应用中我们可以通过 app.controller.xxx 获取。
controllerLoader 核心代码:
//业务文件路径
const businessPath = path.resolve(process.cwd(), `./app`);
// 读取 app/controller/**/**.js 下所有的文件
const controllerPath = path.resolve(businessPath, `./controller`);
const fileList = glob.sync(
path.resolve(controllerPath, `./**/**.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/`) + `controller/`.length,
name.lastIndexOf(".")
);
// 把 '-' 统一改为驼峰式,custom-module/custom-controller.js => customModule.customController
name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
// 挂载 controller 到内存 app 对象中
let tempController = controller;
const names = name.split(sep);
for (let i = 0, len = names.length; i < len; ++i) {
if (i === len - 1) {
const ControllerMoule = require(path.resolve(file))(app);
tempController[names[i]] = new ControllerMoule();
} else {
if (!tempController[names[i]]) {
tempController[names[i]] = {};
}
tempController = tempController[names[i]];
}
}
});
app.controller = controller;
extend
该目录下放置扩展文件,扩展的功能是为 koa 实例扩展额外的功能,如:日志
elpis-core中对extend的处理也是跟 controller 一样收集应用 extend 目录下的所有扩展,不同的是,extend是直接挂载到 koa实例 上, 应用中使用 app.xxx 获取
extendLoader 核心代码:
...
省略类似的文件获取代码
...
fileList.forEach((file) => {
...
省略类似的name获取代码
...
// 过滤 app 已经存在的 key
for (const key in app) {
if (key === name) {
console.log(`[extend load error] name:${name} is already in app`);
return;
}
}
app[name] = require(path.resolve(file))(app);
});
middleware
该目录放置中间件文件,中间件因其洋葱圈模型的特性,可以作用于很多功能,如:请求预处理作用(参数验证、身份认证和权限验证)、异常错误处理,兜底所有异常等
- 洋葱圈模型:
middlewareLoader 核心代码:
...
省略类似的文件获取代码
...
const middlewares = {};
fileList.forEach((file) => {
...
省略类似的name获取代码
...
for (let i = 0, len = names.length; i < len; ++i) {
if (i === len - 1) {
// 不同的是 middleware 是个函数,直接调用,controller是个类
tempMiddleware[names[i]] = require(path.resolve(file))(app);
} else {
if (!tempMiddleware[names[i]]) {
tempMiddleware[names[i]] = {};
}
tempMiddleware = tempMiddleware[names[i]];
}
}
});
app.middlewares = middlewares;
service
该目录放置服务文件,服务的功能是封装复杂业务场景下的业务逻辑,从而保持 controller 中的逻辑简洁,且一个 service 可提供给多个 controller 调用,也应分离逻辑和展示,从而便于编写测试用例。如:数据处理,第三方服务调用等
elpis-core中对service的处理跟controller相同,应用中使用 app.service.xxx 获取。
serviceLoader 核心代码:参考controller的实现,把 controller 换成 service
router-schema
该目录放置json-schema文件,该文件的作用主要是对接口数据的描述配合ajv对接口头部、参数等验证。如下方代码就是对接口*/api/project/list*的描述信息。如何写请参考json-schema网网
/**
* json-schema 描述
*/
module.exports = {
"/api/project/list": {
get: {
query: {
type: "object",
properties: {
proj_key: {
type: "string",
},
},
required: ["proj_key"],
},
},
},
};
elpis-core中对router-schema处理跟前几个有所不同,具体看下面代码实现。应用中使用的话是相同的,通过 app.routerSchema.xxx 获取。
routerSchemaLoader 核心代码:
...
省略类似的文件获取代码
...
const routerSchema = {};
fileList.forEach((file) => {
...
省略类似的name获取代码
...
// 不同部分,json-schema文件导出的是对象,所以处理如下
routerSchema = {
...routerSchema,
...require(path.resolve(file)),
};
});
app.routerSchema = routerSchema;
router
该目录放置路由配置文件,路由的功能定义页面和接口路由,映射对应的controller.
elpis-core中对router处理为收集所有路由注册和一个路由兜底,代码如下:
routerLoader 核心代码:'
// 找到路由文件路径, app.businessPath 上面代码有写
const routerPath = path.resolve(app.businessPath, `./router`);
// 实例化 KoaRouter
const router = new KoaRouter();
// 注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `./**/**.js`));
fileList.forEach((file) => {
require(path.resolve(file))(app, router);
});
// 路由兜底(健壮性)app.options 为 elpis-core 启动时传递的配置
router.get(/.*/, async (ctx) => {
ctx.status = 302; // 临时重定向
ctx.redirect(`${app?.options?.homePage ?? "/"}`);
});
// 路由注册到 app 上
app.use(router.routes());
app.use(router.allowedMethods());
各个load的加载顺序和存在的问题
这个加载顺序也是其中比较困惑的点,目前使用的顺序是为: config => extend => service => middleware => routerSchema => controller => router,遵循的原则就是被依赖方先加载,就是说后面的没有依赖的放最前面,有依赖前者的就放在前者后面。如:
- config: 是一个全局的配置,无需依赖任何文件,其他几个都需依赖该文件,放第一个
- extend:是扩展文件,只依赖配置文件,放第二个
- controller:依赖多个sevice,放sevice后,
- router:放最后
- 其他: 同理
但也有一定的缺点,如复杂的配置文件会影响后面文件的加载。
结尾
以后就是目前对 eplis-core 的理解和应用,可能会存在错误的理解和待优化的地方,希望各路大佬指点,我为大佬点烟,大佬对我笑嘻嘻!
注:抖音 “哲玄前端”,《大前端全栈实践课》