本文首发于:kapeter.com/post/33
文章列表:
- 如何规范接口输出协议——Eggjs最佳实践系列(一)
- 如何合理地设计代码分层——Eggjs最佳实践系列(二)
这是Eggjs最佳实践系列的第二篇文章。
在开始项目之前,我们往往需要根据需求对整个系统进行架构设计。在复杂系统中,一个好的架构能提升代码的可维护性,业务功能的可扩展性,从而延长整个系统的生命周期。反之,一个坏的架构,会造成代码日益冗杂,功能划分不清,最后导致整个系统崩溃。因此,如何根据业务需求设计系统架构是非常关键的。
在架构设计中,最经典的要属分层架构。根据业务职责,将系统分成N层,也称N层架构模式。如MVC架构中,将系统分为Model(模型层)、Controller(控制层)、View(视图层),每一层负责不同的功能,并相互隔离,达到“高内聚,低耦合”的效果。
本文将围绕分层架构进行展开。由于目前大多采用前后端分离的开发方式,本文中不涉及任何View(视图层)。
基础分层
首先,我们来看看Eggjs提供给我们的分层结构。
从图中可知,这是一个类似于MVC架构的分层模型,一个请求过来之后,首先Router
(路由)会进行路径匹配找到对应的Controller
(控制器),Controller
会向Service
(业务逻辑层)请求数据,Service
会去数据库中拉取数据,并返回给Controller
,Controller
在响应给客户端。
来看看Eggjs对于Controller
和Service
的职责划分。
Controller
(控制器):
- 获取用户通过 HTTP 传递过来的请求参数。
- 校验、组装参数。
- 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
- 通过 HTTP 将结果响应给用户。
Service
(服务):
- 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
- 第三方服务的调用,比如 GitHub 信息获取等。
数据访问对象(Data Access Object)
观察基础分层架构,Service
直连数据库,这里存在一些问题:
- 当系统存在多个数据库,且数据库类型不一样,
Service
无法进行有效的管理; - 一般数据库为关系型数据库,获取数据后,
Service
需要将数据转化成对象,转化过程比较复杂; Service
通过原生SQL查询数据库,代码可读性、可靠性较低。
为此,我们需要在Service
与数据库之间引入一个Model层(ORM框架)。
官方推荐使用sequelize
,这是一个基于 promise 的 Node.js ORM框架, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server。它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。
具体使用方法,可见官方文档,这里不做赘述。
自定义分层
以上的分层架构属于业界通用的方案,可以应对绝大多数情况。我们还可以根据自身业务需求,增加新的分层。
假设这么一个业务场景:前端同学提出,需要调整接口数据结构以适应前端需求,且每个接口的处理格式还不一样。按照分层,我们可以在Controller
处理这个。随着业务场景的日益复杂,这部分的代码会不断增加,导致Controller
功能冗杂,出现了厚Controller
的问题。因此,我们需要把这部分恶心且和业务深度绑定的代码抽取到一个新的层上,来保持Controller
部分代码整洁。
这里参考axios
的设计,我们可以在request
和response
中加入interceptor
(拦截器),也就是在用户与Controller
之间加一层处理请求参数和响应数据。
来看一下具体实现。
按照Eggjs的设计规范,在app
文件夹内新建一个interceptor
文件夹用来存放这些拦截器。
先来新建一个interceptor
对象。因为是从Controller
分离出来的,所以需要继承Controller
对象基类,从而在使用中可以调用Controller
中的属性和方法。本着“约定优于配置”的原则,我们可以约定统一使用transformer
方法进行数据转化。
// app/interceptor/showProject.js
'use strict';
const Controller = require('egg').Controller;
class ShowProjectInterceptor extends Controller {
/**
* @description 转化函数
* @author hefeixiang2
* @param {*} data 转化前的数据
* @return {*} 转换后的数据
* @memberof ShowProjectInterceptor
*/
transformer(data) {
data.dataValues.transformer = true;
return data;
}
}
module.exports = ShowProjectInterceptor;
复制代码
接着,通过Loader
注入到全局对象中,这里选择注入到Context
对象中,具体的区别可以看官网解释。
const interceptor = path.join(app.config.baseDir, 'app/interceptor');
app.loader.loadToContext(interceptor, 'interceptor');
复制代码
现在,我们就可以在项目中通过ctx.interceptor
调用拦截器了。
如果需要为所有请求加入统一的拦截器,我们可以使用middleware
(中间件)。
如果需要为某个接口添加拦截器,我们可以在Controller
里单独调用。
我们对《如何规范接口输出协议——Eggjs最佳实践系列(一)》中提到的success
函数进行改造。
/**
* @description
* @author hefeixiang2
* @param {*} { data, status, interceptor }
*/
success({ data, status, interceptor = null }) {
const { ctx } = this;
// 如果存在拦截器,运行拦截器
if (interceptor) {
if (interceptor.transformer && ctx.helper.isFunc(interceptor.transformer)) {
data = interceptor.transformer(data);
} else {
console.error('拦截器设置错误:缺少transformer方法');
}
}
ctx.body = {
code: '0',
message: 'success',
result: data || null,
sysTime: ctx.helper.now(),
};
ctx.status = status || 200;
}
复制代码
然后就可以在Controller
中使用了。
ctx.response.success({ data: result, interceptor: ctx.interceptor.showProject });
复制代码