<路径 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">authors are vetted experts 在ir 场s 和 write on topics in which they have demonstrated experience. All of our content is peer reviewed 和 validated by Toptal experts 在 same 场.

维塔利Senko

Vitaly is a full-堆栈 developer who has extensive experience in creating 应用程序s with 节点.js、React和 .NET, including the maintenance of a healthcare plat为m with nearly 20 million 用户.

工作经验

9

的表达.js tagline rings true: It’s a “fast, unopinionated, minimalist web framework 为 节点.js.“它是如此的不受干扰,尽管目前 JavaScript最佳实践 规定使用承诺,表达.Js默认不支持基于承诺的路由处理程序.

有很多快递.Js教程省略了这些细节, developers often get 在 habit of copying 和 pasting result-sending 和 错误-h和ling code 为 each route, 创造技术债务. We can avoid this antipattern (和 its fallout) with the technique we’ll cover today—one I’ve used successfully in 应用程序s with hundreds of 路线.

表达的典型架构.js路线

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

In real projects, we would store the related data in some database like MongoDB. 但就我们的目的而言, 数据存储细节并不重要, 因此,为了简单起见,我们将模拟它们. What we won’t simplify is good project structure, the key to half the success of any project.

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

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

We’ve pared down the lines of the remaining files that aren’t related to our goals.

这是主要的快车.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 = 应用程序;

这里我们创建一个表达.js 应用程序 和 add some basic middleware to support JSON use, URL encoding, 和 cookie parsing. 然后加上 用户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路由问题

Looking at our route h和lers, we see that each service call uses duplicate .然后(...).抓住(...) 将数据或错误发送回客户端的回调.

乍一看,这似乎并不严重. Let’s add some basic real-world requirements: We’ll need to display only certain 错误s 和 omit generic 500-level 错误s; also, 我们是否应用这种逻辑必须基于环境. 与, what will it look like when our example project grows from its two 路线 into a real project with 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));
});

Looks better: We’re not repeating our implementation of sending data 和 错误s. But we’ll still need to import these h和lers in every route 和 add them to each 表达 promise passed to 然后()抓住().

方法2:中间件

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

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

表达.js underst和s that this is 为 错误s because the 函数 signature has four input arguments. (它利用了每个函数对象都有一个 .长度 属性描述函数期望多少个参数.)

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

// /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 () 函数

Let’s create our promise middleware, which will give us the flexibility to structure our 表达.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));
});

请注意, 我们没有省略错误中间件. It’s still an important 错误 h和ler 为 all synchronous 错误s that may exist in our code. 而不是重复错误发送逻辑, the 错误 middleware now passes any synchronous 错误s to the same central 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中.

向客户端发送错误在哪里更好? 这是一个很好的代码组织问题. We could do that in our 错误 middleware (because it’s supposed to work with 错误s) or in our promise middleware (because it already has interactions with our response object). I decided to keep all response operations in one place in our promise middleware, 但这取决于每个开发人员如何组织自己的代码.

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

我们增加了 res.承诺(), but we’re not locked into using it: We’re free to operate with the response object directly when we need to. Let’s look at two cases where this would be useful: redirecting 和 stream piping.

特殊情况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));
  }
});

注意我们是如何使用 异步/等待, 执行重定向, 和 (most importantly) still have one central place to pass any 错误 because we used res.承诺() 用于快速错误处理.

特殊情况2:流管道

就像我们的头像路线, piping a stream is another situation where we need to manipulate the response object directly.

处理对我们现在重定向到的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 '));

One difference likely st和s out compared to the redirect case: We haven’t used res.承诺()/ img 路由器! This is because the behavior of an already-piped response object being passed an 错误 will be different than if the 错误 occurs 在 middle of the stream.

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

加强 res.承诺()

调用 res.承诺(),我们没有被锁定 实现 这不是我们的方式. promiseMiddleware.js 能不能扩增接受一些选项 res.承诺() 允许调用方指定响应状态码, 内容类型, 或者其他项目可能需要的东西. It’s up to developers to shape their tools 和 organize their code so that it best suits their needs.

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

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

这些示例的完整代码是 可以在GitHub上找到. 从那里,开发人员可以根据需要将自定义逻辑添加到 h和leResponse () 函数, such as changing the response status to 204 instead of 200 if no data is available.

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

  • 将所有错误格式化为 {错误:{消息}}
  • Send a generic message if no status is provided or pass along a given message otherwise
  • 如果环境是 dev (or 测试等.),填充 错误.堆栈
  • 处理数据库索引错误(i.e., some entity with a unique-indexed 场 already exists) 和 gracefully respond with meaningful user 错误s

这个表达.Js的路由逻辑都在一个地方, without touching any service—a decoupling that left the code much easier to maintain 和 extend. This is how simple—but elegant—solutions can drastically improve project structure.

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

了解基本知识

  • 什么是表达中间件?

    表达.js middleware 函数s are 函数s that have access to the request object (typically “req”), 响应对象(“res”), 和 the 下一个 middleware 函数 在 应用程序lication’s request-response cycle (“下一个”). 它们可以在路由处理程序执行之前或之后添加额外的逻辑.

  • 快车有哪些路线.js?

    一个表达.js route is a h和ler 函数 corresponding to a given type of HTTP event matching a specified URI pattern. 是寄给快递的.js路由器或表达.js 应用程序 object 和 contains logic about processing the HTTP request 和 sending results back to the client.

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

    一个表达.js路由器 is a class where each of its instances is an isolated set of middleware 函数s 和 路线. It’s a sort of “mini-应用程序lication,” capable only of per为ming middleware 和 routing 函数s. 每一个表达.Js的应用有一个内置的应用路由器.

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

    表达中的错误处理.js is a technique to h和le 错误s in different places by passing them to a single 错误 h和ler. 然后,错误处理程序对错误执行通用逻辑, 比如在对客户端的响应中发送它们.

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

    JavaScript中内置的承诺对象表示异步操作. 它可以处于以下三种状态之一:挂起、完成或拒绝. Additional actions on fulfilled 和 拒绝ed results may be 应用程序lied using h和lers passed to the object’s 然后() 和 抓住() methods, 分别.

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

    We use promises in JavaScript to avoid “回调 hell”—a code structure whereby every 异步hronous result h和ler creates an additional nesting layer.

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

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

成员自 2019年3月1日

作者简介

Vitaly is a full-堆栈 developer who has extensive experience in creating 应用程序s with 节点.js、React和 .NET, including the maintenance of a healthcare plat为m with nearly 20 million 用户.

Toptalauthors are vetted experts 在ir 场s 和 write on topics in which they have demonstrated experience. All of our content is peer reviewed 和 validated by Toptal experts 在 same 场.

工作经验

9

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

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

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

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

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

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

Toptal开发者

加入总冠军® 社区.