前言
elpis-core 是一个基于 koa 2.7.0 的企业级服务框架的 BFF 层部分,它的主要特点是利用了 koa 洋葱圈模型,旨在简化 Web 应用开发。本文主要来谈论自己对于 elpis-core 的理解。
架构和技术细节
约定大于配置
约定的目录结构
elpis/
├── app/
│ ├── controller/ //业务逻辑
│ ├── extend/ //拓展工具
│ ├── middleware/ //中间件
│ ├── public/ //静态根目录
│ ├── router-schema/ //路由校验规则
│ ├── router/ //路由
│ ├── service/ //service服务的提供
│ └── middlerware.js //全局的中间件引入
├── conifg/ //不同环境的配置文件
├── log/ //持久化的日志文件
└── index.js //入口文件
主要功能
elpis-core 主要的功能就是将约定的目录结构下的项目文件按顺序加载到运行的内存中。
核心组件
elpis-core 包含多个核心组件,共同构成了其完整的功能体系。
elpis-core/
├── loader/
│ ├── config.js
│ ├── controller.js
│ ├── extend.js
│ ├── middleware.js
│ ├── router-schema.js
│ ├── router.js
│ └── service.js
├── env.js
└── index.js
index.js
(完整代码看这里)
module.exports = {
/**
* 启动项目
* @params options 项目配置
* options = {
* name // 项目名称
* homePage // 项目首页
* }
*/
start(options = {}) {
// koa 实例
const app = new Koa();
// 应用配置
app.options = options;
// 基础路径
app.baseDir = process.cwd();
// 业务文件路径
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
// 初始化环境配置
app.env = env();
console.log(`-- [start] env: ${app.env.get()} --`);
// 加载 middleware
middlewareLoader(app);
console.log(`-- [start] load middleware done --`);
// 加载 routerSchema
routerSchemaLoader(app);
console.log(`-- [start] load routerSchema done --`);
// 加载 controller
controllerLoader(app);
console.log(`-- [start] load controller done --`);
// 加载 service
serviceLoader(app);
console.log(`-- [start] load service done --`);
// 加载 config
configLoader(app);
console.log(`-- [start] load config done --`);
// 加载 extend
extendLoader(app);
console.log(`-- [start] load extend done --`);
// 注册全局中间件
try {
require(`${app.businessPath}${sep}middleware.js`)(app);
console.log(`-- [start] load global middleware done --`);
} catch(e){
console.log('[exception] global middleware file not found');
}
// 注册路由
routerLoader(app);
console.log(`-- [start] load router done --`);
// 启动服务
try {
const port = process.env.PORT || 8080;
const host = process.env.IP || '0.0.0.0';
app.listen(port, host);
console.log(`Server running on port: ${port}`);
} catch(e){
console.log(e);
}
}
}
env.js
module.exports = (app) => {
return {
// 判断是否本地环境
isLocal() {
return process.env._ENV === 'local';
},
// 判断是否测试环境
isBeta() {
return process.env._ENV === 'beta';
},
// 判断是否生产环境
isProduction() {
return process.env._ENV === 'production';
},
// 获取当前环境
get() {
return process.env._ENV ?? 'local';
}
}
}
middlewareLoader
(完整代码看这里)
module.exports = (app) => {
// 读取 app/middleware/**/**.js 下所有的文件
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);
// 截取路径 app/middleware/custom-module/custom-middleware.js => custom-module/custom-middleware
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));
// 把 '-' 统一改成驼峰 custom-module/custom-middleware => customModule/customMiddleware
name = name.replace(/[_-][a-z]/ig, (s) =>s.substring(1).toUpperCase());
// 挂载 middlewares 到内存 app 对象上
let tempMiddleware = middlewares;
const names = name.split(sep);
for(let i = 0, len = names.length; i < len; i++){
const n = names[i];
if(i === len - 1){
tempMiddleware[n] = require(path.resolve(file))(app);
}else{
if(!tempMiddleware[n]){
tempMiddleware[n] = {};
}
tempMiddleware = tempMiddleware[n];
}
}
})
app.middlewares = middlewares;
}
routerSchemaLoader
(完整代码看这里)
module.exports = (app) => {
// 读取 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;
}
controllerLoader
逻辑和middlewareLoader一样(完整代码看这里)
serviceLoader
逻辑和middlewareLoader一样(完整代码看这里)
configLoader
(完整代码看这里)
module.exports = (app) => {
// 找到 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.error('[exception] default.config file not found');
}
// 获取 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.error('Error loading config:', e);
}
// 覆盖并加载到 app.config 上
app.config = Object.assign({}, defaultConfig, envConfig);
}
extendLoader
逻辑和middlewareLoader一样(完整代码看这里)
routerLoader
(完整代码看这里)
module.exports = (app) => {
// 找到路由文件路径
const routerPath = path.resolve(app.businessPath, `.${sep}router`);
// 实例化 KaoRouter
const router = new KoaRouter();
// 注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`));
fileList.forEach(file => {
require(path.resolve(file))(app, router);
})
// 路由兜底(健壮性)
router.get('*', async (ctx, next) => {
ctx.status = 302; // 302 临时重定向
ctx.redirect(`${app?.options?.homePage ?? '/'}`);
})
// 路由注册到 app 上
app.use(router.routes());
app.use(router.allowedMethods());
}
应用
app内的应用代码可以看这里
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\index.js",
"env": {
"_ENV": "beta"
}
}
]
}
通过VSCode的调试功能我们可以很清楚的看到都被挂载到app上了
app.middleware 中可以看到被挂载到app上的中间件以及加载顺序
koa-static serve
koa-nunjucks 的一个匿名函数
koa bodyParser
error-handler ()
api-sign-verify ()
api-params-verify ()
koa-router dispatch
koa-router allowedMethods