如何使用Express Middleware

388 阅读8分钟

中间件是一种技术,它允许软件开发者在调用Express路线时使特定的代码运行。中间件模式使得在服务器接收请求和发送响应之间执行代码成为可能。因此被称为中间件,因为它生活在中间。Express使用中间件将行为应用于请求和响应对象。Express中间件可以做一些事情,比如将信息附加到响应中,检查请求,将请求和响应传递给不同的中间件,或者执行一些其他逻辑。任何可以在JavaScript函数中完成的事情都可以在Express中应用于中间件。


app.use()

当你在Express项目中看到熟悉的**app.use()语法时,你就知道一个中间件正在被使用。它的工作方式是在app.use()**中传递一个回调函数,而这个回调函数将在每一个请求中被执行。让我们看看一个超级简单的中间件的例子。相关的代码在下面突出显示。

const express = require('express');
const app = express();

const PORT = process.env.PORT || 4001;

app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});

app.use(myMiddleware);

app.get('/', (req, res, next) => {
    res.send('You have reached the home page');
});

function myMiddleware(req, res, next) {
    console.log('Just ran myMiddleware!');
}

通过用node app.js启动服务器,然后三次访问/主页,我们可以看到中间件在每次页面请求时都会启动,并且每次都会在控制台中登录出 "刚刚运行了myMiddleware!"的信息。

PS C:\node\express> node app.js
Server is listening on port 4001
Just ran myMiddleware!
Just ran myMiddleware!
Just ran myMiddleware!

上面的代码将回调分成了一个单独的函数。这只是为了学习的目的。你通常会使用一个速记,或箭头,函数语法,这将看起来更像这里的代码。

const express = require('express');
const app = express();

const PORT = process.env.PORT || 4001;

app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});

app.use((req, res, next) => {
    console.log('Just ran myMiddleware!');
    next();
});

app.get('/', (req, res, next) => {
    res.send('You have reached the home page');
});

next()

你可能已经注意到,上面的中间件的重构版本也有一个**next()**函数调用。这到底是怎么回事呢?next()函数在Express中间件中其实是非常重要的。Express将自己描述为一系列的中间件函数调用,而next()函数是保持应用流从一个中间件到下一个中间件的原因。这使得Express不仅完美地实现了与各种终端的路由通信,而且我们还可以通过实现必要的中间件来执行我们需要的应用逻辑。


堆叠中间件

你可以根据需要将应用程序的请求-响应周期传递给尽可能多的中间件函数,以完成所需的工作。让我们看一个如何在Express中堆叠中间件的例子。

const express = require('express');
const app = express();

const PORT = process.env.PORT || 4001;

app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});

app.use((req, res, next) => {
    console.log("It's time for planting peppers.");
    next();
});

app.get('/pepper/:type', (req, res, next) => {
    console.log("The gardener is planting a seed!");
    next();
});

app.get('/pepper/:type', (req, res, next) => {
    console.log(`The gardener has planted some ${req.params.type} peppers.`);
    res.status(200).send();
    next()
});

app.get('/pepper/:type', (req, res, next) => {
    console.log("Planting is complete!");
    next();
});

访问http://localhost:4001/pepper/hot

It's time for planting peppers.
The gardener is planting a seed!
The gardener has planted some hot peppers.
Planting is complete!

在上面的代码中,路由是按照它们在文件中出现的顺序被调用的,前提是前一个路由调用了 next() ,从而将控制权传递给下一个中间件。请注意,每个中间件都确保在它的末尾调用 next() 函数。一个Express中间件有三个参数是reqresnext。顺序由一组回调函数表示,在每个中间件完成其目的后,依次调用。next()函数应该总是在中间件主体的最后部分被明确地调用。这就是Express如何将请求的处理和响应的构建移交给堆栈中的下一个中间件。


对(req, res, next)的进一步观察

Express中间件的函数签名(req, res, next),实际上与我们一直在使用的Express Routes的格式完全相同。这是因为Express路由实际上也是中间件!Express应用程序中的每一个路由也是中间件。Express应用程序中的每一个路由也是一个中间件函数,处理堆栈中那一部分的请求和响应对象。Express路由也能够发送响应体和状态码,以及关闭连接。这是可能的,因为Express路由是中间件,所有Express中间件函数都可以访问请求、响应和堆栈中的下一个中间件。


路由特定的中间件

函数的可选参数被放在方括号([])中,当我们看app.use()的函数签名时,我们会看到以下内容。

app.use([path,] callback [, callback...])

这意味着**app.use()**可以接受一个可选的路径参数作为其第一个参数。如果指定了这个参数,那么中间件就只为该特定路径运行。这很重要,因为到目前为止,我们只看到了在每个请求和响应上运行的中间件,当然我们可能不希望这样。下面这个小程序同时使用了全局中间件和特定路径的中间件,每一个都被强调了。

const express = require('express');
const app = express();

app.use(express.static('public'));

const PORT = process.env.PORT || 4001;

const bagOfPeppers = {
    mystery: {
        number: 4
    },
    yummy: {
        number: 5
    },
    lemondream: {
        number: 25
    },
    greenbell: {
        number: 3
    },
    jalapeno: {
        number: 1
    }
};

// Global middleware
app.use((req, res, next) => {
    console.log(`${req.method} Request Received`);
    next();
});

// Route specific middleware
app.use('/peppers/:pepperName', (req, res, next) => {
    const pepperName = req.params.pepperName;
    if (!bagOfPeppers[pepperName]) {
        console.log('Response Sent');
        return res.status(404).send('Pepper with that name does not exist');
    }
    req.pepper = bagOfPeppers[pepperName];
    req.pepperName = pepperName;
    next();
});

// GET http://localhost:4001/peppers/
app.get('/peppers/', (req, res, next) => {
    res.send(bagOfPeppers);
    console.log(bagOfPeppers);
});

// POST http://localhost:4001/peppers/
app.post('/peppers/', (req, res, next) => {
    let bodyData = '';
    req.on('data', (data) => {
        bodyData += data;
    });

    req.on('end', () => {
        const body = JSON.parse(bodyData);
        const pepperName = body.name;
        if (bagOfPeppers[pepperName] || bagOfPeppers[pepperName] === 0) {
            return res.status(400).send('Pepper with that name already exists!');
        }
        const numberOfPeppers = Number(body.number) || 0;
        bagOfPeppers[pepperName] = {
            number: numberOfPeppers
        };
        res.send(bagOfPeppers);
        console.log(bagOfPeppers);
    });
});

// GET http://localhost:4001/peppers/yummy
app.get('/peppers/:pepperName', (req, res, next) => {
    res.send(req.pepper);
    console.log(bagOfPeppers);
});

// POST http://localhost:4001/peppers/yummy/add
app.post('/peppers/:pepperName/add', (req, res, next) => {
    let bodyData = '';
    req.on('data', (data) => {
        bodyData += data;
    });

    req.on('end', () => {
        const numberOfPeppers = Number(JSON.parse(bodyData).number) || 0;
        req.pepper.number += numberOfPeppers;
        res.send(req.pepper);
        console.log(bagOfPeppers);
    });
});

// POST http://localhost:4001/peppers/reaper/remove
app.post('/peppers/:pepperName/remove', (req, res, next) => {
    let bodyData = '';
    req.on('data', (data) => {
        bodyData += data;
    });

    req.on('end', () => {
        const numberOfPeppers = Number(JSON.parse(bodyData).number) || 0;
        if (req.pepper.number < numberOfPeppers) {
            return res.status(400).send('Not enough peppers in the jar to remove!');
        }
        req.pepper.number -= numberOfPeppers;
        res.send(req.pepper);
        console.log(bagOfPeppers);
    });
});

// DELETE http://localhost:4001/peppers/reaper
app.delete('/peppers/:pepperName', (req, res, next) => {
    req.pepper = null;
    res.status(204).send();
    console.log(bagOfPeppers);
});

// PUT http://localhost:4001/peppers/jalapeno/orange
app.put('/peppers/:pepperName/name', (req, res, next) => {
    let bodyData = '';
    req.on('data', (data) => {
        bodyData += data;
    });

    req.on('end', () => {
        const newName = JSON.parse(bodyData).name;
        bagOfPeppers[newName] = bagOfPeppers[req.pepperName];
        bagOfPeppers[req.pepperName] = null;
        res.send(bagOfPeppers[newName]);
        console.log(bagOfPeppers);
    });
});

app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});

Express中间件中的路径阵列

在指定中间件将在哪个路径上执行时,你可以做的另一件有趣的事情是,提供一个路径数组作为**app.use()**函数的第一个参数。在下面的简单例子中,我们有三个Express路线。通过提供的中间件,当第一条或第二条路由被访问时,一条信息将被记录到控制台。当第三条路由被访问时,没有日志记录发生。

const express = require('express');
const app = express();
const PORT = process.env.PORT || 4001;
app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});

app.use(['/one', '/two'], (req, res, next) => {
    console.log('triggers on /one and /two');
    next();
});

app.get('/one', (req, res, next) => {
    res.send('visiting /one');
});

app.get('/two', (req, res, next) => {
    res.send('visiting /two');
});

app.get('/three', (req, res, next) => {
    res.send('visiting /three');
});

中间件中不止一个回调

Express中的中间件并不局限于每次只有一个回调函数。在使用中间件时,你也可以传递两个或更多的回调,以获得所需的结果。例如,考虑下面这段代码,它在请求处理过程中一次利用了两个中间件。

app.get('/peppers', middleware1, middleware2, getPeppers);

function middleware1(req, res, next) {
    // perform first middleware function
    next();  // move on to the next middleware
    next(err);  // or trigger error handler
}

function middleware2(req, res, next) {
    // perform second middleware function
    next();  // move on to the next middleware
    next(err);  // or trigger error handler
}

使用预先构建的中间件

开源软件的美妙之处在于,在你的编码活动中,几乎所有你需要解决的问题都可能有解决方案。Express中的中间件也不例外。有许多可用的预建中间件,你可以简单地将其放入你的应用程序中,并立即对其加以充分利用。

例如,如果我们每次想建立一个网站时都需要从头开始写一个网络服务器,那么在解决那些已经被更大的社区为我们解决的问题时就会有很多浪费。因此,如果你需要在JavaScript中建立一个Web服务器,那么Express已经帮你解决了。

在Express中作为中间件使用的一个常用包是Morgan。Morgan是一个Node.js和Express的中间件,用于记录HTTP请求和错误,这使得在你的Express应用程序中记录非常容易。在我们目前所写的所有代码中,有很多手动调用console.log()的情况,这可能会使代码变得更难看和容易出错。如果已经有软件可以为你完成这些任务,为什么还要花时间去思考和编写完成普通任务的代码?

这就是你会使用像Morgan这样的东西的地方。在这里,我们通过使用以下代码将其包含在上面的Pepper应用程序的例子中。

const morgan = require('morgan');
app.use(morgan('combined'));

当我们运行所有的测试并在终端检查输出时,我们现在可以看到提出了什么类型的请求,向哪个路径提出的请求,以及处理请求所花的时间。

GET /peppers/ 200 2.783 ms - 147
POST /peppers/ 200 1.469 ms - 148
GET /peppers/yummy 200 0.982 ms - 152
POST /peppers/yummy/add 200 0.623 ms - 157
POST /peppers/reaper/remove 200 0.755 ms - 161
DELETE /peppers/reaper 200 0.784 ms - 156
PUT /peppers/jalapeno/name 200 0.310 ms - 160

我们使用了组合日志格式,但是知道Morgan有以下日志级别也是很有用的。

  • combined':为你的日志提供 Apache标准的组合 格式。
  • 'common': 引用Apache标准的通用 格式。
  • 'dev'。一种彩色编码的(按请求状态)日志格式。
  • ''。比默认格式更短,只包括你期望的请求日志中的几个项目。
  • 'tiny':更短,只有响应时间和一些额外的项目。

这个表格显示了Express中一些最常用的中间件。

body-parser解析HTTP请求正文。
压缩压缩 HTTP 响应。
connect-rid生成唯一的请求ID。
cookie-parser解析cookie头并填充req.cookies。
cookie-session建立基于cookie的会话。
cors使用各种选项启用跨源资源共享(CORS)。
csurf保护免受CSRF攻击。
错误处理程序开发错误处理/调试。
方法覆盖(method-override使用标头覆盖HTTP方法。
morganHTTP请求记录器。
multer处理多部分的表单数据。
响应时间记录HTTP响应时间。
送达favicon提供一个图标。
service-index为一个给定的路径提供目录列表。
服务静态服务静态文件。
会话建立基于服务器的会话(仅限开发)。
超时为HTTP请求的处理设置一个超时周期。
vhost创建虚拟域。

如何使用Express中间件总结

Express 中的中间件可以帮助我们编写更简洁、更可维护的代码。我们已经看到了中间件是如何让我们在许多地方提供功能而不重复代码的。在Express中,我们可以通过路由来提供数据,每个可能的端点都被视为我们应用程序的一个独特的端点。我们看到,我们可以使用 next() 链接这些中间件,以继续到栈中的下一个中间件。此外,我们还了解到,依靠外部的开源中间件可以让我们利用Express网络服务器、Node环境和JavaScript编程语言的力量。