Web middleware
Web middleware is a function called before and after (partially). Middleware functions can access request and response objects.
Different upper-layer web frameworks have different middleware forms. Midway standard middleware is based on the onion ring model. Express, on the other hand, is a traditional queue model.
Koa and EggJs can be executed** before and after the **controller. In Express, the middleware can only be called before the controller, which will be introduced separately in Express chapters.
For the following code, we will take @midwayjs/koa
as an example.
Writing middleware
In general, we will write Web middleware in the src/middleware
folder.
Create a src/middleware/report.middleware.ts
. In this web middleware, we print the time when the controller (Controller) executes.
➜ my_midway_app tree
.
├── src
│ ├── controller
│ │ ├── user.controller.ts
│ │ └── home.controller.ts
│ ├── interface.ts
│ ├── middleware ## middleare directory
│ │ └── report.middleware.ts
│ └── service
│ └── user.service.ts
├── test
├── package.json
└── tsconfig.json
Midway uses the @Middleware
decorator to identify the middleware. The complete middleware sample code is as follows.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// Logic executed before the controller
const startTime = Date.now();
// Execute the next Web middleware and finally execute to the controller.
// Here you can get the return value of the next middleware or controller.
const result = await next();
// Logic executed after the controller
console.log(Date.now() - startTime);
// Returns the result to the previous middleware
return result;
};
}
static getName(): string {
return 'report';
}
}
In short, await next()
represents the next logic to be executed, which generally represents the controller execution. Before and after execution, we can perform some printing and assignment operations, which is also the biggest advantage of the onion ring model.
Note that Midway finishes the traditional onion model so that it can obtain the return value of the next middleware. At the same time, you can also return the result of this middleware to the previous middleware by using the return
method.
The static getName
method here is used to specify the name of the middleware to facilitate troubleshooting.
Use middleware
After the Web middleware is written, it needs to be applied to the request process.
According to the location of the application, there are two types:
-
- Global middleware, middleware that all routes will execute, such as cookie, session, etc.
-
- Routing middleware, middleware that a single/partial route will execute, such as pre-check of a route, data processing, etc.
The relationship between them is generally:
Routing middleware
After writing the middleware, we need to apply it to each controller route. @Controller
the second parameter of the decorator, which allows us to easily add middleware to a routing group.
import { Controller } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';
@Controller('/', { middleware: [ ReportMiddleware ] })
export class HomeController {
}
Midway also provides middleware parameters on route decorators such as @Get
and @Post
to facilitate middleware interception of a single route.
import { Controller, Get } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';
@Controller('/')
export class HomeController {
@Get('/', { middleware: [ ReportMiddleware ]})
async home() {
}
}
Global middleware
The so-called global middleware is the Web middleware that takes effect on all routes.
We need to add middleware to the middleware list of the current framework before the application starts. useMiddleware
method, we can add middleware to the middleware list.
// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
this.app.useMiddleware(ReportMiddleware);
}
}
You can add multiple middleware at the same time.
async onReady() {
this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]);
}
Ignore and match routes
When middleware is executed, we can add logic that routes ignore.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// ...
};
}
ignore(ctx: Context): boolean {
// The following route will ignore this middleware
return ctx.path === '/'
|| ctx.path === '/api/auth'
|| ctx.path === '/api/login';
}
static getName(): string {
return 'report';
}
}
Similarly, you can also add matching routes. Only matching routes will execute the middleware. The ignore
and match
only take effect.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// ...
};
}
match(ctx: Context): boolean {
// The following matching route will execute this middleware
if (ctx.path === '/api/index') {
return true;
}
}
static getName(): string {
return 'report';
}
}
In addition, match
and ignore
can also be ordinary strings or regular expressions, and their array forms.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
// string
match = '/api/index';
// regular
match = /^\/api/;
// array
match = ['/api/index', '/api/user', /^\/openapi/, ctx => {
if (ctx.path === '/api/index') {
return true;
}
}];
}
We can also modify properties during the initialization phase, such as:
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
// Configuration of a middleware
@Config('report')
reportConfig;
@Init()
async init() {
// merge some rules dynamically
if (this. reportConfig. match) {
this.match = ['/api/index', '/api/user'].concat(this.reportConfig.match);
} else if (this. reportConfig. ignore) {
this.match = [].concat(this.reportConfig.ignore);
}
}
}
Reuse middleware
The essence of middleware is function. Function can pass different configurations to reuse middleware, but it is difficult to implement in class scenario. Midway provides createMiddleware
method to help create different middleware functions in class scenario.
You can use createMiddleare
to reuse in useMiddleware
phase.
// src/configuration.ts
import { App, Configuration, createMiddleare } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// Add ReportMiddleware middleware
this.app.useMiddleware(ReportMiddleware);
// Add a ReportMiddleware with different parameters
this.app.useMiddleware(createMiddleare(ReportMiddleware, {
text: 'abc'
}, 'anotherReportMiddleare'));
}
}
We can get this parameter in the middleware to execute different logic.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
initData = 'text1';
resolve(_, options?: {
text: string;
}) {
return async (ctx: Context, next: NextFunction) => {
this.ctx.setAttr('data', options?.text || this.initData);
return await next();
};
}
}
createMiddleare
method is defined as follows, containing three parameters.
function createMiddleware(middlewareClass: new (...args) => IMiddleware, options, name?: string);
Parameters | Description |
---|---|
middlewareClass | Middleware class |
options | Passed custom parameters |
name | Optional, middleware name |
options
can pass custom functions of middleware, which can be processed by the logic.
name
field is used for sorting and displaying middleware, and generally a string different from the original middleware name is selected.
createMiddleare
method can also be used in routing middleware.
import { Controller, Get, createMiddleware } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';
const anotherMiddleware = createMiddleware(ReportMiddleware, {
// ...
});
@Controller('/')
export class HomeController {
@Get('/', {
middleware: [anotherMiddleware],
})
async home() {}
}
Note that the decorator will be loaded before the framework is started. At this time, the parameters of createMiddleware
cannot be obtained from the framework configuration and are generally fixed object values.
Function middleware
Midway still supports the form of function middleware and can be added to the middleware list using useMiddleware
.
// src/middleware/another.middleware.ts
export async function fnMiddleware(ctx, next) {
// ...
await next();
// ...
}
// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
import { fnMiddleware } from './middleware/another.middleware';
@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware, fnMiddleware]);
}
}
In this way, many koa tripartite middleware in the community can be easily accessed.
Use community middleware
Let's take koa-static
as an example.
In the koa-static
documentation, it is written like this.
const Koa = require('koa');
const app = new Koa();
app.use(require('koa-static')(root, opts));
Then, require('koa-static')(root, opts)
is actually the returned middleware method, we can export it directly and call useMiddleware
.
async onReady() {
// add middleware
this.app.useMiddleware(require('koa-static')(root, opts));
}
If the middleware supports importing on routes, for example:
const Koa = require('koa');
const app = new Koa();
app.get('/controller', require('koa-static')(root, opts));
We can also treat middleware as ordinary functions and place them in decorator parameters.
const static Middleware = require('koa-static')(root, opts);
//...
class HomeController {
@Get('/controller', {middleware: [staticMiddleware]})
async getMethod() {
//...
}
}
It can also be used as a routing method body.
const static Middleware = require('koa-static')(root, opts);
//...
class HomeController {
@Get('/controller')
async getMethod(ctx, next) {
//...
return static Middleware(ctx, next);
}
}
There are many ways to write three-party middleware, and the above are just the most basic ways to use it.
Get the middleware name
Each middleware should have a name. By default, the name of the class middleware will be obtained according to the following rules:
-
- When the static method of
getName()
exists, take its return value as the name
- When the static method of
-
- If there is no static method of
getName()
, the class name will be used as the middleware name.
- If there is no static method of
A well-recognized middleware name plays a big role in manually sorting or debugging code.
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
// ...
static getName(): string {
return 'report'; // Middleware name
}
}
Function middleware is similar. The defined method name is the name of middleware, such as the following fnMiddleware
.
export async function fnMiddleware(ctx, next) {
// ...
await next();
// ...
}
If the third-party middleware exports an anonymous middleware function, you can use _name
to add a name.
const fn = async (ctx, next) => {
// ...
await next();
// ...
};
fn._name = 'fnMiddleware';
We can use getMiddleware().getNames()
to obtain all middleware names in the current middleware list.
// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
import { fnMiddleware } from './middleware/another.middleware';
@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware, fnMiddleware]);
// output
console.log(this.app.getMiddleware().getNames());
// => report, fnMiddleware
}
}
Middleware sequence
Sometimes, we need to modify the order of middleware in components or applications.
Midway provides insert
API operations to facilitate you to quickly adjust middleware.
We need to use the getMiddleware()
method to obtain the middleware list and then operate on it.
// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// Add middleware to the front
this.app.getMiddleware().insertFirst(ReportMiddleware);
// Adding middleware to the back is equivalent to useMiddleware
this.app.getMiddleware().insertLast(ReportMiddleware);
// After adding middleware to middleware named session
this.app.getMiddleware().insertAfter(ReportMiddleware, 'session');
// Before adding middleware to middleware named session
this.app.getMiddleware().insertBefore(ReportMiddleware, 'session');
}
}
Common examples
Get request scope instance in middleware
Due to the particularity of the life cycle of Web middleware, it will be loaded (bound) to the route before the application request, so it cannot be associated with the request. The scope of the middleware class is fixed as a singleton (Singleton).
Because the middleware instance is a single instance, the instances injected in the middleware are not bound to the request, ctx cannot be obtained, and @Inject()
cannot be used to inject the instance of the request scope. Only the Singleton instances can be obtained.
For example, the following code is wrong.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
@Inject()
userService; // The instance and context injected here are not bound and ctx cannot be obtained.
resolve() {
return async (ctx: Context, next: NextFunction) => {
// TODO
await next();
};
}
}
If you want to get an instance of the request scope, you can use the method obtained from the request scope container ctx.requestContext
, as follows.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
const userService = await ctx.requestContext.getAsync<UserService>(UserService);
// TODO userService.xxxx
await next();
};
}
}
Unified return data structure
For example, all data returned in the /api
uses a unified structure to reduce duplicate code in the Controller.
We can add a middleware code similar to the following.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
const result = await next();
return {
code: 0,
msg: 'OK',
data: result
}
};
}
match(ctx) {
return ctx.path.indexOf('/api') !== -1;
}
}
The preceding code is only the code that is returned with the correct logic. If you want to return an incorrect package, you can use Filter.
About the case where middleware returns null
Under koa/egg, if the middleware returns a null value, the status code will change to 204. If you need to return other status codes (such as 200), you need to explicitly assign additional status codes in the middleware.
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
const result = await next();
if (result === null) {
ctx.status = 200;
}
return {
code: 0,
msg: 'OK',
data: result
}
};
}
match(ctx) {
return ctx.path.indexOf('/api') !== -1;
}
}