基于KOA——ELPIS-CORE内核

217 阅读4分钟

ELPIS-CORE内核

前言

本文为阶段性学习笔记,主要记录全栈框架学习过程中的内核部分—— ELPIS-CORE。

elpis-core为框架解析器,将项目文件读取出来编译后形成可运行的业务代码,基于“约定大于配置“的原则,提升前端项目开发效率。其代码存放在elpis-core目录下,而要转化的项目文件则存放在app目录下,目录结构与内核保持一致。可以这样理解,elpis是自动炒菜机,而app目录下的项目文件则是食材、水等原材料,根据不同的菜谱(项目实际需求)将不同的原材料到炒菜机中就会做出不同的饭菜(项目成品)。主要以koa为依托进行开发

image.png

app
├── controller
├── extend
├── middleware
├── middleware.js //全局中间件
├── public
├── router
├── router-schema
└── service
elpis-core
├── env.js
├── index.js
└── loader
   ├── config.js
   ├── controller.js
   ├── extend.js
   ├── middleware.js
   ├── router-schema.js
   ├── router.js
   └── service.js

loader介绍

1.middleware

将项目文件中的同名文件夹中的各个中间件以名称为key的方式逐一注册到koa的middlewares中,通过app.middlewares.xxx 进行调用。该loader借助自身洋葱模型的特性做所有请求的筛选、过滤、和兜底处理。同时也让其中的每个中间件都只关注自身业务,明确逻辑的执行顺序。以下是其核心代码片段

module.exports = (app) => {
    // 拿到app/middleware目录下的所有文件
    const middlewares = {}
    let tempMiddleware = middlewares;
    
    
	fileList.forEach(file => 
		//获取文件名称
		let name = path.resolve(file)
		const names = name.split('/')
			//使用循环以此将所有中间件挂载到app.middlewares上
        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 = tempMiddleware[names[i]]
            }
        }
   })
    app.middlewares = middlewares
}

2.router-schema

对接口请求格式进行校验,具体配置及使用参考官网:json-schema.org/

该loader的核心代码如下:

module.exports = (app) => {
    //读取app/router-schema/**.js下的所有文件,得到fileList 
    //注册所有routerSchema,使得可以app.routerSchema这样访问
    let routerSchema = {}
    fileList.forEach(file => {
        routerSchema = {
            ...routerSchema,
            ...require(path.resolve(file))
        }
    })
    app.routerSchema = routerSchema
}

以下是app/router-schema下对api/project/getlist接口的示例校验文件:

module.exports = {
    '/api/project/list':{
        //请求方式
       get:{
           //需要校验的参数
           query:{
               type:'object',
               properties:{
                   proj_key:{
                       type:'string',
                       description:'项目key'
                   },
               },
               required:['proj_key']
           }
       }
    }
}

3.controller

业务处理器,主要处理请求并返回响应,会在运行时调用service获取数据并返回给客户端。与middleware的处理方式相同,都是通过获取所有controller目录下的文件名称后以名称为key挂载到koa的controller上,可以通过app.controller.xxx进行调用。核心代码如下:

/*
 *
 * 加载所有的controller,通过app.controller.目录.文件的形式访问
 *
 * 例如:
 *    app/controller
 *     |
 *     | -- controller-module
 *             |
 *             |  -- controller-middleware.js
 *
 *      相当于 app.controller.customController.customerController
 * }
 */

module.exports = (app) => {
    //读取app/controller文件夹下的所有文件
    const businessPath = path.resolve(process.cwd(), `./app`)
    const controllerPath = path.resolve(bussinessPath,`./controller`)
    const fileList = glob.sync(path.resolve(controllerPath,`./**/**.js`))

    const controller = {}

    fileList.forEach(file => {
        let name = path.resolve(file)
        name = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length,name.lastIndexOf('.'))
        name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase()) //将-后的字符的第一位转为大写

        let tempController = controller
        const names = name.split(sep)  //['customModule(目录)','customController(文件)']
        for(let i = 0, len = names.length; i < len; ++i){
            if(i === len -1){
                const ControllerModule = require(path.resolve(file))(app)  //返回class
                tempController[names[i]] = new ControllerModule()  //new class
            } else {
                if(!tempController[names[i]]) {
                    tempController[names[i]] = {}
                }
                tempController = tempController[names[i]]
            }
        }
    })
    app.controller = controller
}

4.service

提供原子化方法,封装具体业务逻辑,由controller进行调用,保持请求处理逻辑的简洁明了。其实现与controller相同,将controller核心代码中的controller替换为service 即可,这里不再过多赘述。

5.config

环境配置,会读取项目根目录下app文件夹中同名文件夹下的js文件(例如app/config/config.local.js),会在运行时加载对应的环境配置文件,如果不设置当前环境的话则加载默认配置。以下是config-loader的核心代码:

module.exports = (app) => {
    // 找到config 目录
    const configPath = path.resolve(process.cwd(),`./config`)

    // 获取default.config
    let defaultConfig = {}
    //防止没有默认配置文件引发require报错
    try {
        defaultConfig = require(path.resolve(configPath,`./config.default.js`))
    } catch (error) {
        console.log('没有找到默认配置文件')
    }

    // 获取源自env.js文件中的环境获取方法
    let envConfig = {}
    try {
        if(app.env.isLocal()){    // 本地环境
            envConfig = require(path.resolve(configPath,`./config.local.js`))
        } else if(app.env.isBeta()){   // 测试环境
            envConfig = require(path.resolve(configPath,`./config.beta.js`))
        } else if(app.env.isProduction()) {  // 生产环境
            envConfig = require(path.resolve(configPath,`./config.prod.js`))
        }
    } catch (error) {
        console.log('没有找到env配置文件')
    }

    // 覆盖并加载配置
    app.config = Object.assign({},defaultConfig,envConfig)
}

6.extend

该loader加载对应app/extend中的扩展配置文件,实现对koa的功能拓展,例如错误日志信息收集。与其他loader不同的是其直接挂载到KOA实例上,通过app.xxx调用。核心代码如下

//获取文件名称
//遍历所有文件目录,将内容加载到app.extend中
fileList.forEach(file => {
    let name = path.resolve(file)
    
    //过滤app已经存在的key
    for(const key in app) {
        if(key === name) {
            console.log(`name:${name} is already in app`)
            return
        }
    }

    //挂载extend到app上
    app[name] = require(path.resolve(file))(app)
})

7.router

该loader解析app/router目录下的各个路由配置文件,其核心代码如下:

module.exports = (app) => {
    // 找到路由文件路径
    const businessPath = path.resolve(process.cwd(), `./app`)
    const routerPath = path.resolve(app.bussinessPath,`./router`)

    // 实例化koaRouter
    const router = new koaRouter()

    // 注册所有路由
    const fileList = glob.sync(path.resolve(routerPath,`./**/**.js`))

    fileList.forEach(file => {
       require(path.resolve(file))(app,router)
    })

    // 设置兜底路由 (健壮性考虑)
    router.get('*', async (ctx,next) => {
        ctx.status = 302; //临时重定向
        ctx.redirect(`${app?.options?.homePage ?? '/'}`)
        await next()
    })

    // 注册路由到app
    app.use(router.routes())
    app.use(router.allowedMethods())
}

总结

以上是初步对elpis-core的理解,有些地方存在不理解的地方还在思索。其中典型的是各个loader的加载顺序,这个问题我觉得没有绝对的标准,配置如何都会影响到实现代码的撰写;重点是两者如何配合发挥各自的效能,达到 1+1>2 的效果。

框架学习来自抖音 “哲玄前端”,《大前端全栈实践课》,有兴趣的同学可以了解下,觉得哲哥讲的很细,而且以框架的角度将后端、前端、数据库、运维部署等串联起来,知识框架非常有体系。觉得很大程度上提高了自身的技术视野