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头信息只能接受en ,es ,和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 类型。
Language 和User 自定义类型被定义在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应用程序所显示的那样。
谢谢你的阅读!我希望你觉得这篇文章对你有帮助。如果有任何问题、评论或建议,请随时与我联系。