<路径 clip-rule="evenodd" d="M33.377 4.574a3.508 3.508 0 0 0-2.633-1.126c-1 0-1.993.67-2.604 1.334l.002-1.24-1.867-.002-.02 10.17v.133l1.877.002.008-3.18c.567.611 1.464.97 2.462.973 1.099 0 2.022-.377 2.747-1.117.73-.745 1.1-1.796 1.103-3.002.003-1.232-.358-2.222-1.075-2.945Zm-3.082.55c.637 0 1.176.23 1.602.683.438.438.663 1.012.66 1.707-.003.7-.22 1.33-.668 1.787-.428.438-.964.661-1.601.661-.627 0-1.15-.22-1.6-.666-.445-.46-.662-1.086-.662-1.789.003-.695.227-1.27.668-1.708a2.13 2.13 0 0 1 1.596-.675h.005Zm5.109-.067-.008 4.291c-.002.926.263 1.587.784 1.963.325.235.738.354 1.228.354.376 0 .967-.146.967-.146l-.168-1.564s-.43.133-.64-.01c-.198-.136-.296-.428-.296-.866l.008-4.022 1.738.002.002-1.492-1.738-.002.005-2.144-1.874-.002-.005 2.143-1.573-.002 1.57 1.497ZM20.016 1.305h-9.245l-.002 1.777h3.695l-.016 8.295v.164l1.955.002-.008-8.459 3.621-.002V1.305Z" fill="#262D3D" fill-rule="evenodd"><路径 clip-rule="evenodd" d="M10.06 5.844 7.277 3.166 4.015.03 2.609 1.374l2.056 1.978-4.51 4.313 6.065 5.831 1.387-1.327-2.073-1.994 4.526-4.331ZM4.274 8.7a.211.211 0 0 1-.124 0c-.04-.013-.074-.03-.15-.102l-.817-.787c-.072-.069-.092-.104-.105-.143a.187.187 0 0 1 0-.12c.013-.039.03-.07.105-.143L5.76 4.938c.072-.07.108-.09.15-.099a.21.21 0 0 1 .123 0c.041.012.075.03.15.101L7 5.727c.072.07.093.104.103.144.013.04.013.08 0 .119-.013.04-.03.072-.106.143L4.422 8.601a.325.325 0 0 1-.147.099Z" fill="#204ECF" fill-rule="evenodd"><路径 clip-rule="evenodd" d="M24.354 4.622a3.94 3.94 0 0 0-2.876-1.149 4.1 4.1 0 0 0-2.829 1.084c-.804.725-1.214 1.733-1.217 2.992-.002 1.26.405 2.267 1.207 2.995a4.114 4.114 0 0 0 2.832 1.094c.04.002.082.002.123.002a3.967 3.967 0 0 0 2.75-1.138c.538-.532 1.183-1.473 1.186-2.938.002-1.465-.637-2.408-1.176-2.942Zm-.59 2.94c-.003.73-.228 1.334-.671 1.794-.441.458-.99.69-1.633.69a2.166 2.166 0 0 1-1.614-.697c-.43-.45-.65-1.057-.65-1.797s.222-1.344.655-1.795a2.17 2.17 0 0 1 1.617-.69c.64 0 1.189.235 1.63.698.443.46.668 1.064.665 1.797ZM41.15 6.324c0-.458.25-1.465 1.632-1.465.49 0 .768.159 1.003.347.227.18.34.626.34.994v.174l-2.282.341C40.035 6.98 39 7.913 38.993 9.28c-.002.708.266 1.314.777 1.76.503.438 1.191.67 2.004.673 1.023 0 1.792-.354 2.341-1.084.003.31.003.621.003.91h1.903l.013-5.246c.002-.856-.289-1.685-.864-2.14-.567-.449-1.31-.679-2.386-.681h-.015c-.82 0-1.69.208-2.274.695-.689.572-1.027 1.478-1.027 2.178l1.682-.02Zm.864 3.814c-.676-.002-1.115-.371-1.112-.938.003-.589.43-.933 1.346-1.081l1.875-.305v.017c-.005 1.36-.87 2.307-2.102 2.307h-.008Zm4.917-8.712-.018 10.058v.044l1.684.005.018-10.06v-.045l-1.684-.002Zm2.654 9.491c0-.173.062-.322.19-.445a.645.645 0 0 1 .462-.186c.18 0 .338.062.465.186a.596.596 0 0 1 .193.445.583.583 0 0 1-.193.443.644.644 0 0 1-.465.183.634.634 0 0 1-.461-.183.59.59 0 0 1-.191-.443Zm.108 0c0 .146.052.273.158.376a.54.54 0 0 0 .389.154.539.539 0 0 0 .547-.53.498.498 0 0 0-.16-.373.531.531 0 0 0-.387-.156.531.531 0 0 0-.387.155.497.497 0 0 0-.16.374Zm.702.344-.176-.3h-.118v.3h-.109v-.688h.292c.144 0 .23.082.23.196 0 .096-.076.168-.176.188l.178.304h-.121Zm-.294-.596v.21h.167c.093 0 .14-.034.14-.104 0-.072-.047-.106-.14-.106h-.167Z" fill="#262D3D" fill-rule="evenodd">作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

维塔利Senko

Vitaly是一名全栈开发人员,在使用节点创建应用程序方面拥有丰富的经验.js、React和 .NET,包括维护一个拥有近2000万用户的医疗保健平台.

工作经验

9

的表达.js的口号听起来很真实:它是一个“快速、无偏见、极简的节点 web框架”.js.“它是如此的不受干扰,尽管目前 JavaScript最佳实践 规定使用承诺,表达.Js默认不支持基于承诺的路由处理程序.

有很多快递.Js教程省略了这些细节, 开发人员经常习惯于为每个路由复制和粘贴结果发送和错误处理代码, 创造技术债务. 我们可以通过我们今天将要介绍的技术来避免这种反模式(及其后果)——我已经在具有数百条路由的应用程序中成功地使用了这种技术.

表达的典型架构.js路线

让我们从快车开始.Js教程应用程序,为用户模型提供了一些路由.

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

一般来说,自耕农可以产生更好的项目骨架, 而是为了我们需要的东西, 我们将简单地创建一个项目骨架 express-generator 然后去掉不必要的部分,直到我们得到这个:

箱子。
  开始.js
node_模块s
路线
  用户.js
服务
  userService.js
应用程序.js
包-lock.json
包.json

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

这是主要的快车.Js应用程序文件, ./应用程序.js:

const createError = require('http-错误s');
Const express = require('express');
const cookieParser = require('cookie-parser');
const 用户Router = require('./线路/用户);

Const 应用程序 = express();
应用程序.使用(表达.json ());
应用程序.使用(表达.Urlencoded ({extended: false}));
应用程序.使用(cookieParser ());
应用程序.使用(' /用户,用户Router);
应用程序.使用(函数(req, res, 下一个) {
  下一个(显示createError (404));
});
应用程序.使用(函数(err, req, res, 下一个) {
  res.状态(犯错.状态|| 500);
  res.发送(错);
});

模块.Exports = 应用程序;

这里我们创建一个表达.并添加一些基本的中间件来支持JSON的使用、URL编码和cookie解析. 然后加上 用户Router/用户. 最后, 我们指定如果没有找到路由该怎么做, 以及如何处理错误, 我们稍后再改.

启动服务器本身的脚本是 /箱子。/开始.js:

Const 应用程序 = require('../应用程序”);
Const HTTP = require(' HTTP ');

端口=进程.env.端口|| '3000';

Const 服务器 = HTTP.createServer(应用);
服务器.听(港口);

/包.json 在我们的快递里.Js承诺的例子也是barebones:

{
  “名称”:“express-promises-example”,
  “版本”:“0.0.0",
  “私人”:没错,
  "脚本":{
    “开始”:“节点 ./箱子。/开始.js"
  },
  “依赖”:{
    “cookie-parser”:“~ 1.4.4",
    “表达”:“~ 4.16.1",
    “的http错误”:“~ 1.6.3"
  }
}

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

Const express = require('express');
Const 路由器 = express.路由器();

const userService = require('../服务/ userService ');

路由器.Get ('/', 函数(req, res) {
  userService.getAll ()
    .然后(result => res.状态(200).发送(结果))
    .抓住(err => res.状态(500).发送(err));
});

路由器.Get ('/:id', 函数(req, res) {
  userService.getById(要求.参数个数.id)
    .然后(result => res.状态(200).发送(结果))
    .抓住(err => res.状态(500).发送(err));
});

模块.Exports = 路由器;

它有两条路线: / 获得所有用户和 /:id 按ID获取单个用户. 它还使用 /服务/ userService.js,它有基于承诺的方法来获取这些数据:

Const 用户 = [
  {id: '1', fullName: '用户第一个'},
  {id: '2', fullName: '第二用户'}
];

const getAll = () => 承诺.解决(用户);
const getById = (id) => 承诺.解决(用户.find(u => u.Id == Id);

模块.出口= {
  getById,
  getAll
};

这里我们避免使用实际的DB连接器或ORM.g.(Mongoose或Sequelize),简单地模仿数据获取 承诺.解决(...).

表达.js路由问题

看看我们的路由处理程序,我们看到每个服务调用都使用duplicate .然后(...).抓住(...) 将数据或错误发送回客户端的回调.

乍一看,这似乎并不严重. Let’s add some basic real-world requirements: We’ll need to display only certain 错误s 和 omit generic 500-level 错误s; also, 我们是否应用这种逻辑必须基于环境. 与, 当我们的示例项目从两条路由扩展到拥有200条路由的实际项目时,它会是什么样子?

方法1:效用函数

也许我们应该创建单独的实用函数来处理 解决拒绝,并将它们应用到我们的快递中.js路线:

// /utils中的一些响应处理程序 
const h和leResponse = (res, data) => res.状态(200).发送(数据);
const h和leError = (res, err) => res.状态(500).发送(错);


/ /线路/用户.js
路由器.Get ('/', 函数(req, res) {
  userService.getAll ()
    .然后(data => h和leResponse(res, data))
    .抓住(err => h和leError(res, err));
});

路由器.Get ('/:id', 函数(req, res) {
  userService.getById(要求.参数个数.id)
    .然后(data => h和leResponse(res, data))
    .抓住(err => h和leError(res, err));
});

看起来更好:我们没有重复发送数据和错误的实现. 但我们仍然需要在每个路由中导入这些处理程序,并将它们添加到传递给的每个表达承诺中 然后()抓住().

方法2:中间件

表达路由器错误处理的另一个解决方案可能是使用表达.js 最佳实践 围绕承诺:将错误发送逻辑移到表达中.Js错误中间件(添加在 应用程序.js),并将异步错误传递给它 下一个 回调. 我们的基本错误中间件设置将使用一个简单的匿名函数:

应用程序.使用(函数(err, req, res, 下一个) {
  res.状态(犯错.状态|| 500);
  res.发送(错);
});

表达.Js理解这是针对错误的,因为函数签名有四个输入参数. (它利用了每个函数对象都有一个 .长度 属性描述函数期望多少个参数.)

通过传递错误 下一个 看起来像这样:

// /utils中的一些响应处理程序 
const h和leResponse = (res, data) => res.状态(200).发送(数据);

/ /线路/用户.js
路由器.Get ('/', 函数(req, res, 下一个) {
  userService.getAll ()
    .然后(data => h和leResponse(res, data))
    .抓住(下);
});

路由器.Get ('/:id', 函数(req, res, 下一个) {
  userService.getById(要求.参数个数.id)
    .然后(data => h和leResponse(res, data))
    .抓住(下);
});

甚至使用官方的最佳实践指南, 我们仍然需要在每个路由处理程序中使用a来解析JS承诺 h和leResponse () 函数并拒绝 下一个 函数.

让我们试着用更好的方法来简化它.

方法3:基于承诺的中间件

JavaScript最伟大的特性之一是它的动态特性. 我们可以在运行时向任何对象添加任何字段. 我们将使用它来扩展表达.js result objects; 表达.Js的中间件函数是一个方便的地方.

我们的 promiseMiddleware () 函数

让我们创建promise中间件,它将为我们提供构建表达的灵活性.Js的路由更优雅. 我们需要一份新文件 /中间件/承诺.js:

const h和leResponse = (res, data) => res.状态(200).发送(数据);
const h和leError = (res, err = {}) => res.状态(犯错.状态|| 500).发送({错误:错误.消息});


模块.exports = 函数 promise中间件(){
  return (req,res,下一个) => {
    res.promise = (p) => {
      让promiseToResolve;
      如果(p.然后 && p.捕捉){
        promiseToResolve = p;
      } else if (typeof p === '函数') {
        允诺to解决 =承诺.解决().然后(() => p());
      } else {
        允诺to解决 =承诺.解决(p);
      }

      返回promiseToResolve
        .然后((data) => h和leResponse(res, data))
        .抓住((e) => h和leError(res, e));  
    };

    返回下一个();
  };
}

In 应用程序.js,让我们将中间件应用到整个表达.js 应用程序 对象并更新默认的错误行为:

const promise中间件= require('./中间件)/承诺');
//...
应用程序.使用(promiseMiddleware ());
//...
应用程序.使用(函数(req, res, 下一个) {
  res.承诺(承诺.拒绝(显示createError (404)));
});
应用程序.使用(函数(err, req, res, 下一个) {
  res.承诺(承诺.拒绝(err));
});

请注意, 我们没有省略错误中间件. 对于代码中可能存在的所有同步错误,它仍然是一个重要的错误处理程序. 而不是重复错误发送逻辑, 错误中间件现在将所有同步错误传递给同一个中心 h和leError () 通过a函数 承诺.拒绝() 呼叫发送到 res.承诺().

这可以帮助我们处理像下面这样的同步错误:

路由器.get('/someRoute', 函数(req, res){
  throw new Error(`这是同步错误!');
});

最后,让我们使用我们的new res.承诺() in /线路/用户.js:

Const express = require('express');
Const 路由器 = express.路由器();

const userService = require('../服务/ userService ');

路由器.Get ('/', 函数(req, res) {
  res.承诺(userService.getAll ());
});

路由器.Get ('/:id', 函数(req, res) {
  res.承诺(() => userService.getById(要求.参数个数.id));
});

模块.Exports = 路由器;

的不同用法 .承诺()我们可以给它传递一个函数或一个承诺. 传递函数可以帮助你处理没有承诺的方法; .承诺() 看到它是一个函数,并把它包装在一个promise中.

向客户端发送错误在哪里更好? 这是一个很好的代码组织问题. 我们可以在错误中间件(因为它应该与错误一起工作)或承诺中间件(因为它已经与响应对象进行了交互)中做到这一点。. 我决定将所有响应操作保留在承诺中间件中的一个位置, 但这取决于每个开发人员如何组织自己的代码.

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

我们增加了 res.承诺(), 但是我们并不一定要使用它:我们可以在需要的时候自由地直接操作响应对象. 让我们看两种有用的情况:重定向和流管道.

特殊情况1:重定向

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

const getUserProfilePicUrl = (id) => 承诺.解决(' / img / $ {id}”);

现在我们在user路由器in中使用它 异步/等待 带有直接反应操作的风格:

路由器.get('/:id/profilePic', 异步 函数 (req, res) {
  尝试{
    const url =等待userService.getUserProfilePicUrl(要求.参数个数.id);
    res.重定向(url);
  } catch (e) {
    res.承诺(承诺.拒绝(e));
  }
});

注意我们是如何使用 异步/等待, 执行重定向, 并且(最重要的是)仍然有一个中心位置来传递任何错误,因为我们使用 res.承诺() 用于快速错误处理.

特殊情况2:流管道

就像我们的头像路线, 流的管道化是我们需要直接操作响应对象的另一种情况.

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

首先我们应该加上 profilePic.jpg 在一个新的 /资产/ img 子文件夹. (在实际项目中,我们会使用像AWS S3这样的云存储, 但是管道的机制是一样的.)

让我们将此图像作为响应管道 / img / profilePic /: id 请求. 我们需要为它创建一个新的路由器 /线路/ img.js:

Const express = require('express');
Const 路由器 = express.路由器();

Const fs = require('fs');
Const 路径 = require('路径');

路由器.Get ('/:id', 函数(req, res) {
  /*注意,我们会根据当前的工作创建一个文件路径
   *目录,而不是路由器文件的位置.
   */

  const 文件流 = fs.createReadStream (
    路径.加入(过程.慢性消耗病()”,./ / img / profilePic资产.png”)
  );
  文件流.管(res);
});

模块.Exports = 路由器;

然后加上new / img 路由器在 应用程序.js:

应用程序.使用(' /用户的要求(“./线路/用户));
应用程序.用(/ img,需要('./线路/ img '));

与重定向情况相比,一个明显的区别是:我们没有使用 res.承诺()/ img 路由器! 这是因为传递错误的管道响应对象的行为将与错误发生在流中间时的行为不同.

表达.js开发人员 在表达中使用流时需要注意.Js应用程序,根据错误发生的时间以不同的方式处理错误. 我们需要在管道()之前处理错误。res.承诺() 可以在那里帮助我们)以及中游(基于 .(错误的) 处理程序),但进一步的细节超出了本文的范围.

加强 res.承诺()

调用 res.承诺(),我们没有被锁定 实现 这不是我们的方式. promiseMiddleware.js 能不能扩增接受一些选项 res.承诺() 允许调用方指定响应状态码, 内容类型, 或者其他项目可能需要的东西. 这取决于开发人员塑造他们的工具和组织他们的代码,使其最适合他们的需求.

表达中的错误处理.js满足现代基于承诺的编码

这里介绍的方法允许 更优雅的路由处理程序 和a 单点处理结果和误差-即使是那些在外面开火的人 res.承诺(...)-由于错误处理 应用程序.js. 不过,我们还是 不强迫 使用它,可以处理我们想要的边缘情况.

这些示例的完整代码是 可以在GitHub上找到. 从那里,开发人员可以根据需要将自定义逻辑添加到 h和leResponse () 功能,例如在无数据可用时将响应状态更改为204而不是200.

但是,对错误的额外控制要有用得多. 这种方法帮助我在生产环境中简洁地实现了这些特性:

  • 将所有错误格式化为 {错误:{消息}}
  • 如果没有提供状态,则发送通用消息,否则传递给定消息
  • 如果环境是 dev (or 测试等.),填充 错误.堆栈
  • 处理数据库索引错误(i.e., 某些具有唯一索引字段的实体已经存在),并优雅地响应有意义的用户错误

这个表达.Js的路由逻辑都在一个地方, 在不触及任何服务的情况下,这种分离使得代码更容易维护和扩展. 这就是简单而优雅的解决方案如何极大地改善项目结构.

关于总博客的进一步阅读:

了解基本知识

  • 什么是表达中间件?

    表达.Js中间件函数是可以访问请求对象(通常是“req”)的函数。, 响应对象(“res”), 以及应用程序请求-响应周期中的下一个中间件功能(“下一个”). 它们可以在路由处理程序执行之前或之后添加额外的逻辑.

  • 快车有哪些路线.js?

    一个表达.js . js的route是一个处理函数,它对应于与指定URI模式匹配的HTTP事件的给定类型. 是寄给快递的.js路由器或表达.它包含处理HTTP请求并将结果发送回客户端的逻辑.

  • 什么是快递.js路由器?

    一个表达.Js的路由器是这样一个类,它的每个实例都是一组独立的中间件函数和路由. 它是一种“迷你应用程序”,只能够执行中间件和路由功能. 每一个表达.Js的应用有一个内置的应用路由器.

  • 什么是表达中的错误处理.js?

    表达中的错误处理.Js是一种通过将错误传递给单个错误处理程序来处理不同位置的错误的技术. 然后,错误处理程序对错误执行通用逻辑, 比如在对客户端的响应中发送它们.

  • 在JavaScript中承诺是如何工作的?

    JavaScript中内置的承诺对象表示异步操作. 它可以处于以下三种状态之一:挂起、完成或拒绝. 可以使用传递给对象的然后()和抓住()方法的处理程序对完成和拒绝的结果应用其他操作, 分别.

  • 为什么我们要在JavaScript中使用承诺?

    我们在JavaScript中使用承诺是为了避免“回调地狱”——每个异步结果处理程序都会创建一个额外的嵌套层的代码结构.

聘请Toptal这方面的专家.
现在雇佣
维塔利Senko

位于 明斯克,白俄罗斯明斯克地区

成员自 2019年3月1日

作者简介

Vitaly是一名全栈开发人员,在使用节点创建应用程序方面拥有丰富的经验.js、React和 .NET,包括维护一个拥有近2000万用户的医疗保健平台.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

9

世界级的文章,每周发一次.

<为m aria-label="Sticky subscribe 为m" class="-Ulx1zbi P7bQLARO _2ABsLCza">

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

<为m aria-label="Bottom subscribe 为m" class="-Ulx1zbi P7bQLARO _2ABsLCza">

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.