如何使用Express.js路由进行基于Promise的错误处理

2021年11月30日00:23:19 发表评论 671 次浏览

扩展后可维护的 Express.js 代码意味着在减少样板文件的同时使通用代码功能更加丰富。在这个Express.js Promise的错误处理教程中,将会带你了解如何启用基于 Promise 的路由代码并在 Express.js 应用程序中集中错误处理和正常结果处理。

如何使用Express.js路由进行基于Promise的错误处理

Express.js 的标语是正确的:它是“一个快速、无偏见、极简主义的 Node.js 网络框架”。尽管当前的JavaScript 最佳实践规定了使用Promise,但 Express.js 默认不支持基于Promise的路由处理程序。

如何进行基于Promise的错误处理?由于许多 Express.js 教程遗漏了这些细节,开发人员经常养成为每条路线复制和粘贴结果发送和错误处理代码的习惯,从而造成技术债务。我们可以使用我们今天介绍的技术来避免这种反模式(及其后果)——我已经在具有数百条路由的应用程序中成功使用了这种技术。

Express.js 路由的典型架构

让我们从一个 Express.js 教程应用程序开始,其中包含一些用于用户模型的路由。

Express.js Promise的错误处理实例:在实际项目中,我们会将相关数据存储在一些数据库中,例如 MongoDB。但就我们的目的而言,数据存储细节并不重要,因此为了简单起见,我们将模拟它们。我们不会简化的是良好的项目结构,这是任何项目成功一半的关键。

通常,Yeoman 可以生成更好的项目骨架,但是对于我们需要的,我们将简单地使用express-generator创建一个项目骨架并删除不必要的部分,直到我们拥有:

bin
  start.js
node_modules
routes
  users.js
services
  userService.js
app.js
package-lock.json
package.json

我们已经减少了与我们的目标无关的剩余文件的行。

这是主要的 Express.js 应用程序文件./app.js

const createError  = require('http-errors');
const express = require('express');
const cookieParser = require('cookie-parser');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/users', usersRouter);
app.use(function(req, res, next) {
  next(createError(404));
});
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.send(err);
});

module.exports = app;

在这里,我们创建了一个 Express.js 应用程序并添加了一些基本的中间件来支持 JSON 使用、URL 编码和 cookie 解析。然后我们添加一个usersRouterfor /users。最后,我们指定如果没有找到路由该怎么做,以及如何处理错误,我们稍后会更改。

启动服务器本身的脚本是/bin/start.js

const app = require('../app');
const http = require('http');

const port = process.env.PORT || '3000';

const server = http.createServer(app);
server.listen(port);

我们/package.json也是准系统:

{
  "name": "express-promises-example",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/start.js"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "express": "~4.16.1",
    "http-errors": "~1.6.3"
  }
}

让我们使用一个典型的用户路由器实现/routes/users.js

const express = require('express');
const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
  userService.getAll()
    .then(result => res.status(200).send(result))
    .catch(err => res.status(500).send(err));
});

router.get('/:id', function(req, res) {
  userService.getById(req.params.id)
    .then(result => res.status(200).send(result))
    .catch(err => res.status(500).send(err));
});

module.exports = router;

它有两条路线:/获取所有用户和/:id通过 ID 获取单个用户。它还使用/services/userService.js,它具有基于Promise的方法来获取此数据:

const users = [
  {id: '1', fullName: 'User The First'},
  {id: '2', fullName: 'User The Second'}
];

const getAll = () => Promise.resolve(users);
const getById = (id) => Promise.resolve(users.find(u => u.id == id));

module.exports = {
  getById,
  getAll
};

在这里,我们避免使用实际的 DB 连接器或 ORM(例如,Mongoose 或 Sequelize),只是模仿使用Promise.resolve(...).

Express.js 路由问题

如何进行基于Promise的错误处理?查看我们的路由处理程序,我们看到每个服务调用都使用重复.then(...).catch(...)回调将数据或错误发送回客户端。

乍一看,这似乎并不严重。让我们添加一些基本的现实世界要求: 我们只需要显示某些错误并省略一般的 500 级错误;还有,我们是否应用这个逻辑必须基于环境。有了这个,当我们的示例项目从它的两条路线发展成一个有 200 条路线的真实项目时,它会是什么样子?

方法一:效用函数

Express.js Promise的错误处理教程:也许我们会创建单独的实用程序函数来处理resolvereject,并将它们应用到 Express.js 路由中的任何地方:

// some response handlers in /utils 
const handleResponse = (res, data) => res.status(200).send(data);
const handleError = (res, err) => res.status(500).send(err);


// routes/users.js
router.get('/', function(req, res) {
  userService.getAll()
    .then(data => handleResponse(res, data))
    .catch(err => handleError(res, err));
});

router.get('/:id', function(req, res) {
  userService.getById(req.params.id)
    .then(data => handleResponse(res, data))
    .catch(err => handleError(res, err));
});

看起来更好:我们不会重复发送数据和错误的实现。但是我们仍然需要在每个路由中导入这些处理程序,并将它们添加到传递给then()和 的每个Promise中catch()

方法二:中间件

另一种解决方案可能是围绕 promise使用 Express.js最佳实践:将错误发送逻辑移动到 Express.js 错误中间件(在 中添加app.js)并使用next回调将异步错误传递给它。我们的基本错误中间件设置将使用一个简单的匿名函数:

app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.send(err);
});

Express.js 明白这是为了错误,因为函数签名有四个输入参数。(它利用了这样一个事实,即每个函数对象都有一个.length属性来描述函数期望的参数数量。)

通过传递错误next看起来像这样:

// some response handlers in /utils 
const handleResponse = (res, data) => res.status(200).send(data);

// routes/users.js
router.get('/', function(req, res, next) {
  userService.getAll()
    .then(data => handleResponse(res, data))
    .catch(next);
});

router.get('/:id', function(req, res, next) {
  userService.getById(req.params.id)
    .then(data => handleResponse(res, data))
    .catch(next);
});

即使使用官方的最佳实践指南,我们仍然需要在每个路由处理程序中使用 JS Promise来解析使用handleResponse()函数并通过传递函数来拒绝next

让我们尝试用更好的方法来简化它。

方法 3:基于 Promise 的中间件

Express.js Promise的错误处理教程:JavaScript 最大的特性之一是它的动态特性。我们可以在运行时向任何对象添加任何字段。我们将使用它来扩展 Express.js 结果对象;Express.js 中间件函数是一个方便的地方。

我们的promiseMiddleware()职能

Express.js Promise的错误处理实例:让我们创建我们的 promise 中间件,这将使我们能够灵活地更优雅地构建我们的 Express.js 路由。我们需要一个新文件/middleware/promise.js

const handleResponse = (res, data) => res.status(200).send(data);
const handleError = (res, err = {}) => res.status(err.status || 500).send({error: err.message});


module.exports = function promiseMiddleware() {
  return (req,res,next) => {
    res.promise = (p) => {
      let promiseToResolve;
      if (p.then && p.catch) {
        promiseToResolve = p;
      } else if (typeof p === 'function') {
        promiseToResolve = Promise.resolve().then(() => p());
      } else {
        promiseToResolve = Promise.resolve(p);
      }

      return promiseToResolve
        .then((data) => handleResponse(res, data))
        .catch((e) => handleError(res, e));  
    };

    return next();
  };
}

在 中app.js,让我们将中间件应用于整个 Express.jsapp对象并更新默认错误行为:

const promiseMiddleware = require('./middlewares/promise');
//...
app.use(promiseMiddleware());
//...
app.use(function(req, res, next) {
  res.promise(Promise.reject(createError(404)));
});
app.use(function(err, req, res, next) {
  res.promise(Promise.reject(err));
});

请注意,我们没有省略错误中间件。它仍然是我们代码中可能存在的所有同步错误的重要错误处理程序。但是,错误中间件现在不再重复错误发送逻辑,而是handleError()通过Promise.reject()发送到res.promise().

这有助于我们处理这样的同步错误:

router.get('/someRoute', function(req, res){
  throw new Error('This is synchronous error!');
});

最后,让我们使用我们的新res.promise()in /routes/users.js

const express = require('express');
const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
  res.promise(userService.getAll());
});

router.get('/:id', function(req, res) {
  res.promise(() => userService.getById(req.params.id));
});

module.exports = router;

注意 : 的不同用途.promise():我们可以传递给它一个函数或一个 promise。传递函数可以帮助你处理没有Promise的方法;.promise()看到它是一个函数并将它包装在一个Promise中。

将错误实际发送给客户端在哪里更好?这是一个很好的代码组织问题。我们可以在我们的错误中间件(因为它应该处理错误)或我们的Promise中间件(因为它已经与我们的响应对象进行了交互)中做到这一点。我决定在我们的 promise 中间件中将所有响应操作放在一个地方,但是由每个开发人员来组织他们自己的代码。

从技术上讲,res.promise()是可选的

如何进行基于Promise的错误处理?我们添加了res.promise(),但我们并没有被限制使用它:我们可以在需要时直接对响应对象进行操作。让我们看一下这会很有用的两种情况:重定向和流管道。

特例 1:重定向

假设我们要将用户重定向到另一个 URL。让我们添加一个函数getUserProfilePicUrl()userService.js

const getUserProfilePicUrl = (id) => Promise.resolve(`/img/${id}`);

现在让我们在我们的用户路由器中使用它async/await风格的直接响应操作:

router.get('/:id/profilePic', async function (req, res) {
  try {
    const url = await userService.getUserProfilePicUrl(req.params.id);
    res.redirect(url);
  } catch (e) {
    res.promise(Promise.reject(e));
  }
});

请注意我们如何使用asyncawait,执行重定向,并且(最重要的是)仍然有一个中心位置来传递任何错误,因为我们用于res.promise()错误处理。

特例 2:流管道

Express.js Promise的错误处理实例 - 就像我们的个人资料图片路由一样,管道流是我们需要直接操作响应对象的另一种情况。

为了处理对我们现在重定向到的 URL 的请求,让我们添加一个返回一些通用图片的路由。

Express.js Promise的错误处理教程:首先,我们应该添加profilePic.jpg一个新的/assets/img子文件夹。(在实际项目中,我们会使用像 AWS S3 这样的云存储,但管道机制是相同的。)

让我们通过管道来响应/img/profilePic/:id请求。我们需要为此创建一个新的路由器/routes/img.js

const express = require('express');
const router = express.Router();

const fs = require('fs');
const path = require('path');

router.get('/:id', function(req, res) {
  /* Note that we create a path to the file based on the current working
   * directory, not the router file location.
   */

  const fileStream = fs.createReadStream(
    path.join(process.cwd(), './assets/img/profilePic.png')
  );
  fileStream.pipe(res);
});

module.exports = router;

然后我们添加我们的新/img路由器app.js

app.use('/users', require('./routes/users'));
app.use('/img', require('./routes/img'));

与重定向情况相比,一个区别可能很突出:我们没有res.promise()/img路由器中使用过!这是因为已通过管道传递错误的响应对象的行为与错误发生在流中间时的行为不同。

Express.js 开发人员在 Express.js 应用程序中处理流时需要注意,根据错误发生的时间以不同的方式处理错误。我们需要在管道之前(res.promise()可以帮助我们)以及中游(基于.on('error')处理程序)处理错误,但更多细节超出了本文的范围。

增强 res.promise()

调用 一样 res.promise(),我们也没有被锁定在以我们现有的方式实现它。promiseMiddleware.js可以扩充以接受一些选项,res.promise()以允许调用者指定响应状态代码、内容类型或项目可能需要的任何其他内容。开发人员可以塑造他们的工具并组织他们的代码,以使其最适合他们的需求。

Express.js 错误处理符合现代基于 Promise 的编码

如何进行基于Promise的错误处理?该方法这里介绍的允许更优雅路线的处理程序比我们开始和处理结果和错误的单点-即使那些解雇之外的res.promise(...)-这要感谢错误处理app.js。尽管如此,我们并没有被迫使用它,并且可以根据需要处理边缘情况。

GitHub 上提供了这些示例的完整代码。从那里,开发人员可以根据需要向handleResponse()函数添加自定义逻辑,例如如果没有可用数据,则将响应状态更改为 204 而不是 200。

但是,对错误的附加控制更为有用。这种方法帮助我在生产中简洁地实现了这些功能:

  • 将所有错误的格式一致为 {error: {message}}
  • 如果未提供状态,则发送通用消息,否则传递给定消息
  • 如果环境是dev(或test等),则填充该error.stack字段
  • 处理数据库索引错误(即某些具有唯一索引字段的实体已经存在)并优雅地响应有意义的用户错误

这个 Express.js 路由逻辑都在一个地方,没有触及任何服务——一种解耦使代码更容易维护和扩展。这就是简单但优雅的解决方案可以显着改善项目结构的方式。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: