基于nodejs实现的elpis-core

88 阅读3分钟

前言

elpis-core是一个服务端的内核引擎,是elpis框架的重要组成部分。elpis-core通过实现不同的loader加载对应目录下的文件,提高开发效率。同时有各种中间件作用在“请求-响应”阶段,增强了elpis-core的健壮性,下面是一些项目的基本结构和各种loader的实现。

目录结构

---elpis
   |-- app
       |-- config // 存放配置文件
       |-- controller // 存放各种控制器
       |-- extend // 存放各种扩展功能
       |-- middleware // 存放各种中间件
       |-- router-schema // 存放json-schema
       |-- router // 存放路由
       |-- service // 存放各种服务
   |-- elpis-core
          |-- loader
                |-- config.js // config loader
                |-- contoller.js // controller loader
                |-- entend.js // extend loader
                |-- middleware.js // middleware loader
                |-- router-schema.js // router-schema loader
                |-- router.js // router loader
                |-- servce.js // service loader
          |-- env.js // 获取环境配置相关方法
          |-- index.js // 启动文件
   |-- config // 环境配置文件
   |-- index.js // 项目启动入口

configLoader

config中有config.default.js(默认配置),config.local.js(本地配置),config.beta.js(测试配置),config.prod.js(生产配置),config loader根据运行环境加载对应的配置文件并与默认配置合并。

const path = require("path");
const { sep } = path;
/**
 * config loader
 * @param {object} app koa实例
 * 配置区分 本地/测试/生产 通过 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
 */
module.exports = (app) => {
  // 找到config/目录
  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.error("[exception] config.default.js not found");
  }
  // 获取到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 (e) {
    console.error("[exception] env.config.js not found");
  }
  // 覆盖并加载config配置
  app.config = Object.assign({}, defaultConfig, envConfig);
};

extendLoader

主要是针对一些扩展的功能,例如日志。该loader通过收集extend目录所有扩展,挂载到koa实例上,可直接通过app.extend.xxx获取。

const path = require("path");
const { sep } = path;
const glob = require("glob");
/**
 * controller loader
 * @param {object} app koa实例
 *  加载所有 controller ,通过app.extend.${文件}’ 访问
 * 例子:
     app/extend
        | -- custom-extend.js
    => app.extend.customExtend  
 */

module.exports = (app) => {
  // 读取 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);
    // 截取路径
    name = name.substring(
      name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length,
      name.lastIndexOf(".")
    );
    // 把”-“统一改成驼峰式
    name = name.replace(/[_-][a-z]/gi, (str) => str.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);
  });
};

controllerLoader

controllerLoader通过读取controller目录下的所有控制器,将文件名字作为key挂载到app.controller上。

const path = require("path");
const { sep } = path;
const glob = require("glob");
/**
 * controller loader
 * @param {object} app koa实例
 *  加载所有 controller ,通过app.controller.${目录}.${文件}’ 访问
 * 例子:
     app/controller
       |
       | -- custom-module
                |
                | -- custom-controller.js
    => app.controller.customModule.customController
 */

module.exports = (app) => {
  // 读取 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);
    // 截取路径
    name = name.substring(
      name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length,
      name.lastIndexOf(".")
    );
    // 把”-“统一改成驼峰式
    name = name.replace(/[_-][a-z]/gi, (str) => str.substring(1).toUpperCase());
    // 挂载controller到内存 app 对象中
    let tempController = controller;
    const names = name.split(sep);
    for (let i = 0, len = names.length; i < names.length; i++) {
      const newName = names[i];
      if (i === len - 1) {
        // 文件
        const ControllerModule = require(path.resolve(file))(app);
        tempController[newName] = new ControllerModule();
      } else {
        // 文件夹
        tempController[newName] = tempController[newName] || {};
        tempController = tempController[newName];
      }
    }
  });
  app.controller = controller;
};

serviceLoader

serviceLoader与controllerLoader类似,只不过是处理服务相关的,实现一些复杂的业务逻辑。通过读取service目录下的所有服务,最终挂载到app.service上,代码同controllerLoader。

middlewareLoader

主要处理一些中间件,在“请求-响应”阶段做一系列的处理,例如参数校验,错误捕获等。也是通过读取middleware目录下的所有中间件,挂载到app.middlewares上。

const path = require("path");
const { sep } = path;
const glob = require("glob");

/**
 * middleware loader
 * @param {object} app Koa 实例
 * 加载所有 middleware,可通过‘app.middleware.${目录}.${文件}’ 访问
 * 例子:
     app/middleware
       |
       | -- custom-module
                |
                | -- custom-middleware.js
    => app.middleware.customModule.customMiddleware
 */
module.exports = (app) => {
  // 读取 app/middleware/**/**.js 目录下所有文件
  const middlewarePath = path.resolve(app.businessPath, `.${sep}middleware`);
  const fileList = glob.sync(
    path.resolve(middlewarePath, `.${sep}**${sep}**.js`)
  );
  // 遍历所有文件,把内容加载到 app.middleware 中
  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]/gi, (str) => str.substring(1).toUpperCase());
    // 挂载middleware到内存 app 对象中
    let tempMiddleware = middlewares;
    const names = name.split(sep);
    for (let i = 0, len = names.length; i < names.length; i++) {
      const newName = names[i];
      if (i === len - 1) {
        tempMiddleware[newName] = require(path.resolve(file))(app);
      } else {
        tempMiddleware[newName] = tempMiddleware[newName] || {};
        tempMiddleware = tempMiddleware[newName];
      }
    }
  });
  app.middlewares = middlewares;
};

routerLoader

在router目录下存放各种路由配置文件,该loader会收集所有路由注册并设置一个重定向路由兜底。

const KoaRouter = require("koa-router");
const path = require("path");
const { sep } = path;
const glob = require("glob");
/**
 * router loader
 * @param {object} app koa实例
 * 解析所有 app/router/ 下所有js文件,加载到KoaRouter下
 */

module.exports = (app) => {
  // 找到路由文件路径
  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) => {
    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());
};

routerSchemaLoader

router-schema目录放置json-schema文件,通过设置对应请求url获取对应配置,具体配置可参考json-schema官网进行查看。同样的会挂载到app.routerSchema下,具体如下:

const path = require("path");
const { sep } = path;
const glob = require("glob");

/**
 * router-schema loader
 * @param {object} app Koa实例
 * 通过 ‘json-schema & ajv‘ 对API规则进行约束,配合api-params-verify中间件使用
 * app/router-schema/**.js
      输出:
      app.routerSchema = {
        '${api1}': ${jsonSchema}
      }
 * 
 */

module.exports = (app) => {
  // 读取 app/controller/**/**.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;
};

总结

通过实现这些loader基本完成了elpis-core的搭建,学习完这一章节,我对nodejs有了更清晰的认识,也对服务端的相关知识有了更全面的了解,希望在后续章节的学习中能够更上一层楼,逐步完善这一框架,并掌握相关设计思想和理念。

引用:抖音“哲玄前端”《大前端全栈实践》