ELPIS-CORE内核
前言
本文为阶段性学习笔记,主要记录全栈框架学习过程中的内核部分—— ELPIS-CORE。
elpis-core为框架解析器,将项目文件读取出来编译后形成可运行的业务代码,基于“约定大于配置“的原则,提升前端项目开发效率。其代码存放在elpis-core目录下,而要转化的项目文件则存放在app目录下,目录结构与内核保持一致。可以这样理解,elpis是自动炒菜机,而app目录下的项目文件则是食材、水等原材料,根据不同的菜谱(项目实际需求)将不同的原材料到炒菜机中就会做出不同的饭菜(项目成品)。主要以koa为依托进行开发
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 的效果。
框架学习来自抖音 “哲玄前端”,《大前端全栈实践课》,有兴趣的同学可以了解下,觉得哲哥讲的很细,而且以框架的角度将后端、前端、数据库、运维部署等串联起来,知识框架非常有体系。觉得很大程度上提高了自身的技术视野