与 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: '[email protected]',
    };
  }
}

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 数据库这一部分就还没学(个人觉得官方文档对数据持久化这一块描述得不是很详细)。至于实际的项目开发,为了控制文章的篇幅,将放到下一篇文章中进行研究。

下一阶段目标

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

相关推荐
kaixin_learn_qt_ing8 分钟前
Golang
开发语言·后端·golang
炒空心菜菜1 小时前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
wowocpp3 小时前
spring boot Controller 和 RestController 的区别
java·spring boot·后端
后青春期的诗go3 小时前
基于Rust语言的Rocket框架和Sqlx库开发WebAPI项目记录(二)
开发语言·后端·rust·rocket框架
freellf3 小时前
go语言学习进阶
后端·学习·golang
全栈派森5 小时前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse6 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭7 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架7 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱7 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端