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