基于koa 之 elpis-core 实现

1,118 阅读3分钟

前言

    本文为个人学习笔记,跟着哲哥(抖音:哲玄前端)学全栈之 elpis-core 内核实现和理解使用,该内核主要奉行约定优于配置的设计理念,通过一系列的load来加载约定目录下 configmiddlewareserviceextendrouterSchemaroutercontroller,可以减少开发人员的学习成本,无需考虑目录结构,只需要把对应的功能写到对应的文件中,引入 elpis-core ,调用start方法就能启动项目,接下来说一下load的实现和应用中各个目录的功能。

应用中各目录功能和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 核心代码:
  // 找到 config/ 目录
  const configPath = path.resolve(process.cwd(), `./config`);
 
 // 获取 default.config
  let defaultConfig = {};
  try {
    defaultConfig = require(path.resolve(
      configPath,
      `./config.default.js`
    ));
  } catch (e) {
    console.log("[exception] there is no default.config file");
    console.log(e);
  }
 
  // 获取 env.config
  let envConfig = {};
  try {
    if (app.env.isLocal()) {
      // 本地环境
      envConfig = require(path.resolve(configPath, `./config.local.js`));
    }  else if (app.env.isProduction()) {
      // 生产环境
      envConfig = require(path.resolve(configPath, `./config.prod.js`));
    }
  } catch (e) {
    console.log("[exception] there is no default.config file");
    console.log(e);
  }

  // 覆盖并加载 config 配置
  app.config = Object.assign({}, defaultConfig, envConfig);
 

controller

该目录下放置控制器文件,控制器是处理请求并返回响应的模块,它们会调用服务来获取数据并将其返回给客户端,如接口,页面。
elpis-core中对controller的处理是获取所有controller目录下的控制器,以文件的名字为key挂载到 koa实例的controller 上,在应用中我们可以通过 app.controller.xxx 获取。
controllerLoader 核心代码:

//业务文件路径
const businessPath = path.resolve(process.cwd(), `./app`);
// 读取 app/controller/**/**.js 下所有的文件
const controllerPath = path.resolve(businessPath, `./controller`);

const fileList = glob.sync(
    path.resolve(controllerPath, `./**/**.js`)
);

// 遍历所有文件目录,把内容加载到 app.controller 下
const controller = {};
fileList.forEach((file) => {
    // 提取文件名称
    let name = path.resolve(file);
    // 截取路径 app/controller/custom-module/custom-controller.js => custom-module/custom-controller
    name = name.substring(
      name.lastIndexOf(`controller/`) + `controller/`.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, len = names.length; i < len; ++i) {
      if (i === len - 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 核心代码:

 ...
  省略类似的文件获取代码
 ...
 fileList.forEach((file) => {
     ...
      省略类似的name获取代码
     ...
    // 过滤 app 已经存在的 key
    for (const key in app) {
      if (key === name) {
        console.log(`[extend load error] name:${name} is already in app`);
        return;
      }
    }
    app[name] = require(path.resolve(file))(app);
  });

middleware

该目录放置中间件文件,中间件因其洋葱圈模型的特性,可以作用于很多功能,如:请求预处理作用(参数验证、身份认证和权限验证)异常错误处理,兜底所有异常

  • 洋葱圈模型:
image.png elpis-core中对*middleware*的处理跟*controller*,挂载到 *koa实例的middlewares* 上, 应用中使用 *app.middlewares.xxx*获取
middlewareLoader 核心代码:
 ...
  省略类似的文件获取代码
 ...
const middlewares = {};
fileList.forEach((file) => {
...
  省略类似的name获取代码
 ...
for (let i = 0, len = names.length; i < len; ++i) {
    if (i === len - 1) {
      // 不同的是 middleware 是个函数,直接调用,controller是个类
        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 routerSchema = {};
fileList.forEach((file) => {
  ...
  省略类似的name获取代码
  ...
  // 不同部分,json-schema文件导出的是对象,所以处理如下
  routerSchema = {
      ...routerSchema,
      ...require(path.resolve(file)),
   };
});

app.routerSchema = routerSchema;

router

该目录放置路由配置文件,路由的功能定义页面和接口路由,映射对应的controller.
elpis-core中对router处理为收集所有路由注册和一个路由兜底,代码如下:
routerLoader 核心代码:'

// 找到路由文件路径, app.businessPath 上面代码有写
const routerPath = path.resolve(app.businessPath, `./router`);

// 实例化 KoaRouter
const router = new KoaRouter();

// 注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `./**/**.js`));

fileList.forEach((file) => {
    require(path.resolve(file))(app, router);
});

//  路由兜底(健壮性)app.options 为 elpis-core 启动时传递的配置
router.get(/.*/, async (ctx) => {
ctx.status = 302; // 临时重定向
ctx.redirect(`${app?.options?.homePage ?? "/"}`);
});

// 路由注册到 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 的理解和应用,可能会存在错误的理解和待优化的地方,希望各路大佬指点,我为大佬点烟,大佬对我笑嘻嘻!

image.png

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