与 midway 的第一次接触

前言

最近老大布置了一个全栈项目给我,前端依旧是老套路,重点是后端方面不再使用去年实习的时候用的 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.tsapi.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.tsnotfound.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.tsconfig.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 Controller
  • middleware:中间件
  • filter:过滤器
  • aspect:拦截器
  • service:服务逻辑目录
  • entitymodel:数据库实体目录
  • 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 开发中数据传递最常用的两类格式分别是JSONForm

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/jsonapplication/json-patch+jsonapplication/vnd.api+jsonapplication/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.headersctx.headercts.request.headersctx.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 中间件在写完之后,需要应用到请求流程之中。

根据应用到的位置,分为两种:

  1. 全局中间件,所有的路由都会执行的中间件,比如 cookie、session 等
  2. 路由中间件,单个/部分路由会执行的中间件,比如某个路由的前置校验,数据处理等

他们之间的关系如下图所示

全局中间件

全局中间件需要在应用启动前,加入到当前框架的中间件列表中,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};
  }
}

使用服务的过程分为下面几个步骤:

  1. 使用@Provide装饰器暴露我们的服务
  2. 在调用的代码处,使用@Inject注入我们的服务
  3. 调用注入服务,执行对应的方法

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 如下的基础理论知识:

  1. 路由和控制器(Controller 层理论)
  2. Web 中间件(middleware 中间件理论)
  3. 服务(Service 层理论)
  4. 守卫(Guard)

仅仅学习完上述的内容,还不足以满足我们实际的项目开发,比如 midway 连接 mysql 数据库这一部分就还没学(个人觉得官方文档对数据持久化这一块描述得不是很详细)。至于实际的项目开发,为了控制文章的篇幅,将放到下一篇文章中进行研究。

下一阶段目标

想要将本文中所学的内容融会贯通,莫过于做一个微项目了,下一阶段的目标就是进行项目实战,在我们初始化的项目上实现我们的目标业务。

相关推荐
AskHarries2 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion3 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder4 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚5 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心5 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴6 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲6 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心6 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
hanglove_lucky7 小时前
本地摄像头视频流在html中打开
前端·后端·html