前言
最近老大布置了一个全栈项目给我,前端依旧是老套路,重点是后端方面不再使用去年实习的时候用的 egg.js 框架而是 midway 框架,因为 egg.js 框架对 TypeScript 的支持存在一定的缺陷。这篇文章记录第一遍阅读 midway 中文官方文档的所学知识。
midway 简介
midway (中文官方文档)是阿里巴巴-淘宝前端架构团队,基于渐进式理念研发的 node.js 框架,通过自研的依赖注入容器,搭配各种上层模块,组合出适用于不同场景的解决方案。
它基于 TypeScript 开发,结合了面向对象(OOP + Class + IoC) 与 函数式(FP + Function + Hooks) 两种编程范式,致力于为用户提供简单、易用、可靠的 node.js 服务端研发体验。
创建第一个应用
环境准备
midway 支持 macOS、Linux 和 Windows 操作系统。在运行环境方面,midway 对 node.js 的版本要求如下表所示:
Midway 版本 | 开发环境 Node.js 版本要求 | 部署环境 Node.js 版本要求 |
---|---|---|
>= v3.9.0 | >= v14,推荐 LTS 版本 | >= v12.11.0 |
3.0.0 ~ 3.9.0 | >= v12,推荐 LTS 版本 | >= v12.0.0 |
2.x | >= v12,推荐 LTS 版本 | >= v10.0.0 |
我这里使用的 node.js 版本为 v18.17.1
初始化项目
使用脚手架,在 vscode 终端输入下面的指令,快速生成项目:
bash
$ npm init midway@latest -y
? Hello, traveller.
Which template do you like? ...
<koa-v3 - A Web application boilerplate with midway v3(koa)>
? What name would you like to use for the new project? <midway-sample>
这里我尝试用 pnpm 包管理器来初始化项目,但是执行pnpm init midway@latest -y
或者pnpm init midway@latest
都会报错,使用 npm 就没问题。
生成的项目目录结构如下图所示:
先使用pnpm install
命令安装依赖,随后使用pnpm run dev
命令启动应用,点击http://127.0.0.1:7001
进入应用页面,如下图所示:
项目分析
Controller 层分析
项目的controller 文件夹中,我们已经有了 home.controller.ts
和 api.controller.ts
文件,home.controller.ts
文件的代码如下:
typescript
// 引入 Controller 和 Get 装饰器
import { Controller, Get } from '@midwayjs/core';
// 使用 Controller 装饰器标注类 HomeController 为控制器
@Controller('/')
export class HomeController {
// 使用 Get 装饰器标注类方法 home() 用于处理前端发送的路径为 `http://127.0.0.1:7001/` 的 Get 请求,返回'Hello Midwayjs' 字符串数据
@Get('/')
async home(): Promise<string> {
// 可以直接返回字符串、数字、JSON、Buffer 等
return 'Hello Midwayjs!';
}
}
api.controller.ts
文件的代码如下:
typescript
// 引入 Inject、Controller、Get 和 Query 装饰器
import { Inject, Controller, Get, Query } from '@midwayjs/core';
// 引入上下文对象类型 Context
import { Context } from '@midwayjs/koa';
// 引入 service 文件夹下定义的 UserService 对象类型
import { UserService } from '../service/user.service';
// Controller 装饰器标注 APIController 为控制器
@Controller('/api')
export class APIController {
// 定义对象属性 ctx
@Inject()
ctx: Context;
// 定义对象属性 userService
@Inject()
userService: UserService;
// Get 装饰器对象标注 getUser() 函数用于处理前端发送的路径为`http://127.0.0.1/api/get_user`的 Get 请求,并返回内容为 { success: true, message: 'OK', data: user }; 的JSON 数据
@Get('/get_user')
async getUser(@Query('uid') uid) { // @Query 装饰器用于标注 Get 请求的url当中需要携带的 query 参数
const user = await this.userService.getUser({ uid });
return { success: true, message: 'OK', data: user };
}
}
Service 层分析
在实际项目中,Controller 一般用来接收请求参数,校验参数,不会包括特别复杂的逻辑,复杂而复用的逻辑需要封装到 Service 文件当中去。
在我们的项目当中,service 文件夹下存在user.service.ts
文件,它的代码如下:
typescript
// 引入 Provide 装饰器
import { Provide } from '@midwayjs/core';
// 引入 src 文件夹下的 interface 文件中定义的 IUserOptions 类型,interface.ts 文件用于添加类型定义,相当于前端项目中的 types.ts
import { IUserOptions } from '../interface';
// 使用 @Provide 装饰器标准类 UserService 是一个业务服务类
@Provide()
export class UserService {
// IUserOptions 定义了 getUser() 的参数对象
async getUser(options: IUserOptions) {
// getUser() 当中通常包含了对数据库进行增删查改的逻辑,以及一些复杂且复用的逻辑,并最终可以返回一个包含有数据的对象
return {
uid: options.uid,
username: 'mockedName',
phone: '12345678901',
email: 'xxx.xxx@xxx.com',
};
}
}
middleware 层分析
Web 中间件是在控制器调用 之前 和 之后 (部分)调用的函数。中间件函数可以访问请求和响应对象。
常见的应用场景莫过于登录鉴权,前端发送的一些请求,后端需要验证该请求的header中是否携带的用于身份验证的token,如果有并且与数据库对应成功则放行,如果没有或者对应失败则进行拦截,返回用户需要重新登录的响应,使得前端跳转到登录页面,令用户重新进行登录。这部分的逻辑就是写在 middleware 中间件 或者守卫当中的,守卫后文有作记录说明。
不同的上层 Web 框架中间件形式不同,Midway 标准的中间件基于洋葱圈模型,而Express则是传统的队列模型。简而言之,Koa 和 EggJs 可以在控制器前后都被执行,在 Express 中,中间件只能在控制器之前调用。
Web 中间件通常写在 src/middleware 文件夹下,我们的项目中该文件夹下有一个report.middleware.ts
文件,它的代码如下:
typescript
// 引入 Middleware 装饰器和 IMiddleware 类型
import { Middleware, IMiddleware } from '@midwayjs/core';
// 引入 NextFunction 类型和 Context 类型
import { NextFunction, Context } from '@midwayjs/koa';
// 使用 Middleware 装饰器标注类 ReportMiddleware 为中间件类
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> { // 类 ReportMiddleware 实现了 IMiddleware 接口
// 定义类方法 resolve,用于返回此中间件对请求进行处理的耗费时长
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
ctx.logger.info(
`Report in "src/middleware/report.middleware.ts", rt = ${
Date.now() - startTime
}ms`
);
// 返回给上一个中间件的结果
return result;
};
}
// 定义静态类方法 getName,返回此中间件的名字
static getName(): string {
return 'report';
}
}
filter 层分析
midway 中的 filter 层用于进行异常处理。
midway 提供了一个内置的异常处理器,负责处理应用程序中所有未处理的异常。当我们的应用程序代码抛出一个异常处理时,该处理器就会捕获该异常,然后等待用户处理。
异常处理器的执行位置处理中间件之后,所以它能拦截所有的中间件和业务抛出的错误。
在我们的项目中,filter 文件夹下存在两个文件default.filter.ts
和notfound.filter.ts
,它们分别用于处理默认的 Error 事件和 notfound Error 事件
default.filter.ts
文件的代码如下:
typescript
// 引入 Catch 装饰器
import { Catch } from '@midwayjs/core';
// 引入类型 Context
import { Context } from '@midwayjs/koa';
// 使用 Catch 装饰器标注类 DefaultErrorFilter 是一个异常处理器
@Catch()
export class DefaultErrorFilter {
async catch(err: Error, ctx: Context) {
// 所有的未分类错误会到这里
return {
success: false,
message: err.message,
};
}
}
notfound.filter.ts
文件的代码如下:
typescript
// 引入 Catch 装饰器,httpError 对象,MidwayHttpError 类型
import { Catch, httpError, MidwayHttpError } from '@midwayjs/core';
// 引入 Context 对象
import { Context } from '@midwayjs/koa';
// 使用 @Catch 装饰器并结合 httpError.NotFoundError 对象标注类 NotFoundFilter 是用于处理 http请求中的 NotFoundError 异常的异常处理器
@Catch(httpError.NotFoundError)
export class NotFoundFilter {
async catch(err: MidwayHttpError, ctx: Context) {
// 404 错误会到这里
ctx.redirect('/404.html');
}
}
config 多环境配置分析
配置是我们常用的功能,而且在不同的环境,经常会使用不同的配置信息。项目中的配置文件放在src/config
目录下,该目录下有两个文件config.default.ts
和config.unittest.ts
,分别是项目运行时默认的配置和单元测试的配置。
config.default.ts
文件中的代码如下:
typescript
import { MidwayConfig } from '@midwayjs/core';
export default {
// use for cookie sign key, should change to your own and keep security
keys: '1695551460155_9032',
koa: {
port: 7001,
},
} as MidwayConfig;
config.unittest.ts
文件中的代码如下:
typescript
import { MidwayConfig } from '@midwayjs/core';
export default {
koa: {
port: null,
},
} as MidwayConfig;
目前的配置还是过于简洁了,我们后续进行扩充。
configuration.ts 文件分析
configuration.ts 是 midway 的主配置文件,或者可以说主入口文件,它相当于前端项目中的 main.ts + vite.config.ts。初始创建的项目中,该文件中包含有如下代码:
typescript
// 引入 Configuration 装饰器和 App 装饰器
import { Configuration, App } from '@midwayjs/core';
// 引入 koa 插件的所有内容,这里由于我们初始化项目的时候选了 koa,所以引入的是 koa 插件,如果引入 egg.js,那么就会引入 egg 插件了
import * as koa from '@midwayjs/koa';
// 引入 validate 插件的所有内容
import * as validate from '@midwayjs/validate';
// 引入 info 插件的所有内容
import * as info from '@midwayjs/info';
// 引入 node.js 中 path 模块中的 join 函数
import { join } from 'path';
// 引入异常处理器
// import { DefaultErrorFilter } from './filter/default.filter';
// import { NotFoundFilter } from './filter/notfound.filter';
// 引入中间件
import { ReportMiddleware } from './middleware/report.middleware';
// 使用 Configuration 装饰器定义配置对象,相当于前端中 vite.config.ts 的部分
@Configuration({
// 引入插件功能
imports: [
koa,
validate,
{
component: info,
enabledEnvironment: ['local'],
},
],
// 引入 src/config 文件夹下的配置信息
importConfigs: [join(__dirname, './config')],
})
// 定义 MainConfiguration 对象,它相当于前端部分的 main.ts 文件
export class MainConfiguration {
// 使用 @App 装饰器标注我们的应用是基于 koa 的应用
@App('koa')
app: koa.Application;
// onReady 是应用的生命周期钩子之一,在 onReady 中进行一些操作
async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware]);
// add filter
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
}
}
到此,我们的项目分析部分就结束了,接下来进行 midway 基础理论部分的学习。
midway 基础理论
开发习惯
midway 对目录没有特别的限制,但是我们会遵守一些简单的开发习惯,将一部分常用的文件进行归类,放到一些默认的文件夹中。
以下 ts 源码文件夹均在src
目录下。
常用的有:
controller
:Web Controllermiddleware
:中间件filter
:过滤器aspect
:拦截器service
:服务逻辑目录entity
或model
:数据库实体目录config
:业务配置目录util
:工具类存放的目录decorator
:自定义装饰器目录interface.ts
:业务的 ts 类型定义文件
随着不同场景的出现,目录的习惯也会不断的增加,具体的目录内容会体现在不同的组件功能中。
路由和控制器(Controller 层基础理论)
在常见的 MVC 架构中,C 即代表控制器,用于负责 解析用户的输入,处理后返回相应的结果。
如下图所示,客户端通过 Http 协议请求服务端的控制器,控制器处理结束后响应客户端,这是一个最基础的"请求 - 响应"流程。
一般来说,控制器常用于对用户的请求参数做一些校验,转换,调用复杂的业务逻辑,拿到相应的业务结果后进行数据组装,然后返回。
在 midway 中,控制器 也承载了路由的能力,每个控制器可以提供多个路由,不同的路由可以执行不同的操作。
@Controller 装饰器和路由装饰器
midway 使用@Controller()
装饰器标注控制器,它有一个可选参数,用于进行路由前缀(分组),这样这个控制器下的所有路由都会带上这个前缀,例如@Controller('/api')
下面的所有路由都会带上/api
前缀。
路由装饰器包括有:@Get()
、@Post()
、@Put()
、@Del()
、@Patch()
、@Options()
、@Head
和@All()
,表示各自对应的 HTTP 请求方法。此外,还有一个@All()
装饰器,能接受前面所有类型的 HTTP 请求方法。
在实际开发的过程中,我们可以将多个路由绑定到同一个方法上,例如下面的代码:
typescript
@Get('/')
@Get('/home')
async home() {
return 'Hello Midwayjs!';
}
获取请求参数
装饰器参数约定
midway 提供了从 Query、Body、Header 等位置获取值的装饰器,这些都是开箱即用,并且适配于不同的上层框架。
下面是这些装饰器,以及对应的等价框架取值方式。
装饰器 | Express 对应的方法 | Koa/EggJS 对应的方法 |
---|---|---|
@Session(key?: string) | req.session / req.session[key] | ctx.session / ctx.session[key] |
@Param(key?: string) | req.params / req.params[key] | ctx.params / ctx.params[key] |
@Body(key?: string) | req.body / req.body[key] | ctx.request.body / ctx.request.body[key] |
@Query(key?: string) | req.query / req.query[key] | ctx.query / ctx.query[key] |
@Queries(key?: string) | 无 | 无 / ctx.queries[key] |
@Headers(name?: string) | req.headers / req.headers[name] | ctx.headers / ctx.headers[name] |
注意 @Queries 装饰器和 @Query 有所区别。Queries 会将相同的 key 聚合到一起,变为数组。当用户访问到的接口参数为/?name=a&name=b
时,@Querirs 会返回{name: [a, b]}
,而 Query 只会返回{ name: b }
Query
在 URL 中?
后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数。例如,GET /user?uid_1&sex=male
就是用户传递过来的参数。Query String 可以从装饰器@Query
中获取,也可以从 API 中获取。
下面是从装饰器获取的代码示例:
typescript
import { Controller, Get, Query } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Get('/')
async getUser(@Query('uid') uid: string): Promise<User> {
// xxxx
}
}
下面是从 API 获取的代码示例:
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/')
async getUser(): Promise<User> {
const query = this.ctx.query;
// xxxx
}
}
注意: 当 Query String 中的 key 重复时,ctx.query
只取 key 第一次出现时的值,后面再出现的都会被忽略。比如GET /user?uid=1&uid=2
通过ctx.query
拿到的值是{ uid: '1' }
。
Body
虽然我们可以通过 URL 传递参数,但是还是有诸多限制:
- 浏览器中会对 URL 的长度有所限制,参数过多则无法传递
- 服务器经常会将访问的完整 URL 记录到日志文件中,一些敏感数据通过 URL 传递会不安全
HTTP 请求报文中有一个 Body 部分,我们通常在这个部分传递 POST、PUT 和 DELETE 等方法的参数。一般请求中有 body 的时候,客户端(浏览器)会同时发送Content-Type
告诉服务端这次请求的 body 是什么格式的。Web 开发中数据传递最常用的两类格式分别是JSON
和Form
。
midway 内置了 bodyParser 中间件来对这两类格式的请求 body 解析成 object 挂载到ctx.request.body
上。
下面是获取单个 body 的示例:
typescript
import { Controller, Post, Body } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Post('/')
async updateUser(@Body('uid') uid: string): Promise<User> {
// uid 等价于 ctx.request.body.uid
}
}
下面是获取整个 body 的示例
typescript
import { Controller, Post, Body } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Post('/')
async updateUser(@Body() user: User): Promise<User> {
// user 等价于 ctx.request.body
}
}
下面是从 API 获取的示例
typescript
import { Controller, Post, Body } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Post('/')
async updateUser(): Promise<User> {
const body = this.ctx.request.body;
}
}
装饰器可以组合使用,下面是同时使用 @Body 和 @Query 的示例
typescript
@Post('/')
async updateUser(@Body() user: User, @Query('pageIdx') pageIdx: number): Promise<User> {
// user 从 body 获取
// pageIdx 从 query 获取
}
值得注意的是,midway 对 bodyParser 中间件设置了一些默认参数,配置好之后拥有以下特性:
- 当请求的 Content-Type 为
application/json
,application/json-patch+json
,application/vnd.api+json
和application/cspreport
时,会按照 json 格式对请求 body 进行解析,并限制 body 最大长度为1mb
。 - 当请求的 Content-Type 为
application/x-www-form-urlencoded
时,会按照 form 格式对请求 body 进行解析,并限制 body 最大长度为1mb
。 - 如果解析成功,body 一定会是一个 Object(可能是一个数组)。
Router Params
如果路由上使用:xxx
的格式来声明路由,那么参数可以通过ctx.params
获取到。
下面是从装饰器 @Params
中获取的示例
typescript
import { Controller, Get, Param } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Get('/:uid')
}
下面是从 API 中获取的示例
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/:uid')
async getUser(): Promise<User> {
const params = this.ctx.params;
}
}
Header
除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取。
ctx.headers
,ctx.header
,cts.request.headers
,ctx.request.header
:这几个方法是等价的,都是获取整个 header 对象ctx.get(name)
,ctx.request.get(name)
:获取请求 header 中的一个字段的值,如果这个字段不存在,会返回空字符串- 推荐使用
ctx.get(name)
,因为它会自动处理大小写
下面是从装饰器 @Header
中获取的示例
typescript
import { Controller, Get, Headers } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Get('/:uid')
async getUser(@Headers('cache-control') cacheSetting: string): Promise<User> {
// ...
}
}
下面是从 API 中获取的示例
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/:uid')
async getUser(): Promise<User> {
const cacheSetting = this.ctx.get('cache-control');
}
}
Cookie
HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie
。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。
通过ctx.cookies
,我们可以在 Controller 中便捷、安全的设置和读取 Cookie。
typescript
import { Inject, Controller, Get, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class HomeController {'
@Inject()
ctx: Context;
@Get('/')
async home() {
// set cookie
this.ctx.cookies.set('foo', 'bar', { encrypt: true });
// get cookie
this.ctx.cookies.get('foo', { encrypt: true} );
}
}
Cookie 虽然在 HTTP 中只是一个头,但是通过foo=bar; foo1=bar1;
的格式可以设置多个键值对。
Session
通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。
框架内置了 Session 插件,给我们提供了ctx.session
来访问或者修改当前用户 Session。
typescript
import { Inject, Controller, Get, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
// 获取 Session 上的内容
const userId = this.ctx.session.userId;
const posts = await this.ctx.service.post.fetch(userId);
// 修改 Session 的值
this.ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
}
}
Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为null
typescript
ctx.session = null;
设置 HTTP 响应
设置返回值
绝大多数的数据都是通过 body 发送给请求方的,和请求中的 body 一样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进行解析。作为一个 RESTFUL 的 API 接口 controller,我们通常会返回 Content-Type 为application/json
格式的 body,内容是一个 JSON 字符串。
在 midway 中我们可以简单的使用return
来返回数据。
typescript
import { Controller, Get, HttpCode } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
// 返回字符串
return 'Hello Midwayjs!';
// 返回 json
return {
a: 1,
b: 2,
};
// 返回 html
return '<html><h1>Hello</h1></html>';
// 返回 stream
return fs.createReadStream('./good.png');
}
}
也可以使用 koa 原生的 API
typescript
import { Controller, Get, HttpCode } from '@midwayjs/core';
@GetController
export class HomeController {
@Get('/')
async home() {
// 返回字符串
this.ctx.body = 'Hello Midwayjs!';
// 返回 json
this.ctx.body = {
a: 1,
b: 2,
};
// 返回 html
this.ctx.body = '<html><h1>Hello</h1></html>';
// 返回 stream
this.ctx.body = fs.createReadStream('./good.png');
}
}
设置状态码
默认情况下,响应的状态码 总是200,我们可以通过在处理程序层添加@HttpCode
装饰器或者通过 API 来轻松更改此行为。
当发送错误时,如4xx/5xx
,可以使用 异常处理 抛出错误的方式实现。
下面是使用装饰器 @HttpCode(201)
设置的示例
typescript
import { Controller, Get, HttpCode } from '@midway`js/core';
@Controller('/')
export class HomeController {
@Get('/')
@HttpCode(201)
async home() {
return 'Hello Midwayjs!';
}
}
下面是使用 API 设置的示例
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.status = 201;
// ...
}
}
设置响应头
midway 提供@SetHeader
装饰器或者通过 API 来简单的设置自定义响应头。
下面是使用装饰器@SetHeader
来设置的示例
typescript
import { Controller, Get, SetHeader } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@SetHeader('x-bbb', '123')
async home() {
return 'Hello Midwayjs!';
}
}
当有多个响应头需要修改的时候,我们可以直接传入对象
typescript
import { Controller, Get, SetHeader } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@SetHeader({
'x-bbb': '123',
'x-ccc': '234',
})
async home() {
return 'Hello Midwayjs!';
}
}
下面是使用 API 设置的示例
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.set('x-bbb', '123');
// ...
}
}
重定向
如果需要简单的将某个路由重定向到另一个路由,可以使用@Redirect
装饰器。@Redirect
装饰器的参数为一个跳转的 URL,以及一个可选的状态码,默认跳转的状态码为302
。
此外,也可以通过 API 来跳转。
下面是使用装饰器@Redirect
进行重定向的示例。
typescript
import { Controller, Get, Redirect } from '@midwayjs/core';
@Controller('/')
export class LoginController {
@Get('/login_check')
async check() {
// TODO
}
@Get('/login')
@Redirect('/login_check')
async login() {
// TODO
}
@Get('/login_another')
@Redirect('/login_check', 302)
async loginAnother() {
// TODO
}
}
下面是使用 API 设置的示例
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.redirect('/login_check');
// ...
}
}
响应类型
midway 提供了@ContentType
装饰器用于设置响应类型,我们也可以通过 API 来设置。
下面是使用装饰器 @ContentType
装饰器设置的示例
typescript
import { Controller, Get, ContentType } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@ContentType('html')
async login() {
return '<body>hello world</body>';
}
}
下面是使用 API 设置的示例
typescript
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.type = 'html';
// ...
}
}
全局路由前缀
需要在src/config/config.default
配置中配置。
typescript
export default {
koa: {
globalPrefix: '/v1'
}
};
配置后,所有的路由都会自动增加该前缀。如果有特殊路由不需要,可以使用装饰器参数忽略。
下面是 Controller 级别的忽略示例
typescript
@Controller('/api', {ignoreGlobalPrefix: true})
export class HomeController {
// ...
}
下面是路由级别的忽略示例
typescript
@Controller('/')
export class HomeController {
// 该路由不会忽略
@Get('/', {})
async homeSet() {
}
// 该路由会忽略全局前缀
@Get('/bbc', {ignoreGlobalPrefix: true})
}
路由优先级
明确的路由优先级最高,长的路由优先级高,通配的优先级最低
Web 中间件(中间件基础理论)
前面在对初始化的项目进行分析的时候,我们已经对中间件有了基本的了解,这里仅简要记录一下中间件的使用方法。
Web 中间件在写完之后,需要应用到请求流程之中。
根据应用到的位置,分为两种:
- 全局中间件,所有的路由都会执行的中间件,比如 cookie、session 等
- 路由中间件,单个/部分路由会执行的中间件,比如某个路由的前置校验,数据处理等
他们之间的关系如下图所示
全局中间件
全局中间件需要在应用启动前,加入到当前框架的中间件列表中,useMiddleware
方法,可以把中间件加入到中间件列表中。具体参考我们初始化项目中 src/configuration.ts 中对 ReportMiddleware 中间件的使用。
typescript
async onReady() {
this.app.useMiddleware(ReportMiddleware);
// 我们可以同时添加多个中间件
this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]);
}
路由中间件
@Controller
装饰器的第二个参数,可以让我们方便的在某个路由分组之上添加中间件。
typescript
import { Controller } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.`middleware';
@Controller('/', { middleware: [ ReportMiddleware ]})
export class HomeController {
}
midway 同时也在@Get
、@Post
等路由装饰器上都提供了 middleware 参数,方便对单个路由做中间件拦截。
服务(Service 层基础理论)
在业务中,只有控制器(Controller)的代码是不够的,一般来说会有一些业务逻辑被抽象到一个特定的逻辑单元中,我们一般称为服务(Service)。
提供这个抽象有以下几个好处:
- 保持 Controller 中的逻辑更加简洁
- 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用
- 将逻辑和展现分离,更容易编写测试用例
创建服务
服务的文件一般存放到src/service
目录中,添加一个 user 服务如下:
typescript
import { Provide } from '@midwayjs/core';
@Provide()
export class UserService {
async getUser(id: number) {
return {
id,
name: 'Harry',
age: 18,
};
}
}
使用服务
在 Controller 处,我们需要来调用这个服务。传统的代码写法,我们需要初始化这个 Class(new),然后将实例放在需要调用的地方。在 Midway 中,我们不需要这么做,只需要编写 midway 提供的"依赖注入"的代码写法。
typescript
import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';
import { UserService } from '../service/user';
@Controller('/api/user')
export class APIController {
@Inject()
userService: UserService;
@Get('/')
async getUser(@Query('id') uid) {
const user = await this.userService.getUser(uid);
return { success: true, message: 'OK', data: user};
}
}
使用服务的过程分为下面几个步骤:
- 使用
@Provide
装饰器暴露我们的服务 - 在调用的代码处,使用
@Inject
注入我们的服务 - 调用注入服务,执行对应的方法
Midway 的核心"依赖注入"容器会自动关联我们的控制器(Controller)和服务(Service),在运行过程中会自动初始化所有的代码,你无需手动初始化这些 Class。
守卫
从 v3.6.0 开始,Midway 提供守卫能力。
守卫会根据运行时出现的某些条件(例如权限、角色、访问控制列表等)来确定给定的请求是否由路由处理程序处理。
普通的应用程序中,一般会在中间件中处理这些逻辑,但是中间件的逻辑过于通用,同时也无法很优雅的去和路由方法进行结合,为此我们在中间件之后,进入路由方法之前设计了守卫,可以方便的进行方法鉴权等处理。
守卫会在中间件之后,路由方法之前执行。
编写守卫
一般情况下,我们会在src/guard
文件夹中编写守卫。
创建一个src/guard/auth.guard.ts
,用于验证路由是否能被用户访问。
midway 使用@Guard
装饰器标识守卫,示例代码如下:
typescript
import { IMiddleware, Guard, IGuard } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Guard()
export class AuthGuard implements IGuard<Context> {
async canActivate(context: Context, supplierClz, methodName: string): Promise<boolean> {
// ...
}
}
canActivate
方法用于在请求中验证是否可以访问后续的方法,当返回 true 时,后续的方法会被执行,当canActivate
返回 false 时,会抛出 403 错误码。
使用守卫
全局守卫
全局守卫需要在应用启动前加入到当前框架的守卫列表中,useGuard
方法,可以把守卫加入到守卫列表中。
typescript
// src/configuration.ts
import { AuthGuard } from './guard/auth.guard';
async onReady() {
// 添加单个守卫
this.app.useGuard(AuthGuard);
// 添加多个守卫
this.app.useGuard([AuthGuard, Auth2Guard]);
}
总结与展望
文章总结
这篇文章记录了我初次阅读 midway 中文官方文档所学习的知识,在学习过程中,我们了解了 midway 的简介,使用脚手架快速创建了我们的第一个 midway 应用,并且对应用中 src 文件夹下的代码进行分析,随后学习 midway 如下的基础理论知识:
- 路由和控制器(Controller 层理论)
- Web 中间件(middleware 中间件理论)
- 服务(Service 层理论)
- 守卫(Guard)
仅仅学习完上述的内容,还不足以满足我们实际的项目开发,比如 midway 连接 mysql 数据库这一部分就还没学(个人觉得官方文档对数据持久化这一块描述得不是很详细)。至于实际的项目开发,为了控制文章的篇幅,将放到下一篇文章中进行研究。
下一阶段目标
想要将本文中所学的内容融会贯通,莫过于做一个微项目了,下一阶段的目标就是进行项目实战,在我们初始化的项目上实现我们的目标业务。