前言
学习声明:本文知识体系来源于
哲玄前端(抖音ID:44622831736)大前端全栈实践课程,结合个人学习实践进行整理。本文档主要记录全栈开发框架elpis-core内核引擎的实现。
核心设计理念
框架通过loader方法体系实现模块自动装配,其核心遵循Convention Over Configuration(约定优于配置)设计范式。通过一系列的loader来加载约定目录下文件,然后引入elpis-core,调用start方法就能启动项目。elpis-core通过预定义模块加载规则与目录结构,开发者仅需按规范编写业务代码,即可免除传统框架繁杂的配置流程,显著降低学习曲线与心智负担。
核心内容
内核引擎设计
声明:设计图来源
哲玄前端(抖音ID:44622831736)大前端全栈实践课程
elpis-core 项目结构
elpis
├─ 📁app
│ ├─ 📁controller // 存放业务处理文件,进行业务逻辑的处理
│ ├─ 📁extend // 存放拓展文件,比如:日志文件...
│ ├─ 📁middleware // 中间件逻辑处理,挂载到koa实例进行一系列的处理
│ ├─ 📁public
│ │ ├─ 📁output
│ │ └─ 📁static
│ ├─ 📁router // 路由文件
│ ├─ 📁router-schema // 对 router 规则校验的文件
│ ├─ 📁service // 服务层的文件,主要用于服务端的交互
│ └─ 📄middleware.js // 全局的中间件
├─ 📁config // 环境配置
├─ 📁elpis-core
│ ├─ 📁loader
│ │ ├─ 📄config.js
│ │ ├─ 📄controller.js
│ │ ├─ 📄extend.js
│ │ ├─ 📄middleware.js
│ │ ├─ 📄router-schema.js
│ │ ├─ 📄router.js
│ │ └─ 📄service.js
│ ├─ 📄env.js
│ ├─ 📄index.js
├─ 📄index.js
调用start方法 启动 elpis-core
// 文件路径:elpis/index.js
// 引入 elpis-core
const ElpisCore = require('./elpis-core');
/**
* 启动项目
* @param {object} options 应用配置
*/
ElpisCore.start(options);
// 文件路径:elpis/app/elpis-core/index.js
/**
* 声明 start 启动项目方法
* @param {*} 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()}`);
// 加载 config
configLoader(app);
console.log("-- [start] load config done --");
// console.log(app.config);
// 加载extend
extendLoader(app);
console.log("-- [start] load extend done --");
// console.log(app.routerSchema);
// 加载 routerSchema
routerSchemaLoader(app);
console.log("-- [start] load routerSchema done --");
// 加载 middleware
middlewareLoader(app);
console.log("-- [start] load middleware done --");
// console.log(app.middlewares);
// 注册全局中间件 用户定义的中间件放在 app.middleware.js
try {
require(path.resolve(app.businessPath, `.${sep}middleware.js`))(app);
// console.log('-- [start] load global middleware done --');
} catch (e) {
console.log(
`[exception] Connot find global middleware, The middleware path is app.middleware.js`
);
}
// 加载 service
serviceLoader(app);
console.log("-- [start]load service done --");
// console.log(app.service);
// 加载 controller
controllerLoader(app);
console.log("-- [start] load controlller done --");
// console.log(app.controller);
// 注册 router
routerLoader(app);
// console.log('load router done');
// console.dir(app);
// 启动服务
try {
// 从环境中获取端口,否则默认8080
const port = process.env.PORT || 8080;
// 从环境中获取ip 域名,否则默认 0.0.0.0
const host = process.env.IP || "0.0.0.0";
// 启动服务
app.listen(port, host);
console.log(`Server running on port: ${port}`);
} catch (error) {
console.error(error);
}
}
elpis-core 引擎内核实现
核心加载器模块,包含各模块的自动加载逻辑的实现。包括以下模块
middleware
加载所有的中间件文件 通过Koa洋葱圈模型对复杂业务逻辑的天然适配性,进行一系列功能的处理(如统一错误处理、鉴权拦截、参数检验...)
tips: 洋葱圈模型设计如上设计图
下面是具体代码实现:
const path = require("path");
const { sep } = path;
const glob = require("glob");
/**
* middleware loader
* @params {object} app koa实例
* 加载所有 middleware , 使其可通过 "app.middlewares.${目录}.${文件}" 访问
* 例子:
* app/middleware
* | -- custom-floder
* | -- custom-middleware.js
*
* => 访问:app.middlewares.customFloder.customMiddleware
*/
module.exports = (app) => {
// 1、获取 app/middleware 目录 middleware/**/**.js
// 获取 middleware 文件路径
const middlewarePath = path.resolve(app.businessPath, `.${sep}/middleware`);
// 获取 middleware 下面的所有文件
// const fileList = ["app/middleware/custom-floder/custom-middleware.js", "app/middleware/custom-floder/custom-middleware1.js"]
const fileList = glob.sync(path.resolve(middlewarePath, `.${sep}**${sep}**.js`));
// 2、遍历所有文件目录,把内容加载到 app/middlewares 目录下
const middlewares = {};
// 遍历文件列表,将文件处理成可以 打点 调用的格式
fileList.forEach(file => {
// 提取文件名称,具体的文件的路径
let name = path.resolve(file);
// 截取文件名称
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));
// 将文件名的 "-" 修改为驼峰格式 custom-middleware --> customMiddleware
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 处理格式
let tempMiddleware = middlewares;
const names = name.split(sep);
for (let i = 0, len = names.length; i < len; i++) {
if (i === len - 1) { // 如果是最后一个,则表示是文件
// 如果是文件,则引入middlewareLoader处理文件
tempMiddleware[names[i]] = require(path.resolve(file))(app);
} else { // 如果不是最后一个,表示是文件夹
// 如果对应文件夹的 names[i] 为空
if (!tempMiddleware[names[i]]) {
tempMiddleware[names[i]] = {};
};
// 赋值临时路径
tempMiddleware = tempMiddleware[names[i]];
};
};
});
// 3、将 middlewares 挂载到内存 app 上
app.middlewares = middlewares;
};
controller
加载处理业务层的文件,使其可通过
"app.controller.${目录}.${文件}"访问。
下面是代码实现:
...
// 省略引入包的代码(与middleware一样)
...
// 省略获取fileList代码(与middleware一样)
const controller = {};
// 遍历文件列表,将文件处理成可以 打点 调用的格式
fileList.forEach(file => {
...
// 省略处理 name 的代码(与middleware一样)
for (let i = 0, len = names.length; i < len; i++) {
if (i === len - 1) { // 如果是最后一个,则表示是文件
// 如果是文件,则引入controllerLoader处理文件
// controller文件是 class, 所以要实例化
const ControllerModule = require(path.resolve(file))(app);
tempController[names[i]] = new ControllerModule();
} else { // 如果不是最后一个,表示是文件夹
// 如果对应文件夹的 names[i] 为空
if (!tempController[names[i]]) {
tempController[names[i]] = {};
};
// 赋值临时路径
tempController = tempController[names[i]];
};
};
});
// 3、将 controller 挂载到内存 app 上
app.controller = controller;
service
加载服务层的文件,主要用于封装核心业务逻辑,与
controller层解耦,也用于数据处理,第三方服务调用等等。实现与controller类似,只需把controller代码的controller部分改为service即可
extend
主要加载拓展层的代码,例如:
日志、其他服务...,为Koa添加额外的功能,该loader与其他loader不同的是,该loader直接挂载到Koa实例上,通过app(koa 实例).xxx调用,而其他loader(例:controller)挂载到app.controller下通过app.controller.xxxx调用
关键代码实现如下:
...
// 其他代码跟上面的一样
for (const key in app) {
if (key === name) {
console.log(`[extend load error] name: ${name} is already in app`);
return;
};
}
// 3、将 extend 挂载到内存 app 上
app[name] = require(path.resolve(file))(app);
router-schema
该loader主要通过
json-schema配合ajv对API 进行检验的(headers、query、params、body等等)
具体代码实现:
const path = require("path");
const { sep } = path;
const glob = require("glob");
/**
* router-scheme loader
* @param {object} app koa实例
*
* 通过 "json-schema & ajv" 对 API 规则进行约束,配合 api-params-verify 中间件使用
* ajv: 检验 params 与 json-schema 的描述是否合法
*
* 文件: app/router-schema/**.js
*
* 输出:
* app.rpuSchema = {
* `${api1}`: `${jsonSchema}`
* `${api2}`: `${jsonSchema}`
* `${api3}`: `${jsonSchema}`
* }
*
*/
module.exports = (app) => {
// 1、获取 app/router-schema 目录 router-schema/**.js
// 获取 router-schema 文件路径
const routerSchemaPath = path.resolve(app.businessPath, `.${sep}/router-schema`);
// 获取 router-schema 下面的所有文件
// const fileList = ["app/router-schema/router-shcema.js", "app/router-schema/custom-shcema1.js"]
const fileList = glob.sync(path.resolve(routerSchemaPath, `.${sep}**.js`));
// 2、注册所有routerSchema, 使得可以通过 "app.routerSchema" 访问
let routerSchema = {};
fileList.forEach(file => {
routerSchema = {
...routerSchema,
...require(path.resolve(file))
}
});
app.routerSchema = routerSchema;
}
json-schema配合ajv的具体应用
json-schema官网: json-schema.org/
ajv官网: ajv.js.org/
// 数据结构:
{
"productId": 1,
"productName": "A green door",
"price": 12.50,
"tags": [ "home", "green" ]
}
// json-schema描述上面的数据结构
{
// schema 版本
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"title": "Product",
"description": "A product from Acme's catalog",
// 数据结构类型
"type": "object",
// 属性
"properties": {
// 属性的类型
"productId": {
"description": "The unique identifier for a product",
"type": "integer"
},
"productName": {
"description": "Name of the product",
"type": "string"
},
"price": {
"description": "The price of the product",
"type": "number",
"exclusiveMinimum": 0
}
},
// 哪些属性是必须的
"required": [ "productId", "productName", "price" ]
}
// app/router/project.js 下定义了一个 /api/project/list 接口
module.exports = (app, router) => {
const { project: projectController } = app.controller;
// 获取项目列表
router.get('/api/project/list', projectController.getList.bind(projectController));
}
// app/router-schema/project.js 文件描述这个接口的query参数
/**
* 检验 router API 是否合法
*
*/
module.exports = {
"/api/project/list": {
get: {
query: {
type: 'object',
properties: {
proj_key: {
type: 'string'
}
},
required: ["proj_key"]
}
}
}
}
// 在 app/middleware 定义一个api-params-verify.js中间件-->app/middleware/api-params-verify.js
const Ajv = require("ajv");
const ajv = new Ajv();
/**
* API 参数检验
*/
module.exports = (app) => {
// 添加 $schema 版本,声明 ajv 使用json-schema哪个版本来检验,这个字段不是必须的,但最好在实际检验中指定它。
const $schema = "http://json-schema.org/draft-07/schema#";
return async (ctx, next) => {
// 只处理 API 请求做签名校验
if (ctx.path.indexOf("/api") < 0) {
return await next();
}
// 获取请求参数
const { query } = ctx.request;
const schema = app.routerSchema[path]?.[method.toLowerCase()];
if (!schema) {
return await next();
}
let valid = true;
// ajv 检验器
let validate;
// 检验query
if (valid && query && schema.query) {
schema.query.$schema = $schema;
validate = ajv.compile(schema.query);
valid = validate(query);
}
if (!valid) {
ctx.status = 200;
ctx.body = {
success: false,
message: `request validate fail: ${ajv.errorsText(validate.errors)}`,
code: 442,
};
return;
}
await next();
};
};
config
该 loader 主要放置默认的环境配置与不同环境的配置,可根据运行的环境(
process.env_ENV)来获取对应的环境配置config.default.js(默认)、config.local.js(本地,不提交至远程仓库)、config.beta.js(开发)、config.prod.js(生产)实现如下:
const path = require("path");
const { sep } = path;
/**
* confid loader
* @param {object} app koa实例
*
* 环境配置 本地、测试、生产,通过 env 读取环境配置 env.config
* 通过 env.config 覆盖 default.config 加载到 app.config 中
*
* 目录下对应的 config 配置
* 默认配置 config/config.default.js
* 本地配置 config/config.local.js
* 测试配置 config/config.beta.js
* 生产配置 config/config.prod.js
*/
module.exports = (app) => {
// 1、获取 config 目录
const configPath = path.resolve(app.baseDir, `.${sep}config`);
// 2、获取默认文件(config.default.js)
let defaultConfig = {};
try {
defaultConfig = require(path.resolve(configPath, `.${sep}config.default.js`));
} catch (e) {
console.error("[exception] Connot find the config.default.js file");
};
// 3、获取环境配置(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`));
}
// 简化
// envConfig = require(path.resolve(configPath, `.${sep}config.${app.env.get()}.js`));
} catch (e) {
// console.error(`[exception] Connot find the config.${app.env.get()}.js file`);
console.error(`[exception] Connot find the config env file`);
};
// 4、覆盖默认目录
app.config = Object.assign({}, defaultConfig, envConfig);
}
router
该 loader 加载router下面的路由,
将router下的路由统一收拢,然后注册到Koa的路由下,同时做路由兜底,临时重定向至用户设置的homePage页面。
关键代码实现如下:
const KoaRouter = require("koa-router");
const path = require("path");
const { sep } = path;
const glob = require("glob");
/**
* router loader 目标: 将 router 目录下的路由注册到koa的路由下
* @param {object} app koa实例
*
* 解析 app/router/ 下的所有文件,加载到KoaRouter 下
*
*/
module.exports = (app) => {
// 1、找到路由文件路径
const routerPath = path.resolve(app.businessPath, `.${sep}router`);
// 2、实例化 KoaRouter 实例
const router = new KoaRouter();
// 3、注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`));
fileList.forEach(file => {
// 路由调用示例: module.exports = (app, router) => { router.get('xxx/xxxxx/xxx/', params) };
// 注册到 KoaRouter 路由上
require(path.resolve(file))(app, router);
});
// 4、路由兜底,考虑健壮性
router.get("*", async (ctx, next) => {
// 临时重定向
ctx.status = 302;
ctx.redirect(app?.options?.homePage ?? "/");
})
// 5、路由注册到 app 上
app.use(router.routes());
app.use(router.allowedMethods());
}
loader 执行顺序遵循的原则
注:注意loader加载顺序
在调用start方法 启动 elpis-core 目录处有加载loader顺序,总体遵循无依赖先加载,有依赖被依赖者先加载,依赖者后加载原则,routerLoader在最后加载,因为它依赖其他loader处理完之后才通过routerLoader 去处理一系列的功能。
总结
elpis-core通过创新的约定式架构,在保持灵活性的同时大幅降低配置复杂度。开发者只需专注业务逻辑实现,框架自动完成技术基础设施的装配工作,是追求高效开发的理想选择。
学习声明:本文知识体系来源于
哲玄前端(抖音ID:44622831736)大前端全栈实践课程,结合个人学习实践进行整理,如有偏差,还望各位大佬指正。