Elpis 服务端引擎

Elpis 服务端引擎

一、项目概述

Elpis 项目是一个基于 Koa 框架构建的服务端应用,它整合了多种功能模块,包括路由管理、中间件处理、服务调用和配置管理等。支持不同环境(本地、测试、生产)的配置,能满足多样化的开发和部署需求。

二、项目结构

整体结构

javascript 复制代码
.eslintignore
.eslintrc.json
.git/
.gitignore
README.md
app/
  controller/
  extend/
  middleware/
  middleware.js
  public/
  router/
  router-schema/
  service/
config/
  config.beta.js
  config.default.js
  config.prod.js
elpis-core/
  env.js
  index.js
  loader/
index.js
package.json

关键目录及功能

  • app:存放业务逻辑代码,包含控制器、中间件、服务、扩展等。

    • controller:处理业务逻辑,响应客户端请求。
    • middleware:包含中间件,用于处理请求的预处理和后处理。
    • service:封装业务逻辑,供控制器调用。
    • router:定义路由规则,将请求映射到相应的控制器方法。
    • router - schema:定义路由的参数校验规则。
  • config:根据不同环境(本地、测试、生产)提供不同的配置文件。

  • elpis - core:核心加载器目录,负责加载中间件、路由、控制器等。

三、核心模块

1. 加载器模块

中间件加载器 (elpis-core/loader/middleware.js)

该模块负责加载 app/middleware 目录下的所有中间件,并将其挂载到 app.middlewares 对象中。

javascript 复制代码
//elpis-core/loader/middleware.js
const glob = require("glob");
const path = require("path");
const { sep } = path;

module.exports = (app) => {
  const middlewarePath = path.resolve(app.businessPath, `.${sep}middleware`);
  const fileList = glob.sync(
    path.resolve(middlewarePath, `.${sep}**${sep}**.js`)
  );
  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, (s) => s.substring(1).toUpperCase());

    let tempMiddleware = middlewares;
    const names = name.split(sep);
    for (let i = 0; i < names.length; i++) {
      if (i === names.length - 1) {
        tempMiddleware[names[i]] = require(path.resolve(file))(app);
      } else {
        if (!tempMiddleware[names[i]]) {
          tempMiddleware[names[i]] = {};
        }
        tempMiddleware = tempMiddleware[names[i]];
      }
    }
  });
  app.middlewares = middlewares;
};
路由加载器 (elpis-core/loader/router.js)

该模块读取 app/router 目录下的所有路由文件,并将其注册到 Koa 路由器中。

javascript 复制代码
//elpis-core/loader/router.js
const KoaRouter = require("koa-router");
const glob = require("glob");
const path = require("path");
const { sep } = path;

module.exports = (app) => {
  const routerPath = path.resolve(app.businessPath, `.${sep}router`);
  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.use(router.routes());
  app.use(router.allowedMethods());
};
控制器加载器

虽然代码中未给出完整的控制器加载器实现,但可以推测其功能是加载 app/controller 目录下的所有控制器,并将其挂载到 app.controller 对象中。

服务加载器 (elpis-core/loader/service.js)

该模块加载 app/service 目录下的所有服务,并将其挂载到 app.service 对象中。

javascript 复制代码
//elpis-core/loader/service.js
const glob = require("glob");
const path = require("path");
const { sep } = path;

module.exports = (app) => {
  const servicePath = path.resolve(app.businessPath, `.${sep}service`);
  const fileList = glob.sync(path.resolve(servicePath, `.${sep}**${sep}**.js`));
  const service = {};
  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]/gi, (s) => s.substring(1).toUpperCase());

    let tempService = service;
    const names = name.split(sep);
    for (let i = 0; i < names.length; i++) {
      if (i === names.length - 1) {
        const ServiceMoule = require(path.resolve(file))(app);
        tempService[names[i]] = new ServiceMoule();
      } else {
        if (!tempService[names[i]]) {
          tempService[names[i]] = {};
        }
        tempService = tempService[names[i]];
      }
    }
  });
  app.service = service;
};
配置加载器 (elpis-core/loader/config.js)

该模块根据不同的环境(本地、测试、生产)加载对应的配置文件,并将其合并到 app.config 对象中。

javascript 复制代码
//elpis-core/loader/config.js
const path = require("path");
const { sep } = path;

module.exports = (app) => {
  const configPath = path.resolve(app.baseDir, `.${sep}config`);
  let defaultConfig = {};
  try {
    defaultConfig = require(path.resolve(
      configPath,
      `.${sep}config.default.js`
    ));
  } catch (error) {
    console.log("-- [exception] config.default.js not found");
  }

  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] env.config not found`);
  }
  app.config = Object.assign({}, defaultConfig, envConfig);
};
扩展加载器 (elpis-core/loader/extend.js)

该模块加载 app/extend 目录下的所有扩展文件,并将其挂载到 app 对象中。

javascript 复制代码
//elpis-core/loader/extend.js
const glob = require("glob");
const path = require("path");
const { sep } = path;

module.exports = (app) => {
  const extendPath = path.resolve(app.businessPath, `.${sep}extend`);
  const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`));
  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, (s) => s.substring(1).toUpperCase());

    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);
  });
};

2. 中间件模块

API 参数校验中间件 (app/middleware/api-params-verify.js)

该中间件使用 ajv 库对 API 请求的参数进行校验,确保请求参数符合预定义的规则。

javascript 复制代码
//app/middleware/api-params-verify.js
const Ajv = require("ajv");
const ajv = new Ajv();

module.exports = (app) => {
  const $schema = "http://json-schema.org/draft-07/schema#";

  return async (ctx, next) => {
    if (ctx.path.indexOf("/api") < 0) {
      return await next();
    }

    const { body, query, headers } = ctx.request;
    const { params, path, method } = ctx;
    app.logger.info(`[${method} ${path}] body: ${JSON.stringify(body)}`);
    app.logger.info(`[${method} ${path}] query: ${JSON.stringify(query)}`);
    app.logger.info(`[${method} ${path}] params: ${JSON.stringify(params)}`);
    app.logger.info(`[${method} ${path}] headers: ${JSON.stringify(headers)}`);

    const schema = app.routerSchema[path]?.[method.toLowerCase()];

    if (!schema) {
      return await next();
    }

    let valid = true;
    let validate;

    if (valid && headers && schema.headers) {
      schema.headers.$schema = $schema;
      validate = ajv.compile(schema.headers);
      valid = validate(headers);
    }

    if (valid && body && schema.body) {
      schema.body.$schema = $schema;
      validate = ajv.compile(schema.body);
      valid = validate(body);
    }

    if (valid && body && schema.query) {
      schema.query.$schema = $schema;
      validate = ajv.compile(schema.query);
      valid = validate(query);
    }

    if (valid && body && schema.params) {
      schema.params.$schema = $schema;
      validate = ajv.compile(schema.params);
      valid = validate(params);
    }

    if (!valid) {
      ctx.status = 200;
      ctx.body = {
        success: false,
        msg: `request validate fail: ${ajv.errorsText(validate.errors)}`,
        code: 442,
      };
      return;
    }

    await next();
  };
};

3. 路由和接口

路由定义 (app/router/project.js)

该文件定义了 /api/project/list 接口的路由规则,将其映射到 projectController.getList 方法。

javascript 复制代码
//app/router/project.js
module.exports = (app, router) => {
  const { project: projectController } = app.controller;
  router.post(
    "/api/project/list",
    projectController.getList.bind(projectController)
  );
};
路由参数校验规则 (app/router-schema/project.js)

该文件定义了 /api/project/list 接口的参数校验规则,要求请求体中必须包含 proj_key 字段。

javascript 复制代码
//app/router-schema/project.js
module.exports = {
  "/api/project/list": {
    post: {
      body: {
        type: "object",
        properties: {
          proj_key: {
            type: "string",
          },
        },
        required: ["proj_key"],
      },
    },
  },
};

4. 控制器和服务

控制器 (app/controller/project.js)

该控制器处理 /api/project/list 接口的请求,调用 projectService.getList 方法获取项目列表,并返回响应。

javascript 复制代码
//app/controller/project.js
class ProjectController extends BaseController {
  async getList(ctx) {
    const { project: projectService } = app.service;
    const projectList = await projectService.getList();
    this.success(ctx, projectList);
  }
}
服务 (app/service/project.js)

该服务封装了获取项目列表的业务逻辑,返回一个包含项目信息的数组。

javascript 复制代码
//app/service/project.js
module.exports = (app) => {
  const BaseService = require("./base")(app);
  return class ProjectService extends BaseService {
    async getList() {
      return [
        {
          id: 1,
          name: "项目1",
          desc: "项目1的描述",
        },
        {
          id: 2,
          name: "项目2",
          desc: "项目2的描述",
        },
      ];
    }
  };
};

四、项目启动

启动脚本 (package.json)

javascript 复制代码
//package.json
"scripts": {
  "lint": "eslint --quiet --ext js,vue .",
  "dev": "set _ENV='local' && nodemon ./index.js",
  "beta": "set _ENV='beta' && nodemon ./index.js",
  "prod": "set _ENV='production' && nodemon ./index.js"
}
  • npm run dev:启动本地开发环境。
  • npm run beta:启动测试环境。
  • npm run prod:启动生产环境。

启动流程 (elpis-core/index.js)

javascript 复制代码
//elpis-core/index.js
const Koa = require("koa");
const path = require("path");
const { sep } = path;
const env = require("./env");
const middlewareLoader = require("./loader/middleware");
const routerSchemaLoader = require("./loader/router-schema");
const routerLoader = require("./loader/router");
const controllerLoader = require("./loader/controller");
const serviceLoader = require("./loader/service");
const configLoader = require("./loader/config");
const extendLoader = require("./loader/extend");

module.exports = {
  start(options = {}) {
    const app = new Koa();
    app.options = options;
    app.baseDir = process.cwd();
    app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
    app.env = env();
    console.log("-- [start] env--", ` ${app.env.get()}`);

    middlewareLoader(app);
    console.log("-- [start] load middlewareLoader--", app.middlewares);

    routerSchemaLoader(app);
    console.log("-- [start] load routerSchemaLoader--", app.routerSchema);

    controllerLoader(app);
    console.log("-- [start] load controllerLoader--", app.controller);

    serviceLoader(app);
    console.log("-- [start] load serviceLoader--", app.service);

    configLoader(app);
    console.log("-- [start] load configLoader--", app.config);

    extendLoader(app);
    console.log("-- [start] load extendLoader--");

    try {
      require(`${app.businessPath}${sep}middleware.js`)(app);
      console.log("-- [start] load global appMiddlewareLoader--");
    } catch (error) {
      console.log("-- [excepiton] global appMiddleware file is not found");
    }

    routerLoader(app);
    console.log("-- [start] load routerLoader--");

    try {
      const port = process.env.PORT || 8080;
      const host = process.env.IP || "0.0.0.0";
      app.listen(port, host);
      console.log(`Server running at http://${host}:${port}/`);
    } catch (error) {
      console.log(error);
    }
  },
};

五、开发注意事项

  • 代码规范 :遵循 ESLint 代码规范,使用 npm run lint 进行代码检查。
  • 模块加载:新增中间件、路由、控制器、服务等需要在对应的加载器中进行配置。
  • 环境配置 :不同环境的配置需要在 config 目录下创建对应的配置文件。
  • 错误处理:确保在控制器和服务中处理可能出现的错误,避免程序崩溃。

六、总结

通过以上文档,你可以全面了解 Elpis 服务端引擎的项目结构、核心模块、路由和接口、控制器和服务的使用方法,以及项目的启动流程和开发注意事项。

相关推荐
Sean_summer3 分钟前
暑期第二周
前端·数据库·python
_未完待续9 分钟前
框架实战指南-组件参考
前端·vue.js
李文旺9 分钟前
图片加载优化-Nextjs与webpack源码
前端·react.js
不想当小卡拉米9 分钟前
高德地图上marker过多(超过3000个)渲染卡顿过慢问题解决
前端
dalancon12 分钟前
WMShell初始化
前端
半花13 分钟前
【Vue】通信组件
前端·vue.js
劫大大14 分钟前
前端开发公众号或服务号,本地怎么与后端测试服接口打通呢
前端·微信
芒果12515 分钟前
【转载】vue3 Ts axios 封装
前端
蓝倾16 分钟前
京东商品SKU数据采集方式及接口说明
前端·后端·api
前端 贾公子17 分钟前
vue如何在data里使用this
前端·javascript·vue.js