基于Koa实现的服务端框架elpis-core

208 阅读2分钟

elpis-core

本文基于 抖音“哲玄前端”,《全栈实践课》,引擎内核实现章节所写。

什么是elpis-core

elpis-core是基于Koa实现的一个类似于Egg的轻量级的服务端框。同样采用约定大于配置的形式,通过一系列的loader解析静态文件加载到内存当中。包括routerLoaderrouterSchemaLoadermiddlewareLoadercontrollerLoaderserviceLoaderextendLoaderconfigLoader

约定的目录结构

app ---------------
  |-- controller
        |-- custom
              |-- xxx.js
  |-- extend
        |-- logger.js
  |-- middlewares
  |-- router
  |-- service
  |-- config
        |-- config.dev.js
        |-- config.prod.js
  |-- middleware.js

routerLoader

注册所有路由到Koa实例app上,并进行路由兜底,保证代码的健壮性

// 注册所有路由
  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());

routerSchemaLoader

注册所有 routerSchema,使得可以 'app.routerSchema' 这样访问,同时挂载到app.routerSchema上

// 读取 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;

middlewareLoader

读取app/middleware下子目录以及下面所有中间件文件,使得可以通过.文件名的方式访问。在这里可以设置一层层的中间件,每个中间件单独负责某个功能。例如进行错误捕获、参数校验、api签名请求合法性校验等一系列操作。这种形式也是基于洋葱圈模型实现的。除此之外,项目还注册使用了全局中间件。 image.png

controllerLoader

主要进行业务逻辑处理。同样是加载app/controller下的所有文件,并挂载到app.controller上。

// 读取 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 = {};
  fileList.forEach((file) => {
    // 提取文件名称
    let name = path.resolve(file);
    // 截取路径 app/controller/custom-module/custom-controller.js
    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); // [customModule, customController]
    for (let i = 0, len = names.length; 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]];
        // { customModule: { customController: {} } }
      }
    }
  });
  app.controller = controller;

serviceLoader。

service层将会提供读写mysql、调用外部服务等功能。用来处理具体的业务,可供controller层调用。

// 读取 app/service/**/**.js 下所有的文件
  const servicePath = path.resolve(app.businessPath, `.${sep}service`);
  const fileList = glob.sync(path.resolve(servicePath, `.${sep}**${sep}**.js`));
  // 遍历所有文件目录,把内容加载到 app.service 下
  const service = {};
  fileList.forEach((file) => {
    // 提取文件名称
    let name = path.resolve(file);

    // 截取路径 app/service/custom-module/custom-service.js
    name = name.substring(
      name.lastIndexOf(`service${sep}`) + `service${sep}`.length,
      name.lastIndexOf(".")
    );
    // 把 '-' 统一改成驼峰式,custom-module/custom-service.js => customModule.customService
    name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
    // 挂载 service 到内存 app 中
    let tempService = service;
    const names = name.split(sep); // [customModule, customService]
    for (let i = 0, len = names.length; 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]];
        // { customModule: { customService: {} } }
      }
    }
  });
  app.service = service;

extendLoader

提供扩展服务,例如日志

// 读取 app/extend/**.js 下所有的文件
  const extendPath = path.resolve(app.businessPath, `.${sep}extend`);
  const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`));
  // 遍历所有文件目录,把内容加载到 app.extend 下
  //   const extend = {};
  fileList.forEach((file) => {
    // 提取文件名称
    let name = path.resolve(file);

    // 截取路径 app/extend/custom-module/custom-extend.js => 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
    for (const key in app) {
      if (key === name) {
        console.log(`[extend load error] name:${name} is already in app`);
        return;
      }
    }
    // 挂载 extend 到 app 上
    app[name] = require(path.resolve(file))(app);
  });

configLoader

配置区分 本地/测试/生产,通过env 环境读取不同文件配置 env.config,通过 env.config 覆盖 default.config 加载到 app.config 中。

目录对应的config 配置

  • 默认配置 config/config.default.js
  • 本地配置 config/config.local.js
  • 测试配置 config/config.beta.js
  • 生产配置 config/config.prod.js
// 找到 config /目录
  const configPath = path.resolve(app.baseDir, `.${sep}config`);
  // 获取default.config
  let defaultConfig = {};
  try {
    defaultConfig = require(path.resolve(
      configPath,
      `.${sep}config.default.js`
    ));
  } catch (error) {
    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.prod.js`));
    }
  } catch (error) {
    console.log("[exception] there is no env.config file");
  }
  // 覆盖并加载config配置
  app.config = Object.assign({}, defaultConfig, envConfig);

最终实现

在index.js中加载所有的loader,目前加载顺序 middlewareLoader=>routerSchemaLoader=>controllerLoader=>serviceLoader=>configLoader=>extendLoader=>routerLoader

总结

目前初次接触Nodejs、Koa中间件等内容,对elpis-core的理解还不够深刻。希望能够跟着哲哥和群内大佬多多学习!!!