elpis-core
注:抖音 “哲玄前端”,《大前端全栈实践课》
该项目是一个基于koa二次封装的一个应用程序,通过实现一系列loader来加载对应目录下的文件。该应用程序由 middlewareLoader、routerSchemeLoader、controllerLoader、serviceLoader、configLoader、extendLoader 以及routerLoader 组成,同时拥有注册 globalMiddleware 的能力。项目结构如下
middlewareLoader
- middlewareloader会读取app/middleware下子目录以及下面所有中间件文件,并挂载到应用上。中间件可以做很多事情,例如
异常处理、api合法性校验、api参数校验等 - elpis-core是基于koa实现的,所以elpis-core的中间件形式和koa的中间件形式是一样的,都是基于洋葱模型
module.exports = (app) => {
//读取 app/middleware/**/*.js 下的所有文件
const middlewarePath = path.resolve(app.bussinessPath, `.${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-mudule/custom-middleware.js => custom-mudule/custom-middleware
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));
//将 '-' 命名替换成驼峰命名 custom-mudule/custom-middleware=> customMudule/customMiddleware
name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
//挂载middlewares到内存对象app中
let tempMiddleware = middlewares;
const names = name.split(sep);
names.forEach((item, index) => {
if (index === names.length - 1) {
//文件
tempMiddleware[item] = require(path.resolve(file))(app);
} else {
// 文件夹 递归 tempMiddleware = {a:{b:{c:{d:{}}}
tempMiddleware[item] = tempMiddleware[item] ?? {};
//指向下一层
tempMiddleware = tempMiddleware[item];
}
});
});
app.middlewares = middlewares;
};
routerSchemeLoader
- routerSchemeLoader读取app/router-schema下的文件,app/router-schema下的文件描述信息遵循json-scheme规范,配合ajv对api请求信息进行校验
module.exports = (app) => {
//获取app/router-schema下的所有文件
const routerSchemaPath = path.resolve(app.bussinessPath, `.${sep}router-schema`);
const fileList = glob.sync(path.resolve(routerSchemaPath, `.${sep}**${sep}*.js`));
// 注册所有routeSchema ,可以通过app.routerSchema访问
let routerSchema = {};
fileList.forEach((file) => {
routerSchema = {
...routerSchema,
...require(path.resolve(file)),
};
});
app.routerSchema = routerSchema;
};
- scheme描述信息 app/router-schema/xxx.js
module.exports = {
'/api/project/list': {
get: {
query: {
type: 'object',
properties: {
name: {
type: 'string',
},
},
required: ['name'],
},
},
},
};
controllerLoader
- controller负责调用对应的service处理业务,并在需要时对service返回的结果进行处理,将响应结果返回给客户端
- controllerLoader会读取app/controller下以及其子目录下的所有文件,并将controller挂载到应用上,通过
app.controller.${目录}.${文件}访问
module.exports = (app) => {
//读取 app/controller/**/*.js 下的所有文件
const controllerPath = path.resolve(app.bussinessPath, `.${sep}controller`);
const fileList = glob.sync(path.resolve(controllerPath, `.${sep}**${sep}*.js`));
//遍历所有文件,将内容加载到app/controller下
const controller = {};
fileList.forEach((file) => {
//读取文件路径
let name = path.resolve(file);
//截取路径,app/controller/custom-mudule/custom-controller.js => custom-mudule/custom-controller
name = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length, name.lastIndexOf('.'));
//将 '-' 命名替换成驼峰命名 custom-mudule/custom-controller=> customMudule/customController
name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
//挂载controller到内存对象app中
let tempController = controller;
const names = name.split(sep);
names.forEach((item, index) => {
if (index === names.length - 1) {
//文件 controller返回的是class类
const ControllerModule = require(path.resolve(file))(app);
tempController[item] = new ControllerModule();
} else {
// 文件夹 递归 tempController = {a:{b:{c:{d:{}}}
tempController[item] = tempController[item] ?? {};
//指向下一层
tempController = tempController[item];
}
});
});
app.controller = controller;
};
serviceLoader
- service是负责处理具体的业务,可以使controller层的代码更加简洁,一个service可以被多个controller调用
- serviceLoader会读取app/service下以及其子目录下的所有文件,并将service挂载到应用上,通过
app.service.${目录}.${文件}访问,实现参考controllerLoader
configLoader
- configLoader可以根据不同的环境变量将不同的config配置挂载到应用上
module.exports = (app) => {
//获取config目录,config在根目录下
const configPath = path.resolve(app.baseDir, `.${sep}config`);
//读取config.default
let defaultConfig = {};
try {
defaultConfig = require(path.resolve(configPath, `.${sep}config.default.js`));
} catch (error) {
console.log('[exception] config.default.js not found');
}
//获取config.env
let envConfig = {};
try {
envConfig = require(path.resolve(configPath, `.${sep}config.${app.env.get()}.js`));
} catch (error) {
console.log(`[exception] config.${app.env.get()}.js not found`);
}
//覆盖config.default,并加载到app.config
app.config = Object.assign({}, defaultConfig, envConfig);
};
extendLoader
- 用于拓展应用的额外能力,例如日志
- extendLoader读取 app/extend下的所有文件,并挂载到应用上
module.exports = (app) => {
//读取 app/extend下的所有文件
const extendPath = path.resolve(app.bussinessPath, `.${sep}extend`);
const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}*.js`));
//遍历所有文件,将内容加载到app下
fileList.forEach((file) => {
//读取文件路径
let name = path.resolve(file);
//截取路径,app/extend/custom-extend.js => custom-extend
name = name.substring(name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length, name.lastIndexOf('.'));
//将 '-' 命名替换成驼峰命名 custom-extend=> customExtend
name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
//app实例下本身存在一些key 处理重名
if (app.hasOwnProperty(name)) {
console.log(`[exception] extend load error name:${name} already exist`);
return;
}
//挂载 extend 到 app上
app[name] = require(path.resolve(file))(app);
});
};
routerLoader
- router负责将请求分发到对应的controller
- routerLoader读取 app/router下的所有文件,并注册到应用上
module.exports = (app) => {
//找到路由文件
const routerPath = path.resolve(app.bussinessPath, `.${sep}router`);
//实例化koarouter
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; //重定向
ctx.redirect(`${app?.options?.homePath ?? '/'}`);
});
//路由注册到app上
app.use(router.routes());
app.use(router.allowedMethods());
};
globalMiddleware
- 注册全局中间件
module.exports = (app) => {
//设置静态资源根目录
app.use(require('koa-static')(path.resolve(app.baseDir, `.${sep}app${sep}public`)));
//模板引擎渲染
const koaNunjucks = require('koa-nunjucks-2');
app.use(
koaNunjucks({
ext: 'tpl',
path: path.resolve(app.baseDir, `.${sep}app${sep}public`),
nunjucksConfig: {
noCache: true,
trimBlocks: true,
},
}),
);
//引入ctx.body解析中间件
const bodyParser = require('koa-bodyparser');
app.use(
bodyParser({
formList: '1000mb',
enableTypes: ['json', 'form', 'text'],
}),
);
//引入自定义的异常处理中间件,包裹在最外层
app.use(app.middlewares.errorHandle);
//引入api合法性校验中间件
app.use(app.middlewares.apiSign);
//引入api参数校验中间件
app.use(app.middlewares.apiParamsVerify);
};
总结
elpis-core是一个框架,通过上述的学习对node.js有一个初步的了解和认知,是基于现有技术的一种探索和尝试,希望大家一起指出问题,一起学习