前言
本文为个人学习笔记,跟随前端业界著名人士(抖音:哲玄前端)学习全栈,本章记录搭建 elpis-core 内核实现和理解,该内核主要奉行约定优于配置的设计理念,通过编写的loader来加载约定目录下config、middleware、service、extend、routerSchema、router、controller,可以减少开发人员的学习成本,无需考虑目录结构,只需要把对应的功能写到对应的文件中,引入 elpis-core ,调用start方法就能启动项目,接下来说一下内核的实现和各个目录的功能,以及如何搭建自己的私服的 npm 服务
应用中各目录功能和elpis-core中load实现
- 应用目录结构图
Elpis
├─ .DS_Store
├─ app
│ ├─ controller
│ ├─ extend
│ ├─ middleware
│ ├─ middleware.js //全局中间件
│ ├─ public
│ ├─ router
│ ├─ router-schema
│ └─ service
├─ config
├─ elpis-core
config
该目录下放置不同环境的配置文件和默认配置文件 如: config.default.js(默认) 、config.prod.js(生产) 、config.local.js(开发) elpis-core可以根据你运行的环境去加载对应的配置文件,且合并默认配置文件。
- configLoader 核心代码:
const path = require('path');
const { sep } = path;
/**
* @description config loader
* @param {object} app koa 实例
*
* 配置区分 本地/测试/生产, 通过 env 环境读取不同文件配置 env.config
* 通过 env.config 覆盖 default.config 加载 app.config 中
*
* 目录下对应的 config 配置
* 默认配置 config/default.js
* 本地配置 config/local.js
* 测试配置 config/beta.js
* 生产配置 config/prod.js
*/
module.exports = (app) => {
// 找到 config 目录
const configPath = path.resolve(app.baseDir, `.${sep}config`);
// 获取 default.config
let defaultConfig = {};
try {
defaultConfig = require(path.resolve(configPath, `.${sep}default.js`));
} catch (error) {
console.log('[-- elips-core/loader/config --][-- 第26行 --] there is no default.config file');
}
// 获取 env.config
let envConfig = {};
try {
envConfig = require(path.resolve(configPath, `.${sep}${app.env.get()}.js`));
} catch (error) {
console.log('[-- elips-core/loader/config --][-- 第34行 --] there is no env.config file');
}
// 覆盖并加载 config 配置
app.config = Object.assign({}, defaultConfig, envConfig);
};
controller
该目录下放置控制器文件,控制器是处理请求并返回响应的模块,它们会调用服务来获取数据并将其返回给客户端,如接口,页面。
elpis-core中对controller的处理是获取所有controller目录下的控制器,以文件的名字为key挂载到 koa实例的controller 上,在应用中我们可以通过 app.controller.xxx 获取。
- controllerLoader 核心代码:
const glob = require('glob');
const path = require('path');
const { forEach } = require('lodash');
const { sep } = path;
/**
* @description controller loader 配置中间件
* @param {object} app koa 实例
*
* 加载所有的 controller, 可通过'app.controlle.${目录}.${文件}'访问
* 例子:
* app/controller
* |
* | -- custom-module
* | -- custom-controller.js
* ==> app.controller.customModule.customController
*/
module.exports = (app) => {
// 读取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 = {};
forEach(fileList, (file) => {
// 提取文件名称
let name = path.resolve(file);
// 截取路径 app/controller/custom-module/custom-controller => custom-module/custom-controller
name = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length, name.lastIndexOf('.'));
// 把'-' 统一改成驼峰式 custom-module/custom-controller.js =>customModule.customController
name = name.replace(/[/-][a-z]/gi, (s) => s.substring(1).toUpperCase());
// 挂载 controller 到内存 app 对象中
let tempController = controller;
const names = name.split(`${sep}`);
for (let i = 0; i < names.length; i++) {
if (i === names.length - 1) {
const ControllerMoule = require(path.resolve(file))(app);
tempController[names[i]] = new ControllerMoule();
} else {
if (!tempController[names[i]]) {
tempController[names[i]] = {};
}
tempController = tempController[names[i]];
}
}
});
app.controller = controller;
};
extend
该目录下放置扩展文件,扩展的功能是为 koa 实例扩展额外的功能,如:日志
elpis-core中对extend的处理也是跟 controller 一样收集应用 extend 目录下的所有扩展,不同的是,extend是直接挂载到 koa实例 上, 应用中使用 app.xxx 获取
- extendLoader 核心代码:
const glob = require('glob');
const path = require('path');
const { forEach, forIn } = require('lodash');
const { sep } = path;
/**
* @description extend loader 配置中间件
* @param {object} app koa 实例
*
* 加载所有的 extend, 可通过'app.controlle.${目录}.${文件}'访问
* 例子:
* app/extend
* |
* | -- custom-extend
* ==> app.extend.customExtend
*/
module.exports = (app) => {
// 读取app/extend/**.js 下所有文件
const extendPath = path.resolve(app.businessPath, `.${sep}extend`);
const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`));
forEach(fileList, (file) => {
// 提取文件名称
let name = path.resolve(file);
// 截取路径 app/extend/custom-extend => custom-extend
name = name.substring(name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length, name.lastIndexOf('.'));
// 把'-' 统一改成驼峰式 custom-module/custom-extend.js =>customModule.customExtend
name = name.replace(/[/-][a-z]/gi, (s) => s.substring(1).toUpperCase());
// 过滤 app 已经存在的 key
forIn(app, (_value, key) => {
if (key === name) {
console.log(`[-- elips-core/loader/extend --][-- 第34行 --] [extend load error] name:${name} is already in app`);
return;
}
});
// 挂载 extend 到 app 上
app[name] = require(path.resolve(file))(app);
});
};
middleware
该目录放置中间件文件,中间件因其洋葱圈模型的特性,可以作用于很多功能,如:请求预处理作用(参数验证、身份认证和权限验证) 、异常错误处理,兜底所有异常等
- middlewareLoader 核心代码:
const glob = require('glob');
const path = require('path');
const { forEach } = require('lodash');
const { sep } = path;
/**
* @description middleware loader 配置中间件
* @param {object} app koa 实例
*
* 加载所有的 middleware, 可通过'app.middleware.${目录}.${文件}'访问
* 例子:
* app/middleware
* |
* | -- custom-module
* | -- custom-middleware.js
* ==> app.middlewares.customModule.customMiddleware
*/
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 = {};
forEach(fileList, (file) => {
// 提取文件名称
let name = path.resolve(file);
// 截取路径 app/middlewares/custom-module/custom-middleware => custom-module/custom-middleware
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));
// 把'-' 统一改成驼峰式 custom-module/custom-middleware.js =>customModule.customMiddleware
name = name.replace(/[/-][a-z]/gi, (s) => s.substring(1).toUpperCase());
// 挂载 middleware 到内存 app 对象中
let tempMiddleware = middlewares;
const names = name.split(`${sep}`);
for (let i = 0; i < names.length; i++) {
if (i === names.length - 1) {
tempMiddleware[names[i]] = require(path.resolve(file))(app);
} else {
if (!tempMiddleware[names[i]]) {
tempMiddleware[names[i]] = {};
}
tempMiddleware = tempMiddleware[names[i]];
}
}
});
app.middlewares = middlewares;
};
service
该目录放置服务文件,服务的功能是封装复杂业务场景下的业务逻辑,从而保持 controller 中的逻辑简洁,且一个 service 可提供给多个 controller 调用,也应分离逻辑和展示,从而便于编写测试用例。如:数据处理,第三方服务调用等
elpis-core中对service的处理跟controller相同,应用中使用 app.service.xxx 获取。
serviceLoader 核心代码:参考controller的实现,把 controller 换成 service
router-schema
该目录放置json-schema文件,该文件的作用主要是对接口数据的描述配合ajv对接口头部、参数等验证。如下方代码就是对接口*/api/project/list*的描述信息。如何写请参考json-schema网网
/**
* json-schema 描述
*/
module.exports = {
'/api/project/list': {
get: {
query: {
type: 'object',
properties: {
proj_key: {
type: 'string'
}
},
required: ['proj_key']
}
}
}
};
elpis-core中对router-schema处理跟前几个有所不同,具体看下面代码实现。应用中使用的话是相同的,通过 app.routerSchema.xxx 获取。
- routerSchemaLoader 核心代码:
const glob = require('glob');
const path = require('path');
const { forEach } = require('lodash');
const { sep } = path;
/**
* @description 路由schema loader
* @param {object} app koa 实例
*
* 通过 'json-schema' & 'ajv' 对 API 规则进行约束,配置 api-params-verify 中间件的使用
* app/router-schema/**js
* 输出:
* app.routeSchema = {
* '${api1}': ${jsonSchema}
* '${api2}': ${jsonSchema}
* '${api3}': ${jsonSchema}
* '${api4}': ${jsonSchema}
* }
*/
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.roterSchema' 这样访问
let routerSchema = {};
forEach(fileList, (file) => {
routerSchema = {
...routerSchema,
...require(path.resolve(file))
};
});
app.routerSchema = routerSchema;
};
router
该目录放置路由配置文件,路由的功能定义页面和接口路由,映射对应的controller.
elpis-core中对router处理为收集所有路由注册和一个路由兜底.
- routerLoader 核心代码:
const KoaRouter = require('koa-router');
const glob = require('glob');
const path = require('path');
const { forEach } = require('lodash');
const { sep } = path;
/**
* @description router loader
* @param {object} app koa 实例
*
* 解析所有 app/router/ 下所有 js 文件, 加载到 KoaRouter 下
*/
module.exports = (app) => {
// 找到路由文件路径
const routerPath = path.resolve(app.businessPath, `.${sep}router`);
// 实例化 KoaRouter
const router = KoaRouter();
// 注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`));
forEach(fileList, (file) => 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());
};
各个load的加载顺序和存在的问题
这个加载顺序也是其中比较困惑的点,目前使用的顺序是为: config => extend => service => middleware => routerSchema => controller => router,遵循的原则就是被依赖方先加载,就是说后面的没有依赖的放最前面,有依赖前者的就放在前者后面。如:
- config: 是一个全局的配置,无需依赖任何文件,其他几个都需依赖该文件,放第一个
- extend:是扩展文件,只依赖配置文件,放第二个
- controller:依赖多个sevice,放sevice后,
- router:放最后
- 其他: 同理
但也有一定的缺点,如复杂的配置文件会影响后面文件的加载。
结尾
以后就是目前对 eplis-core 的理解和应用
注:抖音 “哲玄前端”,《大前端全栈实践课》