ElpisCore--基于koa的简单服务端内核引擎

72 阅读2分钟

前言

本文是个人学习笔记,用来记录学习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后就不需要进行手动执行。

注:抖音 “哲玄前端”,《大前端全栈实践课》