elpis-core 简介
这是elpis应用的内核,由middleware、routerschema、controller、service、config、extend、router组成。
知识补充
elpis-core 所用到的模块、插件等
- path.resolve: 将路径换为绝对路径
- glob: 将文件组合成对象
- koa-router
- nodemon: 用于启动项目,且与node不同的是当文件修改后会自动重启
- koa-Nunjucks: 用于渲染模板
- koa-static: 使得静态资源能得到解析
- koa-bodyParser: 解析body,使得post请求的信息能够读取
- md5:将信息进行加密
- json-schema: 一个描述 json 文件的 json 文件,用于描述 json 文件内的内容
- ajv: 对 json 文件与 json-schema 内的文件进行比较
elpis-core 的加载顺序
理论顺序
config => service => middleware => router-schema => controller => extend => router
实际顺序
middleware => router-schema => controller => service => config => extend => router
各个 loader 介绍
小模块功能介绍(用于各个 loader 中)
查找路径功能
- 功能: 读取app/loader//.js下的所有文件,然后返回一个拥有所有文件的数组。
const middlewarePath = path.resolve(app.businessPath,`.${sep}middleware`);
const fileList = glob.sync(path.resolve(middlewarePath,`.${sep}**${sep}**.js`));
截取路径功能
- 功能: 截取路径,将文件的路径从一个由app路径开始的路径截取为剩下最后的文件信息的路径,并且将文件的后缀去除。
- 例子: app/middlewares/custom-module/custom-middleware.js => custom-module/custom-middleware
const middlewarePath = path.resolve(app.businessPath,`.${sep}middleware`);
const fileList = glob.sync(path.resolve(middlewarePath,`.${sep}**${sep}**.js`));
路径名驼峰转换功能
- 功能: 把路径中的 '-' 转换为驼峰式的书写方法
- 例子: custom-module/custom-middleware => customModule/customMiddleware
name = name.replace(/[_-][a-z]/ig,(s)=> s.substring(1).toUpperCase());
loader 介绍
- middleware(中间件)
- router-schema(路由参数校验的规则)
- controller(处理器)
- service
- config
- extend
- router
middleware
- 功能: 对请求和响应进行处理,例如: API 参数校验 、 API 参数合法性校验 、处理错误异常
- 处理方式: 该模块能够将middleware文件夹下的文件进行处理,处理成通过a.b.c的方式读取,然后挂载到 app.middlewares 下
- 处理机制: 将所有业务逻辑以洋葱圈模型进行处理,通过 request 进入洋葱圈模型,后 response 出来
const middlewarePath = path.resolve(app.businessPath,`.${sep}middleware`);
const fileList = glob.sync(path.resolve(middlewarePath,`.${sep}**${sep}**.js`));
// 遍历所有文件目录,把所有内容加载到app.middlewares下
const middlewares = {};
fileList.forEach(file => {
// 提取文件名称
let name = path.resolve(file);
// 截取路径 AS: app/middlewares/custom-module/custom-middleware.js => custom-module/custom-middleware
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length,name.lastIndexOf('.'));
// 把'-'统一改为驼峰式 AS:custom-module/custom-middleware => customModule.customMiddleware
name = name.replace(/[_-][a-z]/ig,(s)=> s.substring(1).toUpperCase());
// 挂载middleware 到 内存 APP 对象中
let tempMiddleware = middlewares;
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);
}else{
if(!tempMiddleware[names[i]]){
tempMiddleware[names[i]] = {}
}
// tempMiddleware === { customModule:{ a:{ b:{ } } } }
tempMiddleware = tempMiddleware[names[i]]
// tempMiddleware === {}
}
}
});
app.middlewares = middlewares;
router-schema
- 功能: 设置对每一个 API 的校验规则,配合 middleware 对 API 的参数进行校验
- 处理方式: 将API规则进行约束,将文件路径放置到一个对象之中,然后挂载到 app.routerSchema 下
// 读取app/router-schema/**/**.js下的所有文件
const routerSchemaPath = path.resolve(app.businessPath,`.${sep}router-schema`);
const fileList = glob.sync(path.resolve(routerSchemaPath,`.${sep}**${sep}**.js`));
// 注册所有routerSchema,使得可以'app.routerSchema'这样访问
let routerSchema = {};
// 处理过程
fileList.forEach(file =>{
routerSchema ={
...routerSchema,
...require(path.resolve(file))
}
})
app.routerSchema = routerSchema
controller
- 功能: 处理请求,并使用 service 中相应的方法来进行处理
- 处理方式: 该模块能够将controller文件夹下的文件进行处理,处理成通过a.b.c的方式读取。然后挂载在 app.controller 下
// 读取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);
// 截取路径 AS: app/controller/custom-module/custom-controller.js => custom-module/custom-controller
name = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length,name.lastIndexOf('.'));
// 把'-'统一改为驼峰式 AS:custom-module/custom-controller.js => customModule.customController
name = name.replace(/[_-][a-z]/ig,(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 === { customModule:{ a:{ b:{ } } } }
tempController = tempController[names[i]]
// tempController === {}
}
}
});
app.controller = controller;
service
- 功能: 对具体的业务逻辑处理,并被 controller 调用使用
- 处理方式: 该模块能够将service文件夹下的文件进行处理,处理成通过a.b.c的方式读取,然后将文件挂载到APP中。然后挂载到 app.service 下
// 读取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);
// 截取路径 AS: app/service/custom-module/custom-service.js => custom-module/custom-service
name = name.substring(name.lastIndexOf(`service${sep}`) + `service${sep}`.length,name.lastIndexOf('.'));
// 把'-'统一改为驼峰式 AS:custom-module/custom-service.js => customModule.customService
name = name.replace(/[_-][a-z]/ig,(s)=> s.substring(1).toUpperCase());
// 挂载service 到 APP 中
let tempService = service;
const names = name.split(sep) //[customModule,customservice]
for (let i =0,len = names.length;i < len;i++){
if(i == len - 1){ // 文件
const serviceMoule = require(path.resolve(file))(app)
tempService[names[i]] =new serviceMoule();
}else{ //文件夹
if(!tempService[names[i]]){
tempService[names[i]] = {}
}
// tempService === { customModule:{ a:{ b:{ } } } }
tempService = tempService[names[i]]
// tempService === {}
}
}
});
app.service = service;
config
- 功能: 配置当前的环境变量,确立后续的运行环境
- 处理方式: 读取是什么配置(本地/测试/生产),后通过将env.config覆盖 default.config 最后加载到app.config
// 找到 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(' [exception] 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.isProduction()) { //生产环境
envConfig = require(path.resolve(configPath,`.${sep}config.prod.js`));
}
} catch (e) {
console.log(' [exception] there is no env.config file');
}
// 覆盖并加载 config 配置
app.config = Object.assign({},defaultConfig,envConfig);
extend
- 功能: 挂载一些辅助工具或拓展功能等
- 处理方式: 该模块能够将extend文件夹下的文件进行处理,extend下默认没有目录只有js文件,最后将相应的文件挂载到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);
// 截取路径 AS: app/extend/custom-extend.js => custom-extend
name = name.substring(name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length,name.lastIndexOf('.'));
// 把'-'统一改为驼峰式 AS:custom-extend =>customExtend
name = name.replace(/[_-][a-z]/ig,(s)=> s.substring(1).toUpperCase());
// 过滤 app 已经存在的key
for (const key in app){
if(key === name){
console.log(` [extend load error] name:${name} is already in app `);
return
}
}
// 挂载extend 到 APP 中
app[name] = require(path.resolve(file))(app);
});
router
- 功能: 对请求进行处理,并调用相应的 controller 方法
- 处理方式: 注册所有路由,并且创建一个路由兜底(路由重定向),最后把注册的路由挂载到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 =>{
// 每个router文件内的内容
// module.exports = (app,router)=>{
// router.get('xxx/xx/xx/',xxxContro)
// }
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());
app 中的 middleware 中间件
说明: 此为整个引擎的核心,洋葱圈模型。可以添加一些校验规则、渲染页面等中间件。
koa-static
功能: 配置静态资源的目录,从而使得静态资源能够正常加载
koaNunjucks
功能: 用于渲染模板
koa-bodyparser
功能: ctx.body 解析中间件 能够解析 post 、 get 请求的信息,也就是 request 中的 body、query、headers 内的信息
error-handler
功能: 运行时异常错误处理,兜底所有异常
api-sign-verify
功能: 对 API 签名的合法性进行校验
api-params-verify
功能: 对 APi 参数进行一个校验
关于整个内核的运行机制
-
- 首先对当前的运行环境做一个查询,查询当前环境为什么环境。 loacl(本地)、beta(测试)、prod(生产)
-
- 然后接受来自页面的请求 通过router 去调用到 controller,这中间会经过 middleware 由 middleware 对相应的业务逻辑进行处理。然后由 controller 调用 service 从而获取数据与渲染页面。
- 补充:
- 在 middleware 中首先会对整个洋葱圈模型中的每一层进行成功与错误的监控
- 其次是对请求的有效性进行检验
- 然后是运用 router-schema 对API请求所携带的参数进行检验
- 后面可能还有其他业务逻辑
中间件 middlerware 的运行机制
- middleware 的核心为一个洋葱圈模型,可以将其执行的本质看作递归,通过next() 去异步的执行下一个中间件或返回上一个中间件(这取决于当前中间件后是否有下一个中间件)。如果在最后一个中间件内执行next() 则会将当前中间件内的程序走完然后返回至上一层中间件。如果一个中间件不存在next() ,则会在该中间件中断运行。返回一个 response 。
- 作用:
- 1.可以将 ctx 传递给下一层中间件
- 2.可以将下一个中间件返回的内容作为 next() 的返回值
- 3.如果上一层中间件依赖下一层中间件的结果时,则可以进行错误的拦截
总结
由内核的运行机制可以得出,首先整个内核在运行前需要先获取当前的运行环境config,然后是由router去调用 controller ,而在这之中会使用到 middleware 去监控成功与失败,后去根据router-schema内的设计去校验API参数,其中 middleware、router-schema、controller 这三个 loader 都需要依赖于 service。
文章引用:抖音“哲玄前端”,《全栈实践课》