Egg 框架模型简述 (一)
- 简述
- 简单层级关系
- 路由(Router)
- 内置对象
- 配置(Config)
- 中间件(MiddleWare)
- 插件(Plugins)
- 常用对象关系模型(egg-sequelize)
- Worker和Agent
- 定时任务
1. 简述
官方文档:https://eggjs.org
egg.js是基于koa为底层,由阿里nodejs团队封装的企业级Web应用解决方案,以约束和规范化团队开发,帮助开发团队和开发人员降低开发和维护成本为核心设计理念的优秀解决方案。
官方文档对 egg.js 的阐述极致细致,撰写本文的目的仅仅是对 Egg 的整体结构做一个简述,以引导学习为主要目的。
P.S. 本文示例代码部分使用 TypeScript
进行编写,因此所有源码文件都以 .ts
作为扩展名。
2. 简单层级关系
MVC(Model View Controller)是一种软件设计模式,一种以“展示界面、业务逻辑、数据模型”分离的方法组织代码,将业务设计打散分离,以便实现高可复用性,及可维护性。
早些年的项目中,Controller层级中需要处理的事情非常之多:接受用户请求、验证请求有效性、计算或发送请求至Model抓取数据或修改、计算响应数据、返回响应数据等。
image
随着一些项目逐渐庞大,这样的设计造成了同一文件(或函数)的代码剧增,可维护性降低。同时,有一些可公用的业务操作也急需单独提取,因此形成了独立的业务层,分化了Controller部分。
image
至此,形成了常见的软件设计层次结构的主线路:
- View:作为用户的 视图表现 部分,常见的展示形式如浏览器作为载体的网页、原生APP应用界面、桌面应用界面等,用于提供用户界面以便收集、响应用户行为产生的数据;
- Controller:作为 控制器层 部分,控制用户界面(View)的数据流转途径,主要行为包含接收用户数据请求、发送请求至业务层(Service)、获取业务层(Service)数据响应,将响应数据发送至用户界面(View),或生成相应的模板界面发送至用户;
- Service:作为 业务处理层 部分,主要负责收集及对数据进行相应的运算处理,主要行为包含收集控制器请求数据、数据有效性验证、运算、请求数据模型(Model)、接收数据模型(Model)响应消息、响应结果至控制器等;
- Model:作为 数据模型层 部分,主要用于将数据持久化(OUT)、查询持久化数据(IN),常见行为如对数据库进行操作、缓存数据库数据等;
// 这是一个 egg 项目的目录结构
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ └─ model
│ └─ user.ts
3. 路由(Router)
路由主要用于对数据流向进行指引,并处理请求转发。生活中常见的就是家用的路由器:
在Web应用进行前后端交互的过程中,路由亦起到了通过URL地址定位控制器函数的作用,当然,更准确的说法应该是定位静态资源(无论是接口数据、页面、图片等其他文件)。如假设
app/controller/home.ts
中存在函数 a()
和函数 b()
,我们约定了跳转 http://luv-ui.com/a
则执行函数 a()
;跳转 http://luv-ui.com/b
则执行函数 b()
。这是Web应用中的控制器-路由的常见表现手段。在JAVA项目中,常见的路由表现手段例如
- 在XML配置文件中对路由进行统一描述:
<package name="default" namespace="/" extends="struts-default">
<action name="aa" class="com.pro.controller.HomeAction" method="aa">
<result>/aa.jsp</result>
</action>
<action name="bb" class="com.pro.controller.HomeAction" method="bb">
<result>/bb.jsp</result>
</action>
</package>
- 在JAVA控制器文件中以注解的形式进行单独描述:
@RestController
@RequestMapping("/home")
public class HomeController {
@RequestMapping(value = "/aa", method = RequestMethod.POST)
public Message aa(){
// do something
}
@RequestMapping(value = "/bb", method = RequestMethod.POST)
public Message bb(){
// do something
}
}
在 Egg 中,约定了路由统一由 app/router.ts
进行定义,理由是:通过统一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突,集中在一起我们可以更方便的来查看全局的路由规则。
因此,我们的目录结构变化为:
// 这是一个 egg 项目的目录结构
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ └─ router.ts
而 router.ts
中的处理方式如:
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
router.get('/aa', controller.home.aa);
router.get('/bb', controller.home.bb);
router.post('/user/cc', controller.user.cc);
// ...
}
其业务逻辑如下图所示:
image
4. 内置对象
Egg 中包含两种内置对象:
- 由
Koa
继承的对象:Application
、Context
、Request
、Response
- 框架扩展的对象:
Controller
、Service
、Helper
、Config
、Logger
其主要作用如下:
对象名 | 注释 |
---|---|
Application | 全局应用对象,在一个应用中,只会实例化一个,我们可以为其挂载一些全局的方法和对象。在框架运行时,会在 Application 实例上触发一些事件。我们几乎可以在编写应用时的任何一个地方获取到 Application 对象用于操作。 |
Context | 一个请求级别的对象,在每一次收到用户请求时,框架都会实例化一个 Context 对象,这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。通常在 Middleware 、Controller 、Service 中获取操作。 |
Request | 一个请求级别的对象,封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。通过 Context 对象的 ctx.request 来获取其实例。 |
Response | 一个请求级别的对象,封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。通过 Context 对象的 ctx.response 来获取其实例。 |
Controller |
Controller 控制器的基类,所有的 Controller 都应该继承于该基类。它提供了如下常用属性: - ctx : 获取当前请求中的Context 对象;- app : 应用的 Application 实例; - config :当前应用的配置对象。 - service :包含应用所有 Service 的对象。 - logger :为当前 Controller 封装的 logger 日志对象。 |
Service |
Service 业务层的基类,所有的 Service 都应该继承于该基类。其提供的属性和基类调用的方式,都与 Controller 类似。 |
Helper | 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的隶属于工具对象的动作抽离在 helper.js 里面成为一个独立的函数,避免逻辑分散各处,同时可以更好的编写测试用例。 |
Config |
Egg 推荐应用开发遵循配置和代码分离的原则,将一些需要硬编码的业务配置都放到配置文件中。在不同的运行环境可以应用不同的配置改变框架运行方式。(如开发环境和生产环境不同,对数据源、日志、插件等的应用也可能有所不同) |
Logger |
Egg 内置了功能强大的日志功能,可以非常方便的打印各种级别的日志到对应的日志文件中,每一个 logger 对象都提供了 4 个级别的方法:- logger.debug() :用于调试阶段日志记录。 - logger.info() :用于正常流程日志记录。 - logger.warn() :用于警告级别的日志记录。 - logger.error() :用于严重错误的日志记录。 |
4.1 应用过程 - Controller
结合数据流转过程,当数据传递至 Controller
时,我们需要进行相应的处理。Egg
约定了所有的 Controller
对象都放在 app/controller/
位置。 Controller
部分大致长这个样子:
import { Context, Controller } from 'egg';
export default class HomeController extends Controller {
constructor(ctx: Context) {
super(ctx);
// do something
}
// 具体的请求函数
public async foo() {
const { ctx } = this; // this 代表当前 Controller 对象本身
const { code } = ctx.query; // 获取 Get 请求中的参数 code
ctx.body = await ctx.service.home.foo( code ); // 异步调用 Service 对象中的相应业务处理,并将结果对调用者响应
}
}
在应用的过程中,我们也可以创建自己的 BaseController
继承自 Controller
基类。再由具体的控制器类继承自 BaseController
,以便于实现统一的代码部分封装。
该示例中,默认导出的类命名方式为 XxxController
,此时,在 router.ts
中,便可以通过 app.controller.home.foo
来指定业务流转至该函数,来获取相应资源。
同理,ctx
对象中包含的 service
对象, 囊括了所有 app/service/
层级下的 Service
继承类,因此可以简单的使用 ctx.service.xxx.yyy
来定位业务函数。
4.2 应用过程 - Service
在业务处理的 Service
部分,Egg
约定了所有的 Service
对象都放在 app/service/
位置。大概长这个样子:
import { Context, Service } from 'egg';
export default class HomeService extends Service {
constructor(ctx: Context) {
super(ctx);
// do something
}
// 具体的业务处理函数
public async foo( code: string ) {
const { ctx } = this; // this 代表当前 Service 对象本身
const where = { code };
return await ctx.model.user.findAll({where}); // 通过 Model(数据模型) 部分获取静态资源
}
}
至此,我们所看到的业务流程就变成了这个样子:
image
5. 配置(Config)
Egg
使用代码的方式配置当前应用的运行方式,Egg
约定了所有的配置文件都放在 ./config/
位置。目录结构如下:
// 这是一个 egg 项目的目录结构
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ └─ router.ts
├─ config
│ ├─ config.default.ts
│ ├─ config.prod.ts
│ └─ config.local.ts
配置文件返回的是一个 object 对象,可以覆盖框架的一些配置,应用也可以将自己业务的配置放到这里方便管理。配置文件大概长这个样子:
// 配置文件的写法 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 其他的配置内容...
return {
...config
};
}
我们常常在配置文件中定义 中间件、日志、其他插件 的运行方式,比如在整个应用启动的过程中,运行哪些中间件;日志输出的方式、其他一些插件在运行过程中的参数配置之类的。这样的配置,可能会区分为 开发环境、测试环境、生产环境 等等,在每个环境中的配置方式都可能有所不同。例如你的本地开发使用本地数据库跑数据,连接本地库的 IP、用户、密码、端口等,与线上环境的肯定有所不同。因此,针对不同环境应用不同的配置非常有意义。
值得注意的是,config.default 在任何环境中都会被加载,但加载的过程中,若环境配置中有重复项,则会覆盖 default 中的内容。
image
由于 config.{env}.ts 的优先级更大 (它需要覆盖默认配置,来彰显自己的独立性),因此应用启动时配置文件的加载顺序是:
- config.default.ts
- config.{env}.ts
如何变更当前运行环境中的启动配置:
- 在
config
目录下新建文件env
,在文件中键入当前环境关键字。如键入prod
,则在应用启动时加载文件config/config.prod.ts
; - 配置环境变量
EGG_SERVER_ENV
指定运行环境,启动应用的过程中会读取process.env.EGG_SERVER_ENV
来判断当前应使用何种方式配置应用。
注意,与其他语言开发项目不同的是,nodejs 作为服务器端环境,自提供了一个 webserver,而无需使用其他容器作为应用载体。因此,应用的启动就代表着服务器的启动。
此时,我们的项目结构变成了这个样子:
image
6. 中间件(MiddleWare)
Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。类似于这个样子:
Egg 约定一个中间件是一个放置在
app/middleware/
下的独立文件,并会 exports 一个函数。函数接收两个参数:- options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
- app: 当前应用 Application 的实例。
例如,我们写了一个验证请求中是否携带 token 的中间件:
// 一个中间件 ( app/middleware/xtoken.ts )
import { Context } from 'egg';
export default (options) => {
return async (ctx: Context, next: Function) => {
// 排除登录路径, 其他路径需通过 token 校验
const { url } = ctx.request;
if (!options.exclude[url]) {
return await next();
}
// 检查 token 有效性...
};
}
中间件编写完成之后,我们需要在配置文件中,配置该中间件,使其生效:
// 配置文件 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 配置中间件
config.middleware = ['xtoken', 'otherMiddleWare'];
// 为中间件添加动态配置
config.xtoken = {
exclude: { '/access': true }
};
// 其他的配置内容...
return {
...config
};
}
届时,我们通过该中间件,描述了所有的请求必须经过 token 校验,除了排除列表中的请求。当然,这是应用中使用中间件的方式,还可以在框架、插件,乃至于在 router
中明确哪个请求才会由中间件进行处理。
此时的目录结构如下:
// 这是一个 egg 项目的目录结构
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ ├─ middleware
│ │ └─ xtoken.ts
│ └─ router.ts
├─ config
│ ├─ config.default.ts
│ ├─ config.prod.ts
│ └─ config.local.ts
多个中间件时
当应用中包含有多个中间件,则中间件的加载顺序以 config
中声明中间件的数组顺序而定,假设我们在中间件定义中声明:config.middleware = ['mw1', 'mw2', 'mw3'];
,则中间件的加载顺序为:mw1 -> mw2 -> mw3
,在请求拦截处理中的嵌套关系为:
由此可见,最后被加载的中间件,将置于请求过程中的最内层进行拦截。
更简单的拦截处理
在上述示例中,我们在 config
配置文件中,在声明中间件结束时,为 xtoken
设置了自定义属性 exclude
作为拦截条件,在中间件的定义文件 app/middleware/xtoken.ts
中以参数 options
获取了拦截条件并执行相应的逻辑。而在实际开发应用时,中间件已配备了几个通用参数,用以更简便的设置中间件的状态:
属性名 | 类型 | 注释 |
---|---|---|
enable | boolean | 控制中间件是否开启。 |
match | string、stringp[]、RegEx、function | 设置只有符合某些规则的请求前缀才会经过这个中间件。 |
ignore | string、stringp[]、RegEx、function | 设置符合某些规则的请求前缀不经过这个中间件。 |
因此,我们在 config
中的拦截规则便可以简单的改造为:
// 配置文件 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 配置中间件
config.middleware = ['xtoken', 'otherMiddleWare'];
// 为中间件添加动态配置
config.xtoken = {
// 配置所有的前缀为 /access 或 /morepath 的 url 不经过该中间件
ignore: [ '/access', '/morepath' ]
};
// 其他的配置内容...
return {
...config
};
}
而在中间件文件中,便可以省去了对于拦截条件的校验 -
网友评论