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 服务端引擎的项目结构、核心模块、路由和接口、控制器和服务的使用方法,以及项目的启动流程和开发注意事项。

相关推荐
qq_3325394523 分钟前
React 前端框架推荐
前端·react.js·前端框架
拉不动的猪40 分钟前
刷刷题34(uniapp中级实际项目问题-1)
前端·vue.js·面试
奔跑的露西ly1 小时前
【HarmonyOS NEXT】实现文字环绕动态文本效果
前端·javascript·html·harmonyos
irving同学462383 小时前
Next.js 组件开发最佳实践文档(TypeScript 版)
前端
刺客-Andy3 小时前
React Vue 项开发中组件封装原则及注意事项
前端·vue.js·react.js
marzdata_lily3 小时前
从零到上线!7天搭建高并发体育比分网站全记录(附Java+Vue开源代码)
前端·后端
小君3 小时前
让 Cursor 更加聪明
前端·人工智能·后端
顾林海3 小时前
Flutter Dart 异常处理全面解析
android·前端·flutter
残轩4 小时前
JavaScript/TypeScript异步任务并发实用指南
前端·javascript·typescript
用户88442839014254 小时前
xterm + socket.io 实现 Web Terminal
前端