前言
本文是个人学习笔记,用来记录学习Elpis项目时一些理解和收获。该项目基于MVC模型,通过加载一系列的loader来解析项目中的controller、service、view、、router、extend、config,一次解析,全局调用,给项目开发带来了极大的便利。
应用主要目录和各loader的功能
文件目录
elpis
├─ app
│ ├─ controller
│ ├─ extend
│ ├─ middleware
│ ├─ middleware.js // 全局中间件注入文件
│ ├─ public
│ ├─ router
│ ├─ router-schema
│ └─ service
├─ config
├─ elpis-core
│ ├─ env.js
│ ├─ index.js
│ └─ loader
│ ├─ config.js
│ ├─ controller.js
│ ├─ extend.js
│ ├─ middleware.js
│ ├─ router-schema.js
│ ├─ router.js
│ └─ service.js
├─ index.js
├─ logs
└─ package.json
configLoader
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.log(`[exception] there is no default.config file`);
}
// 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.production.js`))
}
} catch (error) {
console.log(`[exception] there is no env.config file`);
}
// 覆盖并加载 config 配置
app.config = Object.assign({}, defaultConfig, envConfig);
config文件主要是把项目不同环境的变量进行区分,通过运行命令中的环境参数,加载不同配置文件。
controllerLoader
// 读取 app/controller/**/**.js 下所有的文件
const controllerPath = path.resolve(app.businessPath, `.${sep}controller`);
const fileList = glob.sync(path.resolve(controllerPath, `.${sep}**${sep}**.js`));
// 遍历所有的文件目录,把内容加载到 app.controllers 下
const controllers = {};
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());
// 挂载 controller 到内存 app 对象中
let tempController = controllers;
const names = name.split(sep);
const len = names.length;
for (let i = 0; i < len; i++) {
if (i === len - 1) {
const ControllerModule = require(path.resolve(file))(app);
tempController[names[i]] = new ControllerModule();
} else {
if (!tempController[names[i]]) {
tempController[names[i]] = {}
}
tempController = tempController[names[i]];
}
}
});
app.controller = controllers;
controller.js这个loader加载所有的controller文件,并挂载到app.controller上,我们在项目中可以通过app.comfig.fileName 的形式进行调用。通过const ControllerModule = require(path.resolve(file))(app); tempController[names[i]] = new ControllerModule();这两行代码,使得controller的实例挂载到app.controller上。
extendLoader
extend loader将扩展文件挂载到app实例上,例如日志文件,实现代码如下
// 读取 app/extend/**.js 下所有的文件
const extendPath = path.resolve(app.businessPath, `.${sep}extend`);
const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`));
// 遍历所有的文件目录,把内容加载到 app.extends 下
const extend = {};
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('.'));
// 把 ‘-’ 改为驼峰式 app/extend/custom-extend.js => customExtend
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 过滤app已经存在的key
for (const key in app) {
if (key === name) {
console.log(`[exception load error] name: ${name} is already in app`);
return;
}
}
app[name] = require(path.resolve(file))(app);
});
app.extend = extend;
middlewareLoader
中间件是Koa应用的基础组件,负责处理请求和响应的流程。中间件加载器middlerwareLoader就将项目中的中间件挂载到app.middlewares上。
// 读取 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);
// 截取文件路径
name = name.substring(name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length, name.lastIndexOf('.'));
// 把 ‘-’ 改为驼峰式
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 挂载 middleware 到内存 app 对象中
let tempMiddleware = middlewares;
const names = name.split(sep);
const len = names.length;
for (let i = 0; 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;
routerSchemaLoader
处理所有api路由的json-schema校验文件,关于json-schema的使用方式,参考官网
// 读取 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
routerLoader
路由加载器负责对项目路由的加载,同时也对路由进行错误兜底。
// 找到文件路径
const routerPath = path.resolve(app.businessPath, `.${sep}router`);
// 实例化 KoaRouter
const router = new KoaRouter();
// 注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`));
fileList.forEach(file => {
// router.js 的实现方式
// module.exports = (app, router)=> {
// router.get('xxxxx/xxx/xx', xxx)
//}
require(path.resolve(file))(app, router);
})
// 路由兜底,没有匹配到路由
router.get('*', async (ctx, next) => {
// 重定向
ctx.status = 302;
ctx.redirect(`${app?.options?.homePage ?? '/'}`);
})
// 路由注册到 app 上
app.use(router.routes())
app.use(router.allowedMethods())
serviceLoader
service文件是对api请求的数据获取文件,serviceLoader实现方式类似于controllerLoader。
// 读取 app/service/**/**.js 下所有的文件
const servicePath = path.resolve(app.businessPath, `.${sep}service`);
const fileList = glob.sync(path.resolve(servicePath, `.${sep}**${sep}**.js`));
// 遍历所有的文件目录,把内容加载到 app.services 下
const services = {};
fileList.forEach(file => {
// 提取文件名
let name = path.resolve(file);
// 截取文件路径
name = name.substring(name.lastIndexOf(`service${sep}`) + `service${sep}`.length, name.lastIndexOf('.'));
// 把 ‘-’ 改为驼峰式
name = name.replace(/[_-][a-z]/ig, (s) => s.substring(1).toUpperCase());
// 挂载 service 到内存 app 对象中
let tempService = services;
const names = name.split(sep);
const len = names.length;
for (let i = 0; i < len; i++) {
if (i === len - 1) {
const ServiceModule = require(path.resolve(file))(app);
tempService[names[i]] = new ServiceModule();
} else {
if (!tempService[names[i]]) {
tempService[names[i]] = {}
}
tempService = tempService[names[i]];
}
}
});
app.service = services;
总结
以上的loader文件的作用主要用于统一处理各种文件,不需要每开发一个文件就配置一个文件。比如,没有controllerLoader,我们开发完project-loader后会执行一下app.controller.project = require('./loader/prokect.js')(app);。同样其它类型的文件也都要进行手动挂载,有了loader后就不需要进行手动执行。
注:抖音 “哲玄前端”,《大前端全栈实践课》