在TypeScript中扩展Express Request对象的介绍及实例

1,896 阅读9分钟

Request 对象被 Express 用来向 Node.js 服务器的控制器提供有关 HTTP 请求的数据。因此,所有来自请求的数据在应用层的可用性都取决于这个对象。

你可能需要阐述你在HTTP请求中收到的数据,并向控制器提供自定义信息。在JavaScript中,你可以简单地在Request 对象中定义新的属性并在需要时使用它们。在TypeScript中,如果你想定义自定义属性,你需要扩展Request 类型。

现在让我们学习一下Express中的Request 是什么,并深入了解为什么在TypeScript中扩展Request 类型会很有用。然后,让我们看看你如何通过一个用TypeScript构建的Express演示应用程序来利用扩展的Request 对象的优势。

TL;DR: 让我们学习如何在TypeScript中扩展Request 类型,使其实例存储自定义数据,你可以在控制器级别使用。

什么是Express中的Request 对象?

[Request](https://expressjs.com/en/5x/api.html#req)对象代表了由客户端向Express服务器执行的HTTP请求。换句话说,Express服务器可以通过Request 实例读取从客户端收到的数据。因此,Request 有几个属性来访问HTTP请求中包含的所有信息,但最重要的属性是。

  • [query](https://expressjs.com/en/5x/api.html#req.query):此对象包含请求的URL中存在的每个查询字符串参数的一个属性:

    app.get("/users", (req: Request, res: Response) => {
      // on GET "/users?id=4" this would print "4"
      console.log(req.query.id)
    });
    
  • [params](https://expressjs.com/en/5x/api.html#req.params): 此对象包含根据Express路由惯例在API URL中定义的参数:

    app.get("/users/:id", (req: Request, res: Response) => {
      // on GET "/users/1" this would print "1"
      console.log(req.params.id)
    });
    
  • [body](https://expressjs.com/en/5x/api.html#req.body): 这个对象包含在HTTP请求正文中提交的数据的键值对:

    app.get("/users", (req: Request<never, never, { name: string; surname: string }, never>, res: Response) => {
      const { name, surname } = req.body
    
      // ...
    })
    
  • headers: 这个对象包含了请求所发送的每个HTTP头的一个属性。

  • [cookies](https://expressjs.com/en/5x/api.html#req.cookies): 当使用 [cookie-parser](https://www.npmjs.com/package/cookie-parser)Express中间件时,这个对象包含了由请求发送的每个cookie的一个属性。

为什么要扩展Request

Express控制器可以通过Request 对象访问HTTP请求中包含的所有数据。这并不意味着Request 对象是与控制器交互的唯一方式。相反,Express还支持中间件。Express中间件是可以用来增加应用级或路由器级功能的函数。

中间件函数与路由器级别的端点有如下关联。

const authenticationMiddleware = require("../middlewares/authenticationMiddleware")
const FooController = require("../controllers/foo")

app.get(
  "/helloWorld",
  FooController.helloWorld, // (req, res) => { res.send("Hello, World!") }
  // registering the authenticationMiddleware to the "/helloWorld" endpoint
  authenticationMiddleware,
)

请注意,在调用包含API的业务逻辑的控制器函数之前,中间件函数会被执行。在这里了解更多关于它们如何工作以及Express中间件能提供什么。

这里需要注意的是,中间件可以修改Request 对象,添加自定义信息,使其在控制器层可用。例如,假设你想让你的API只对拥有有效认证令牌的用户可用。为了实现这一点,你可以定义一个简单的认证中间件,如下所示。

import { Request, Response, NextFunction } from "express"

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
  const authenticationToken = req.headers["authorization"]

  if (authenticationToken !== undefined) {
    const isTokenValid = // verifying if authenticationToken is valid with a query or an API call...

    if (isTokenValid) {
      // moving to the next middleware
      return next()
    }
  }

  // if the authorization token is invalid or missing returning a 401 error
  res.status(401).send("Unauthorized")
}

当在HTTP请求头中收到的认证令牌 [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)头中收到的认证令牌是有效的,这个值是与你的服务的用户唯一相关的。换句话说,认证令牌允许你识别发出请求的用户,这是非常重要的信息。例如,控制器层面的业务逻辑可能会根据用户的角色而改变。

如果几个控制器级别的函数需要知道执行API调用的用户是谁,你必须在多个地方复制从Authorization 头部检索用户所需的逻辑。相反,你应该用一个user 属性来扩展Request 对象,并在认证中间件中给它一个值。

请注意,TypeScript中的ExpressRequest 类型并不涉及user 属性。这意味着你不能简单地扩展Request 对象,如下所示。

import { Request, Response, NextFunction } from "express"

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
  const authenticationToken = req.headers["authorization"]

  if (authenticationToken !== undefined) {
    const isTokenValid = // verifying if authenticationToken is valid with a query or an API call...

    if (isTokenValid) {
      const user = // retrieving the user info based on authenticationToken  

      req["user"] = user // ERROR: Property 'user' does not exist on type 'Request'

      // moving to the next middleware
      return next()
    }
  }

  // if the authorization token is invalid or missing returning a 401 error
  res.status(401).send("Unauthorized")
}

这将导致以下错误:

Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.

同样,你可以使用一个扩展的Request ,以避免在控制器级别的类型铸造,并使你的代码库更干净和健壮。让我们假设你的后端应用程序只支持三种语言。英语、西班牙语和意大利语。换句话说,你已经知道Content-Language HTTP头信息只能接受enes ,和it 。当头信息被省略或包含一个无效的值时,你希望将英语作为默认语言。

请记住,req.headers["Content-Language"] 会返回一个string | string[] | undefined 类型。这意味着,如果你想使用Content-Language 头部的值作为string ,你必须按如下方式进行投递。

const language = (req.headers["content-language"] || "en") as string | undefined

用这种逻辑填满你的代码并不是一个优雅的解决方案。相反,你应该使用一个中间件来扩展Request ,如下所示:

import { Request, Response, NextFunction } from "express"

const SUPPORTED_LANGUAGES = ["en", "es", "it"]
// this syntax is equals to "en" | "es" | "it"
export type Language = typeof SUPPORTED_LANGUAGES[number] 

export function handleCustomLanguageHeader(req: Request, res: Response, next: NextFunction) {
  const languageHeader = req.headers["content-language"]

  // default language: "en"
  let language: Language = SUPPORTED_LANGUAGES[0]

  if (typeof languageHeader === "string" && SUPPORTED_LANGUAGES.includes(languageHeader)) {
    language = languageHeader
  }

  // extending the Request object with a language property of type Language...

  return next()
}

这只是两个例子,但还有其他几种情况,用自定义数据扩展Request ,可以节省时间,使你的代码库更加优雅和可维护。

在TypeScript中扩展ExpressRequest 类型

在ExpressRequest 类型定义中添加额外的字段只需要几行代码。让我们看看如何实现它,并通过一个基于前面介绍的中间件的演示应用程序来利用扩展对象。

克隆支持该文章的GitHub仓库,用以下命令在本地启动示例后台应用程序。

git clone https://github.com/Tonel/extend-express-request-ts-demo
cd extend-express-request-ts-demo
npm i
npm start

继续跟随文章,学习如何在TypeScript中利用扩展的Express类型Request

前提条件

你需要这些先决条件来复制这篇文章的目标:

  • [express](https://www.npmjs.com/package/express)>= 4.x
  • [@types/express](https://www.npmjs.com/package/@types/express)>= 4.x
  • [typescript](https://www.npmjs.com/package/typescript)>= 4.x

如果你在Typescript中没有Express项目,你可以在这里学习如何从头开始建立一个Express和TypeScript项目

Request 类型添加自定义属性

要扩展Request 类型,你所要做的就是定义一个index.d.ts 文件,如下所示:

// src/types/express/index.d.ts

import { Language, User } from "../custom";

// to make the file a module and avoid the TypeScript error
export {}

declare global {
  namespace Express {
    export interface Request {
      language?: Language;
      user?: User;
    }
  }
}

将此文件放在src/types/express 文件夹中。Typescript使用 [.d.ts](https://en.wikipedia.org/wiki/TypeScript#Declaration_files)声明文件来加载关于用JavaScript编写的库的类型信息。在这里, [index.d.ts](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-d-ts.html)global模块将被TypeScript用来在全球范围内扩展ExpressRequest 类型。

LanguageUser 自定义类型被定义在src/types/custom.ts 文件中,如下所示。

// src/types/custom.ts

export const SUPPORTED_LANGUAGES = ["en", "es", "it"]
// this syntax is equals to "en" | "es" | "it"
export type Language = typeof SUPPORTED_LANGUAGES[number]

export type User = {
    id: number,
    name: string,
    surname: string,
    authenticationToken : string | null
}

这些类型将分别在handleCustomLanguageHeader()handleTokenBasedAuthentication() 中间件函数中使用。让我们来看看如何。

使用扩展的Request 对象

现在,让我们来学习如何运用扩展的Request 对象。首先,让我们完成前面介绍的中间件函数。这就是authentication.middleware.ts 的样子。

// src/middlewares/authentication.middleware.ts 

import { Request, Response, NextFunction } from "express"
import { User } from "../types/custom"

// in-memory database
const users: User[] = [
    {
        id: 1,
        name: "Maria",
        surname: "Williams",
        authenticationToken: "$2b$08$syAMV/CyYt.ioZ3w5eT/G.omLoUdUWwTWu5WF4/cwnD.YBYVjLw2O",
    },
    {
        id: 2,
        name: "James",
        surname: "Smith",
        authenticationToken: null,
    },
    {
        id: 3,
        name: "Patricia",
        surname: "Johnson",
        authenticationToken: "$2b$89$taWEB/dykt.ipQ7w4aTPGdo/aLsURUWqTWi9SX5/cwnD.YBYOjLe90",
    },
]

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
    const authenticationToken = req.headers["authorization"]

    if (authenticationToken !== undefined) {
        // using the in-memory sample database to verify if authenticationToken is valid
        const isTokenValid = !!users.find((u) => u.authenticationToken === authenticationToken)

        if (isTokenValid) {
            // retrieving the user associated with the authenticationToken value
            const user = users.find((u) => u.authenticationToken === authenticationToken)

            req.user = user

            // moving to the next middleware
            return next()
        }
    }

    // if the authorization token is invalid or missing returning a 401 error
    res.status(401).send("Unauthorized")
}

为了简单起见,认证令牌是通过一个内存数据库验证的。在现实世界的场景中,用一些查询或API调用来代替这个简单的逻辑。注意你现在可以把与令牌相关的用户分配给Request 自定义user 属性。

现在,让我们看看最后的language.middleware.ts 中间件文件。

// src/middlewares/headers.middleware.ts

import { Request, Response, NextFunction } from "express"
import { Language, SUPPORTED_LANGUAGES } from "../types/custom"

export function handleLanguageHeader(req: Request, res: Response, next: NextFunction) {
    const languageHeader = req.headers["content-language"]

    // default language: "en"
    let language: Language = SUPPORTED_LANGUAGES[0]

    if (typeof languageHeader === "string" && SUPPORTED_LANGUAGES.includes(languageHeader)) {
        language = languageHeader
    }

    req.language = language

    return next()
}

Request 自定义language 属性被用来存储Language 信息。

接下来,让我们看看Request 自定义属性在两个不同的API中的作用。这就是HelloWorldController 对象的样子。

// src/controllers/helloWorld.ts 

import { NextFunction, Request, Response } from "express"

export const HelloWorldController = {
    default: async (req: Request<never, never, never, never>, res: Response, next: NextFunction) => {
        let message

        switch (req.language) {
            default:
            case "en": {
                message = "Hello, World!"
                break
            }
            case "es": {
                message = "¡Hola, mundo!"
                break
            }
            case "it": {
                message = "Ciao, mondo!"
                break
            }
        }

        res.json(message)
    },
    hello: async (req: Request<never, never, never, never>, res: Response, next: NextFunction) => {
        res.json(`Hey, ${req.user?.name}`)
    },
}

正如你所看到的,HelloWorldController 定义了两个API。第一个使用了Request 自定义language 属性,而第二个则采用了Request 自定义user 属性。

最后,你必须在路由器文件中注册中间件函数和它们的端点,如下所示。

// src/routes/index.ts

import { Router } from "express"
import { HelloWorldController } from "../controllers/helloWorld"
import { handleLanguageHeader } from "../middleware/customHeaders.middleware"
import { handleTokenBasedAuthentication } from "../middleware/authentication.middleware"

export const router = Router()

router.get("/", handleLanguageHeader, HelloWorldController.default)
router.get("/hello", handleTokenBasedAuthentication, HelloWorldController.hello)

测试扩展Request

让我们测试一下前面定义的API,用 [curl](https://curl.se/docs/manpage.html).首先,用npm run start 启动演示的Express应用程序。

现在,让我们看一下/ 端点的行为:

  • curl -i -H "Content-Language: it" [http://localhost:8081/](http://localhost:8081/)返回Ciao, mondo!
  • curl -i [http://localhost:8081/](http://localhost:8081/)返回默认值Hello, World!
  • curl -i -H "Content-Language: es" [http://localhost:8081/](http://localhost:8081/)返回¡Hola, mundo!

正如你所看到的,由API返回的消息的语言由Content-Language 头部返回,正如预期。

现在,让我们测试一下/hello 端点。

  • curl -i [http://localhost:8081/hello](http://localhost:8081/hello)返回一个401 Unauthorized 错误。
  • curl -i -H "Authorization: $2b$08$syAMV/CyYt.ioZ3w5eT/G.omLoUdUWwTWu5WF4/cwnD.YBYVjLw2O" [http://localhost:8081/hello](http://localhost:8081/hello)返回¡Hola, Maria!

同样,由于Authorization 头的值,响应取决于加载的用户。

Et voila!你刚刚学会了如何扩展ExpressRequest 类型,以及如何使用它来为控制器提供自定义信息。

总结

在这篇文章中,你看到了什么是ExpressRequest 对象,为什么它如此重要,以及什么时候你可能需要扩展它。Request 对象存储了所有关于 HTTP 请求的信息。能够扩展它代表了一种有效的方式,可以直接将自定义数据传递给控制器。如图所示,这可以让你避免代码的重复。

在这里,你学到了如何在TypeScript中扩展ExpressRequest 类型。这很简单,只需要几行代码。而且,它可以为你的后端应用程序带来一些优势,正如通过用TypeScript开发的示例演示Express应用程序所显示的那样。

谢谢你的阅读!我希望你觉得这篇文章对你有帮助。如果有任何问题、评论或建议,请随时与我联系。