elpis 一个企业级应用 —— elpix-core(内核)篇

340 阅读5分钟

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 参数进行一个校验

关于整个内核的运行机制

    1. 首先对当前的运行环境做一个查询,查询当前环境为什么环境。 loacl(本地)、beta(测试)、prod(生产)
    1. 然后接受来自页面的请求 通过router 去调用到 controller,这中间会经过 middleware 由 middleware 对相应的业务逻辑进行处理。然后由 controller 调用 service 从而获取数据与渲染页面。
  • 补充:
    • 在 middleware 中首先会对整个洋葱圈模型中的每一层进行成功与错误的监控
    • 其次是对请求的有效性进行检验
    • 然后是运用 router-schema 对API请求所携带的参数进行检验
    • 后面可能还有其他业务逻辑

中间件 middlerware 的运行机制

  • middleware 的核心为一个洋葱圈模型,可以将其执行的本质看作递归,通过next() 去异步的执行下一个中间件或返回上一个中间件(这取决于当前中间件后是否有下一个中间件)。如果在最后一个中间件内执行next() 则会将当前中间件内的程序走完然后返回至上一层中间件。如果一个中间件不存在next() ,则会在该中间件中断运行。返回一个 response 。
  • 作用:
    • 1.可以将 ctx 传递给下一层中间件
    • 2.可以将下一个中间件返回的内容作为 next() 的返回值
    • 3.如果上一层中间件依赖下一层中间件的结果时,则可以进行错误的拦截

image.png

总结

由内核的运行机制可以得出,首先整个内核在运行前需要先获取当前的运行环境config,然后是由router去调用 controller ,而在这之中会使用到 middleware 去监控成功与失败,后去根据router-schema内的设计去校验API参数,其中 middleware、router-schema、controller 这三个 loader 都需要依赖于 service

文章引用:抖音“哲玄前端”,《全栈实践课》