本文详细介绍阿里巴巴 Midway 框架的使用方法。作为基于 TypeScript 的渐进式 Node.js 框架,Midway 通过依赖注入容器支持 Web、微服务等多种场景。文章涵盖应用创建、目录结构解析、Controller 路由配置、参数获取(Query/Body/Params)、中间件使用及 Validate 和 Swagger 等核心组件的集成,旨在帮助开发者构建高效、稳定的前端中间件服务。
一、 关于midway
Midway 是由阿里巴巴淘宝前端架构团队研发的 Node.js 框架,其设计理念为渐进式。该框架通过自研的依赖注入容器,结合各类上层模块,能够组合出适用于不同业务场景的解决方案。Midway 基于 TypeScript 开发,融合了面向对象(OOP + Class + IoC)与函数式(FP + Function + Hooks)两种编程范式,并在此基础上支持 Web、全栈、微服务、RPC、Socket 以及 Serverless 等多种应用场景,致力于为用户提供简单、易用且可靠的 Node.js 服务端研发体验。
1. 解决什么痛点
在传统开发模式中,前端直接调用后台服务接口往往会导致对后台数据的过度依赖,或者必须在请求服务后于渲染层进行数据加工,这大大降低了开发效率。此外,在多部门协同开发场景下,接口数据格式难以统一,导致前端数据处理任务加重,并引发诸多沟通问题。
2. 期望达到什么效果
在项目或产品开发中,引入中间件对服务接口进行二次加工或转发,从而为前端提供统一的数据结构。典型的分工如下:数据团队负责开发对外提供服务数据接口;后端团队负责业务逻辑开发,对外提供服务业务逻辑接口;中间层根据前端需求,调用多端不同的服务接口并进行数据拼接与加工,最终向提供加工后的接口;前端团队则专注于数据渲染。
二、创建应用并使用
1. 创建midway应用
执行命令 $npm init midway 以初始化项目。选择 koa-v3 项目进行创建,项目名可自定义,例如 weather-sample。创建完成后,可启动应用进行体验:$ npm run dev。
这将创建一个类似如下结构的文件目录:
. ├── src ## midway 项目源码 │ └── controller ## Web Controller 目录 │ └── home.controller.ts ├── test ├── package.json └── tsconfig.json
该项目包含最基本的文件和目录:src 是 Midway 项目的源码目录,后续所有开发源码均存放于此;test 是项目测试目录,存放所有代码测试文件;package.json 是 Node.js 项目基础的包管理配置文件;tsconfig.json 是 TypeScript 编译配置文件。
2. 认识Midway
2.1 目录结构
• controller:Web Controller 目录 • middleware:中间件目录 • filter:过滤器目录 • aspect:拦截器 • service:服务逻辑目录 • entity 或 model:数据库实体目录 • config:业务的配置目录 • util:工具类存放的目录 • decorator:自定义装饰器目录 • interface.ts:业务的 ts 定义文件
2.2 Controller
控制器通常用于对用户请求参数进行校验、转换,调用复杂的业务逻辑,获取业务结果后进行数据组装并返回。在 Midway 中,控制器还承载了路由能力,每个控制器可提供多个路由,不同路由可执行不同的操作。
import { Controller, Get } from '@midwayjs/decorator';
@Controller('/')
export class WeatherController {
// 这里是装饰器,定义一个路由
@Get('/weather')
async getWeatherInfo(): Promise<string> {
// 这里是 http 的返回,可以直接返回字符串,数字,JSON,Buffer 等
return 'Hello Weather!';
}
}@Controller 装饰器标识这是一个 Web 控制器类型的类,而 @Get 装饰器指定被修饰的 home 方法将暴露为 / 路由,可通过 GET 请求访问。通过访问 /weather 接口返回数据;该方法返回一个字符串,在浏览器中会收到 text/plain 响应类型及 200 状态码。
2.3 路由
上述创建了一个 GET 路由。一般情况下,我们需要其他 HTTP Method,Midway 提供了更多的路由方法装饰器。例如:
import { Controller, Get, Post } from '@midwayjs/decorator';
@Controller('/')
export class HomeController {
@Get('/')
async home() {
return 'Hello Midwayjs!';
}
@Post('/update')
async updateData() {
return 'This is a post method'
}
}Midway 还提供了其他装饰器,包括 @Get、@Post、@Put()、@Del()、@Patch()、@Options()、@Head() 和 @All(),分别表示各自的 HTTP 请求方法。其中 @All 装饰器较为特殊,表示能接受以上所有类型的 HTTP Method。你可以将多个路由绑定到同一个方法上。
@Get('/')
@Get('/main')
async home() {
return 'Hello Midwayjs!';
}返回内容类型将定义的内容放在 src/interface.ts 文件中。例如:
export interface User {
id: number;
name: string;
age: number;
}使用:下方粗下划线处
import { Controller, Get, Query } from "@midwayjs/decorator";
@Controller('/api/user')
export class UserController {
@Get('/')
async getUser(@Query('id') id: string): Promise<User> {
// xxxx
}
}2.4 获取请求参数
请求的数据通常是动态的,会在 HTTP 的不同位置传递,例如常见的 Query、Body 等。
第一种 query:@Query 装饰器若有参数,可传入指定的字符串 key 以获取对应值并赋值给入参;若不传入,则默认返回整个 Query 对象。
// URL = /?id=1
async getUser(@Query('id') id: string) // id = 1
async getUser(@Query() queryData) // {"id": "1"}
如果通过api获取query中的参数
import { Controller, Get, Inject } from "@midwayjs/decorator";
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/')
async getUser(): Promise<User> {
const query = this.ctx.query;
// {
// uid: '1',
// sex: 'male',
// }
}
}注意:当 Query String 中的 key 重复时,ctx.query 仅取 key 第一次出现时的值,后续出现的将被忽略。例如 GET /user?uid=1&uid=2 通过 ctx.query 拿到的值是 { uid: ‘1’ }。
第二种 body:为何使用 body 传递参数? • 浏览器对 URL 长度有限制,若参数过多则无法通过 URL 传递。 • 服务端常将完整 URL 记录到日志文件中,通过 URL 传递敏感数据存在安全隐患。 注意:框架内置了 bodyParser 中间件,将两类格式的请求 body 解析为 object 并挂载到 ctx.request.body 上。HTTP 协议不建议通过 GET、HEAD 方法传递 body,因此无法在 GET、HEAD 方法中按此方法获取内容。框架对 bodyParser 设置了默认参数,配置后具备以下特性: • 当请求的 Content-Type 为 application/json、application/json-patch+json、application/vnd.api+json 和 application/csp-report 时,按 json 格式解析请求 body,并限制 body 最大长度为 1mb。 • 当请求的 Content-Type 为 application/x-www-form-urlencoded 时,按 form 格式解析请求 body,并限制 body 最大长度为 1mb。 • 若解析成功,body 必定是一个 Object(可能是一个数组)。
获取单个 body
• // src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Body } from "@midwayjs/decorator";
@Controller('/user')
export class UserController {
@Post('/')
async updateUser(@Body('uid') uid: string): Promise<User> {
// id 等价于 ctx.request.body.uid
}
}获取整个 body
• // src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Body } from "@midwayjs/decorator";
@Controller('/user')
export class UserController {
@Post('/')
async updateUser(@Body() user: User): Promise<User> {
// user 等价于 ctx.request.body 整个 body 对象
// => output user
// {
// uid: '1',
// name: 'harry',
// }
}
}从 API 获取
• // src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Inject } from "@midwayjs/decorator";
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Post('/')
async getUser(): Promise<User> {
const body = this.ctx.request.body;
// {
// uid: '1',
// name: 'harry',
// }
}
}此外装饰器还可以组合使用,同时获取 query 和 body 参数。
@Post('/')
async updateUser(@Body() user: User, @Query('pageIdx') pageIdx: number): Promise<User> {
// user 从 body 获取
// pageIdx 从 query 获取
}第三种 Params:如果路由上使用 :xxx 格式声明路由,参数可通过 ctx.params 获取。示例:从装饰器获取
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Param } from "@midwayjs/decorator";
@Controller('/user')
export class UserController {
@Get('/:uid')
async getUser(@Param('uid') uid: string): Promise<User> {
// xxxx
}
}示例:从 API 获取
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Inject } from "@midwayjs/decorator";
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/:uid')
async getUser(): Promise<User> {
const params = this.ctx.params;
// {
// uid: '1',
// }
}
}2.5 Web中间件
Web 中间件是在控制器调用之前和之后(部分)调用的函数。中间件函数可以访问请求和响应对象。
import { IMiddleware } from '@midwayjs/core';
import { Middleware } from '@midwayjs/decorator';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
console.log(Date.now() - startTime);
// 返回给上一个中间件的结果
return result;
};
}
static getName(): string {
return 'report';
}
}例如:
export class ErrorMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: Context, next: IMidwayWebNext) => {
try {
await next()
} catch (err: any) {
const errorInter = RestCode.INTERNAL_SERVER_ERROR;
console.info('错误信息' + err.name, err.message, err.status);
const status = err.status || errorInter;
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
const errorMsg = status === errorInter && ctx.app.config.env === 'prod' ?
'Internal Server Error' :
err.message;
ctx.body = {
code: err.name === 'ValidationError' ? RestCode.VALIDATE_ERROR : status,
error: errorMsg.replaceAll('\"',''),
}
if (status === 422) {
ctx.body.detail = err.errors;
}
ctx.status = 200
}
};
}
}全局使用中间件:所有路由都会执行的中间件,例如 cookie、session 等。
// src/configuration.ts
import { App, Configuration } from '@midwayjs/decorator';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
@Configuration({
imports: [koa]
// ...
})
export class AutoConfiguration {
@App()
app: koa.Application;
async onReady() {
this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]);
}
}路由使用中间件:单个或部分路由会执行的中间件,例如某个路由的前置校验、数据处理等。
import { Controller } from '@midwayjs/decorator';
import { ReportMiddleware } from '../middleware/report.middlweare';
@Controller('/', { middleware: [ ReportMiddleware ] })
export class HomeController {
}忽略和匹配路由:在中间件执行时,我们可以添加路由忽略的逻辑。
ignore(ctx: Context): boolean {
// 下面的路由将忽略此中间件
return ctx.path === '/'
|| ctx.path === '/api/auth'
|| ctx.path === '/api/login';
}同理,也可以添加匹配的路由,只有匹配到的路由才会执行该中间件。ignore 和 match 同时只有一个会生效。
match(ctx: Context): boolean {
// 下面的匹配到的路由会执行此中间件
if (ctx.path === '/api/index') {
return true;
}
}2.6 组件使用
参数校验:Midway 提供了 Validate 组件。配合 @Validate 和 @Rule 装饰器,可快速定义校验规则,帮助用户减少重复代码。注意:从 v3 开始,@Rule 和 @Validate 装饰器从 @midwayjs/validate 中导出。 1、 安装依赖:$ npm i @midwayjs/validate@3 –save 2、 开启组件:在 configuration.ts 中增加组件。
import * as validate from ‘@midwayjs/validate';
import { join } from ‘path';
@Configuration({
imports: [ validate],
importConfigs: [join(__dirname, ‘./config')],
})
}3、 定义检查规则:为方便后续处理,将 user 放到 src/dto 目录中。例如:
// src/dto/user.ts
import { Rule, RuleType } from ‘@midwayjs/validate';
export class UserDTO {
@Rule(RuleType.number().required())
id: number;
@Rule(RuleType.string().required())
firstName: string;
@Rule(RuleType.string().max(10))
lastName: string;
@Rule(RuleType.number().max(60))
age: number;
}4、 应用:定义完类型后,可直接在业务代码中使用。开启校验能力还需要 @Validate 装饰器。
// src/controller/home.ts
import { Controller, Get, Provide } from ‘@midwayjs/decorator';
import { UserDTO } from ‘./dto/user';
@Controller(‘/api/user')
export class HomeController {
@Post(‘/')
async updateUser(@Body() user: UserDTO ) {
// user.id
}
}Swagger-ui:基于最新的 OpenAPI 3.0.3 实现了新版的 Swagger 组件。 1、 安装依赖: npm install @midwayjs/swagger@3 --save npm install swagger-ui-dist --save-dev 2、 开启组件:
import { Configuration } from ‘@midwayjs/decorator';
import * as swagger from ‘@midwayjs/swagger';
@Configuration({
imports: [
{
component: swagger,
enabledEnvironment: [‘local'] //只在 local 环境下启用
}]})
export class MainConfiguration {
}然后启动项目,访问地址: • UI: http://127.0.0.1:7001/swagger-ui/index.html • JSON: http://127.0.0.1:7001/swagger-ui/index.json
2.7 服务(service)
在业务中,仅有控制器(Controller)的代码是不够的,通常会将部分业务逻辑抽象到特定的逻辑单元中,称为服务(Service)。

提供这种抽象有以下几个好处: • 保持 Controller 中的逻辑更加简洁。 • 保持业务逻辑的独立性,抽象出来的 Service 可被多个 Controller 重复调用。 • 将逻辑和展现分离,更容易编写测试用例。创建服务:一般会存放到 src/service 目录中。我们来添加一个 user 服务。
import { Provide, App, Inject } from '@midwayjs/decorator';
import { Application } from 'egg';
import { HttpService } from '@midwayjs/axios';
var fs = require('fs');
var path = require('path');
@Provide() //抛出服务
export class UserService {
@App()
app: Application;
@Inject() //依赖注入
httpService: HttpService;
async getUser(options: any) {
const url = 'https://172.30.154.46:9998/samp/v1/auth/login';
const { data } = await this.httpService.post(url, options);
return data;
}
}使用服务
在 Controller 处,我们需要调用这个服务。传统代码写法需初始化 Class(new),并将实例放在需要调用的地方。在 Midway 中,无需如此操作,只需编写提供的“依赖注入”代码写法即可。
import { Inject, Controller, Get, Provide, Query } from '@midwayjs/decorator';
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};
}
}三、写到最后
Midway 框架已在内部使用超过 5 年,是 Node.js 框架,有长期投入和持续维护的团队支持,并在每年大促场景中经过考验,稳定性无需担心。其拥有丰富的组件和扩展能力,例如数据库、缓存、定时任务、进程模型、部署以及 Web、Socket 甚至 Serverless 等新场景的支持。一体化调用方案可方便快捷地与前端页面协同开发,并提供良好的 TypeScript 定义支持。因此在项目中应用 Midway,能够为应用提供更优雅的架构。
到此这篇关于前端中间件Midway的使用的文章就介绍到这了,更多相关Midway的使用内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!