Middleware
12003In the previous chapter, we introduced that Egg is implemented based on Koa, so the middleware format of Egg is the same as that of Koa, both based on the onion model. Every time we write a middleware, it is equivalent to wrapping a layer outside the onion.
Writing Middleware
Syntax
Let's start by writing a simple gzip middleware to understand how to write middleware.
// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');
async function gzip(ctx, next) {
await next();
// After subsequent middleware execution is complete, convert the response body to gzip
let body = ctx.body;
if (!body) return;
if (isJSON(body)) body = JSON.stringify(body);
// Set gzip body and correct response headers
const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
}As you can see, the syntax for the framework's middleware is exactly the same as that of Koa, so any Koa middleware can be directly used by the framework.
Configuration
Generally speaking, middleware will also have its own configuration. In the framework, a complete middleware includes configuration handling. We agree that a middleware is a separate file placed in the app/middleware directory. It needs to exports a regular function that accepts two parameters:
options: The configuration items for the middleware, which the framework will pass asapp.config[${middlewareName}].app: The instance of the current applicationApplication.
Next, we will make a simple optimization to the gzip middleware mentioned above, so that it only performs gzip compression when the size exceeds the configured threshold. We will create a new file gzip.js in the app/middleware directory.
// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');
module.exports = (options) => {
return async function gzip(ctx, next) {
await next();
// After subsequent middleware execution is complete, convert the response body to gzip
let body = ctx.body;
if (!body) return;
// Support options.threshold
if (options.threshold && ctx.length < options.threshold) return;
if (isJSON(body)) body = JSON.stringify(body);
// Set gzip body and correct response headers
const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
};
};Using Middleware
Once the middleware is written, we also need to manually mount it, supporting the following methods:
Using Middleware in the Application
In the application, we can fully load custom middleware through configuration and determine their order.
If we need to load the above gzip middleware, we can simply add the following configuration in config.default.js to enable and configure the middleware:
module.exports = {
// Configure the required middleware, the order of the array is the loading order of the middleware
middleware: ['gzip'],
// Configure the gzip middleware settings
gzip: {
threshold: 1024 // Responses smaller than 1k will not be compressed
}
};This configuration will ultimately be merged into app.config.appMiddleware at startup.
Using Middleware in the Framework and Plugins
The framework and plugins do not support matching middleware in config.default.js, and need to be done as follows:
// app.js
module.exports = app => {
// Count request time at the very beginning of the middleware
app.config.coreMiddleware.unshift('report');
};
// app/middleware/report.js
module.exports = () => {
return async function(ctx, next) {
const startTime = Date.now();
await next();
// Report request time
reportTime(Date.now() - startTime);
};
};Middleware defined at the application layer (app.config.appMiddleware) and the default middleware of the framework (app.config.coreMiddleware) will both be loaded by the loader and mounted to app.middleware.
Using Middleware in the Router
The middleware configured in the above two ways is global and will handle every request. If you only want it to take effect for a single route, you can instantiate and mount it directly in app/router.js, as follows:
module.exports = app => {
const gzip = app.middleware.gzip({ threshold: 1024 });
app.router.get('/needgzip', gzip, app.controller.handler);
};Default Middleware of the Framework
In addition to loading middleware at the application layer, the framework itself and other plugins will also load many middleware. All configuration items for these built-in middleware can be changed by modifying the same-named configuration items in the configuration file. For example, there is a middleware called bodyParser in the framework's built-in middleware list (the framework's loader will convert the delimiters in the filename into camel case variable names). If we want to modify the configuration of bodyParser, we only need to write the following content in config/config.default.js:
module.exports = {
bodyParser: {
jsonLimit: '10mb',
},
};Note: Middleware loaded by the framework and plugins will be loaded before middleware configured at the application layer. Default middleware of the framework cannot be overridden by application layer middleware. If there is a custom middleware with the same name at the application layer, an error will occur at startup.
Using Koa Middleware
It is very easy to introduce Koa middleware into the framework.
Taking koa-compress as an example, when used in Koa:
const koa = require('koa');
const compress = require('koa-compress');
const app = new koa();
const options = { threshold: 2048 };
app.use(compress(options));We will load this Koa middleware in the application according to the framework's specifications:
// app/middleware/compress.js
// The interface exposed by koa-compress (`(options) => middleware`) is consistent with the framework's requirements for middleware
module.exports = require('koa-compress');// config/config.default.js
module.exports = {
middleware: ['compress'],
compress: {
threshold: 2048,
},
};If the Koa middleware used does not conform to the parameter specification, you can handle it yourself:
// config/config.default.js
module.exports = {
webpack: {
compiler: {},
others: {},
},
};
// app/middleware/webpack.js
const webpackMiddleware = require('some-koa-middleware');
module.exports = (options, app) => {
return webpackMiddleware(options.compiler, options.others);
};Common Configuration
Both middleware loaded at the application layer and built-in middleware of the framework support several common configuration items:
enable: Controls whether the middleware is enabled.match: Sets that only requests that meet certain rules will pass through this middleware.ignore: Sets that requests that meet certain rules will not pass through this middleware.
enable
If our application does not need the default bodyParser middleware to parse the request body, we can disable it by configuring enable to false.
module.exports = {
bodyParser: {
enable: false,
},
};match and ignore
The parameters supported by match and ignore are the same, but their effects are completely opposite; match and ignore cannot be configured simultaneously.
If we want gzip to only be enabled for URL requests starting with the /static prefix, we can configure the match option.
module.exports = {
gzip: {
match: '/static',
},
};match and ignore support various types of configuration methods:
- String: When the parameter is of string type, it configures a URL path prefix, and all URLs that match the configured string as a prefix will be matched. Of course, you can also directly use an array of strings.
- Regular Expression: When the parameter is a regular expression, it directly matches URLs that satisfy the regular expression validation.
- Function: When the parameter is a function, the request context will be passed to this function, and the result returned by the function (
true/false) will be used to determine whether it matches.
module.exports = {
gzip: {
match(ctx) {
// Only enable for iOS devices
const reg = /iphone|ipad|ipod/i;
return reg.test(ctx.get('user-agent'));
},
},
};For more information on match and ignore configurations, see egg-path-matching.