1.x → 2.x
2.0 版迁移指南。
关于发布
¥About the release
版本 2.0 为库带来了自成立以来最大的 API 变化。除了新的 API 之外,它还包括各种功能,例如 ReadableStream
支持、ESM 兼容性和无数的错误修复。本指南将帮助你将应用迁移到 2.0 版。我们强烈建议你从头到尾阅读。
¥Version 2.0 brings the biggest API change to the library since its inception. Alongside the new API, it includes various features, such as ReadableStream
support, ESM-compatibility, and countless bug fixes. This guide will help you migrate your application to version 2.0. We highly recommend you read it from start to finish.
如果你遗漏了此版本的官方公告,请务必阅读!
¥Make sure to read the official announcement for this release if you’ve missed it!
Introducing MSW 2.0
Official announcement post.
Codemods
我们在 Codemod.com 的朋友准备了一个很棒的 codemod 集合,可以帮助你迁移到 MSW 2.0。
¥Our friends at Codemod.com have prepared a fantastic collection of codemods that can help you migrate to MSW 2.0.
安装
¥Installation
npm install msw@latest
重大更改
¥Breaking changes
环境
¥Environment
Node.js 版本
¥Node.js version
此版本将支持的最低 Node.js 版本设置为 18.0.0。
¥This release sets the minimal supported Node.js version to 18.0.0.
不再支持 Node.js 18 之前的 Node.js 版本。要使用 MSW 的下一个版本,请迁移到 Node.js 18 或更高版本。
¥Node.js versions prior to Node.js 18 are no longer supported. To use the next versions of MSW, please migrate to Node.js 18 or higher.
TypeScript 版本
¥TypeScript version
此版本将支持的最低 TypeScript 版本设置为 4.7。如果你使用的是旧版 TypeScript,请迁移到 4.7 或更高版本以使用 MSW。请考虑,在撰写本文时,TypeScript 4.6 已有近两年的历史。
¥This release sets the minimal supported TypeScript version to 4.7. If you are using an older TypeScript version, please migrate to version 4.7 or later to use MSW. Please consider that at the moment of writing this TypeScript 4.6 is almost two years old.
导入
¥Imports
Worker 导入
¥Worker imports
现在从 msw/browser
入口点导出与浏览器端集成相关的所有内容。这包括 setupWorker
函数和相关类型定义。
¥Everything related to the browser-side integration is now exported from the msw/browser
entrypoint. This includes both the setupWorker
function and the relevant type definitions.
之前:
¥Before:
import { setupWorker } from 'msw'
之后:
¥After:
import { setupWorker } from 'msw/browser'
响应解析器参数
¥Response resolver arguments
响应解析器函数不再接受 req
、res
和 ctx
参数。相反,它接受一个参数,该参数是一个包含有关被拦截请求的信息的对象。
¥Response resolver function no longer accepts req
, res
, and ctx
arguments. Instead, it accepts a single argument which is an object containing information about the intercepted request.
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {})
之后:
¥After:
http.get('/resource', (info) => {})
根据所使用的处理程序命名空间(http
或 graphql
),info
对象包含不同的属性。你现在可以在 请求更改 中了解如何访问请求信息。
¥Depending on the handler namespace used (http
or graphql
), the info
object contains different properties. You can learn about how to access request information now in the Request changes.
了解有关请求处理程序命名空间的更新调用签名的更多信息:
¥Learn more about the updated call signature of the request handler namespaces:
http
API reference for the `http` namespace.
graphql
API reference for the `graphql` namespace.
请求更改
¥Request changes
请求 URL
¥Request URL
由于拦截的请求现在被描述为 Fetch API Request
实例,其 request.url
属性不再是 URL
实例,而是普通的 string.
¥Since the intercepted request is now described as a Fetch API Request
instance, its request.url
property is no longer a URL
instance but a plain string.
之前:
¥Before:
rest.get('/resource', (req) => {
const productId = req.url.searchParams.get('id')
})
之后:
¥After:
如果你希望将其作为 URL
实例进行操作,则应首先从 request.url
字符串创建它。
¥If you wish to operate with it as a URL
instance, you should create it first from the request.url
string.
import { http } from 'msw'
http.get('/resource', ({ request }) => {
const url = new URL(request.url)
const productId = url.searchParams.get('id')
})
请求参数
¥Request params
路径参数不再在 req.params
下公开。
¥Path parameters are no longer exposed under req.params
.
之前:
¥Before:
rest.get('/post/:id', (req) => {
const { id } = req.params
})
之后:
¥After:
要访问路径参数,请在响应解析器上使用 params
对象。
¥To access path parameters, use the params
object on the response resolver.
import { http } from 'msw'
http.get('/post/:id', ({ params }) => {
const { id } = params
})
请求 cookies
¥Request cookies
请求 cookie 不再在 req.cookies.
下公开
¥Request cookies are no longer exposed under req.cookies.
之前:
¥Before:
rest.get('/resource', (req) => {
const { token } = req.cookies
})
之后:
¥After:
要访问请求 cookie,请在响应解析器上使用 cookies
对象。
¥To access request cookies, use the cookies
object on the response resolver.
import { http } from 'msw'
http.get('/resource', ({ cookies }) => {
const { token } = cookies
})
请求正文
¥Request body
你无法再通过 req.body
属性读取被拦截的请求主体。事实上,根据 Fetch API 规范,如果设置了主体,request.body
现在将返回 ReadableStream
。
¥You can no longer read the intercepted request body via the req.body
property. In fact, according to the Fetch API specification, request.body
will now return a ReadableStream
if the body is set.
之前:
¥Before:
rest.post('/resource', (req) => {
// The library would assume a JSON request body
// based on the request's "Content-Type" header.
const { id } = req.body
})
之后:
¥After:
MSW 将不再假设请求主体类型。相反,你应该使用标准 Request
方法(如 .text()
、.json()
、.arrayBuffer()
等)根据需要读取请求主体。
¥MSW will no longer assume the request body type. Instead, you should read the request body as you wish using the standard Request
methods like .text()
, .json()
, .arrayBuffer()
, etc.
import { http } from 'msw'
http.post('/user', async ({ request }) => {
// Read the request body as JSON.
const user = await request.json()
const { id } = user
})
响应声明
¥Response declaration
模拟响应不再使用 res()
组合函数声明。我们正在脱离组合方法,转而遵守 Web 标准。
¥Mocked responses are no longer declared using the res()
composition function. We are departing from the composition approach in favor of adhering to the web standards.
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.json({ id: 'abc-123' }))
})
之后:
¥After:
要声明模拟响应,请创建一个 Fetch API Response
实例并从响应解析器返回它。
¥To declare a mocked response, create a Fetch API Response
instance and return it from the response resolver.
import { http } from 'msw'
http.get('/resource', () => {
return new Response(JSON.stringify({ id: 'abc-123' }), {
headers: {
'Content-Type': 'application/json',
},
})
})
为了提供更简洁的接口并支持模拟响应 cookie 等功能,该库现在提供了一个自定义 HttpResponse
类,你可以将其用作原生 Response
类的替代品。
¥To provide a less verbose interface and also support such features as mocking response cookies, the library now provides a custom HttpResponse
class that you can use as a drop-in replacement for the native Response
class.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.json({ id: 'abc-123' })
}),
]
了解有关新 HttpResponse
API 的更多信息:
¥Learn more about the new HttpResponse
API:
HttpResponse
API reference for the `HttpResponse` class.
req.passthrough()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return req.passthrough()
})
之后:
¥After:
import { http, passthrough } from 'msw'
export const handlers = [
http.get('/resource', () => {
return passthrough()
}),
]
res.once()
由于 res()
组合 API 已消失,res.once()
一次性请求处理程序声明也已消失。
¥Since the res()
composition API is gone, so is the res.once()
one-time request handler declaration.
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res.once(ctx.text('Hello world!'))
})
之后:
¥After:
要声明一次性请求处理程序,请提供一个对象作为它的第三个参数,并将该对象的 once
属性设置为 true
。
¥To declare a one-time request handler, provide an object as the third argument to it, and set the once
property of that object to true
.
import { http, HttpResponse } from 'msw'
http.get(
'/resource',
() => {
return new HttpResponse('Hello world!')
},
{ once: true }
)
res.networkError()
要模拟网络错误,请调用 HttpResponse.error()
静态方法并从响应解析器返回它。
¥To mock a network error, call the HttpResponse.error()
static method and return it from the response resolver.
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res.networkError('Custom error message')
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.error()
})
请注意,
Response.error()
不接受自定义错误消息。以前,MSW 会尽最大努力将你提供的自定义错误消息强制传递给底层请求客户端,但它从未可靠地工作过,因为这取决于请求客户端是否处理或忽略网络错误消息。¥Note that the
Response.error()
doesn’t accept a custom error message. Previously, MSW did its best to coerce the custom error message you provided to the underlying request client but it never worked reliably because it’s up to the request client to handle or disregard the network error message.
上下文实用程序
¥Context utilities
在此版本中,我们将弃用 ctx
实用程序对象。相反,使用 HttpResponse
类来声明模拟响应属性,如状态、标题或正文。
¥With this release we are deprecating the ctx
utilities object. Instead, use the HttpResponse
class to declare mocked response properties, like status, headers, or body.
ctx.status()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.status(201))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
status: 201,
})
})
ctx.set()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.set('X-Custom-Header', 'foo'))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
headers: {
'X-Custom-Header': 'foo',
},
})
})
了解标准
Headers
API。¥Learn about the standard
Headers
API.
ctx.cookie()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.cookie('token', 'abc-123'))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
headers: {
'Set-Cookie': 'token=abc-123',
},
})
})
库能够检测到你何时通过 HttpResponse
类模拟响应 cookie。如果你希望模拟响应 cookie,则必须使用该类,因为在设置响应 cookie 后,无法在原生 Response
类上读取它们。
¥The library is able to detect whenever you are mocking response cookies via the HttpResponse
class. If you wish to mock response cookies, you must use that class, since response cookies cannot be read on the native Response
class after they are set.
ctx.body()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.body('Hello world'), ctx.set('Content-Type', 'text/plain'))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', (req, res, ctx) => {
return new HttpResponse('Hello world')
})
ctx.text()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.text('Hello world!'))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse('Hello world!')
})
ctx.json()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.json({ id: 'abc-123' }))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.json({ id: 'abc-123' })
})
请注意,使用静态
HttpResponse
方法(如HttpResponse.text()
、HttpResponse.json()
等)时,你不必明确指定Content-Type
响应标头。¥Note that you don’t have to explicitly specify the
Content-Type
response header when using staticHttpResponse
methods likeHttpResponse.text()
,HttpResponse.json()
, and others.
ctx.xml()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.xml('<foo>bar</foo>'))
})
之后:
¥After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.xml('<foo>bar</foo>')
})
ctx.data()
之前:
¥Before:
graphql.query('GetUser', (req, res, ctx) => {
return res(
ctx.data({
user: {
firstName: 'John',
},
})
)
})
之后:
¥After:
graphql
处理程序命名空间不再得到特殊处理。相反,你应该直接声明标准 JSON 响应。
¥The graphql
handler namespace no longer gets a special treatment. Instead, you should declare standard JSON responses directly.
为了使 GraphQL 操作的模拟响应定义更舒适,请使用 HttpResponse.json()
静态方法:
¥To make the mocked response definition for GraphQL operations more comfortable, use the HttpResponse.json()
static method:
import { graphql, HttpResponse } from 'msw'
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
})
})
使用
HttpResponse
,你必须在响应中明确包含根级data
属性。¥Using
HttpResponse
, you have to explicitly include the root-leveldata
property on the response.
ctx.errors()
之前:
¥Before:
graphql.mutation('Login', (req, res, ctx) => {
const { username } = req.variables
return res(
ctx.errors([
{
message: `Failed to login: user "${username}" does not exist`,
},
])
)
})
之后:
¥After:
import { graphql, HttpResponse } from 'msw'
graphql.mutation('Login', ({ variables }) => {
const { username } = variables
return HttpResponse.json({
errors: [
{
message: `Failed to login: user "${username}" does not exist`,
},
],
})
})
使用
HttpResponse
,你必须在响应中明确包含errors
根级属性。¥Using
HttpResponse
, you have to explicitly include theerrors
root-level property on the response.
ctx.extensions()
之前:
¥Before:
graphql.query('GetUser', (req, res, ctx) => {
return res(
ctx.data({
user: {
firstName: 'John',
},
}),
ctx.extensions({
requestId: 'abc-123',
})
)
})
之后:
¥After:
import { graphql, HttpResponse } from 'msw'
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
extensions: {
requestId: 'abc-123',
},
})
})
ctx.delay()
之前:
¥Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.delay(500), ctx.text('Hello world'))
})
之后:
¥After:
库现在导出返回超时 Promise
的 delay()
函数。你可以在响应解析器中的任何位置等待它以模拟服务器端延迟。
¥The library now exports the delay()
function that returns a timeout Promise
. You can await it anywhere in your response resolvers to emulate server-side delay.
import { http, HttpResponse, delay } from 'msw'
http.get('/resource', async () => {
await delay(500)
return HttpResponse.text('Hello world')
})
delay()
函数的调用签名与之前的 ctx.delay()
相同。
¥The call signature of the delay()
function remains identical to the previous ctx.delay()
.
delay
API reference for the `delay` function.
ctx.fetch()
之前:
¥Before:
rest.get('/resource', async (req, res, ctx) => {
const originalResponse = await ctx.fetch(req)
const originalJson = await originalResponse.json()
return res(
ctx.json({
...originalJson,
mocked: true,
})
)
})
之后:
¥After:
要在处理程序中执行其他请求,请使用从 msw
导出的新 bypass
函数。此函数封装任何给定的 Request
实例,将其标记为 MSW 在拦截请求时应忽略的实例。
¥To perform an additional request within the handler, use the new bypass
function exported from msw
. This function wraps any given Request
instance, marking it as the one MSW should ignore when intercepting requests.
import { http, HttpResponse, bypass } from 'msw'
http.get('/resource', async ({ request }) => {
const originalResponse = await fetch(bypass(request))
const originalJson = await originalResponse.json()
return HttpResponse.json({
...originalJson,
mocked: true,
})
})
bypass
API reference for the `bypass` function.
printHandlers()
worker
/server
上的 .printHandlers()
方法已被删除,取而代之的是新的 .listHandlers()
方法。
¥The .printHandlers()
method on worker
/server
has been removed in favor of the new .listHandlers()
method.
之前:
¥Before:
worker.printHandlers()
之后:
¥After:
新的 .listHandlers()
方法返回当前活动的请求处理程序的只读数组。
¥The new .listHandlers()
method returns a read-only array of currently active request handlers.
worker.listHandlers().forEach((handler) => {
console.log(handler.info.header)
})
onUnhandledRequest
onUnhandledRequest
的 request
参数已从抽象请求对象更改为 Fetch API Request
实例。在访问其属性(如 request.url
)时请考虑到这一点。
¥The request
argument of the onUnhandledRequest
has changed from being an abstract request object to be a Fetch API Request
instance. Take that into account when accessing its properties, like request.url
.
之前:
¥Before:
server.listen({
onUnhandledRequest(request, print) {
const url = request.url
if (url.pathname.includes('/assets/')) {
return
}
print.warning()
},
})
之后:
¥After:
request
参数是 Request
的一个实例,这使得它的 url
属性成为 string
。
¥The request
argument is an instance of Request
, which makes its url
property a string
.
server.listen({
onUnhandledRequest(request, print) {
// Create a new URL instance manually.
const url = new URL(request.url)
if (url.pathname.includes('/assets/')) {
return
}
print.warning()
},
})
生命周期事件
¥Life-cycle events
此版本对 生命周期事件 监听器的调用签名进行了更改。
¥This release brings changes to the Life-cycle events listeners’ call signature.
之前:
¥Before:
server.events.on('request:start', (request, requestId) => {})
之后:
¥After:
每个生命周期事件监听器现在都接受一个对象参数。
¥Every life-cycle event listener now accepts a single argument being an object.
server.events.on('request:start', ({ request, requestId }) => {})
新 API
¥New API
除了重大更改之外,此版本还引入了一系列新 API。它们中的大多数都专注于提供与已弃用功能的兼容性。
¥In addition to the breaking changes, this release introduces a list of new APIs. Most of them are focused on providing compatibility with the deprecated functionality.
常见问题
¥Frequent issues
Request
/Response
/TextEncoder
未定义(Jest)
¥Request
/Response
/TextEncoder
is not defined (Jest)
在继续阅读之前,请确保你使用的是 Node.js v18 或更新版本。
¥Make sure you are using Node.js v18 or newer before reading further.
此问题是由于你的环境由于某种原因没有 Node.js 全局变量而导致的。这通常发生在 Jest 中,因为它故意剥夺了你的 Node.js 全局变量,并且无法完全重新添加它们。因此,你必须自己明确添加它们。
¥This issue is caused by your environment not having the Node.js globals for one reason or another. This commonly happens in Jest because it intentionally robs you of Node.js globals and fails to re-add them in their entirely. As the result, you have to explicitly add them yourself.
在你的 jest.config.js
旁边创建一个 jest.polyfills.js
文件,内容如下:
¥Create a jest.polyfills.js
file next to your jest.config.js
with the following content:
// jest.polyfills.js
/**
* @note The block below contains polyfills for Node.js globals
* required for Jest to function when running JSDOM tests.
* These HAVE to be require's and HAVE to be in this exact
* order, since "undici" depends on the "TextEncoder" global API.
* * Consider migrating to a more modern test runner if
* you don't want to deal with this.
*/
const { TextDecoder, TextEncoder } = require('node:util')
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
})
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
Object.defineProperties(globalThis, {
fetch: { value: fetch, writable: true },
Blob: { value: Blob },
File: { value: File },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
})
确保安装
undici
。它是 Node.js 中的官方获取实现。¥Make sure to install
undici
. It’s the official fetch implementation in Node.js.
然后,将 jest.config.js
中的 setupFiles
选项设置为指向新创建的 jest.polyfills.js
:
¥Then, set the setupFiles
option in jest.config.js
to point to the newly created jest.polyfills.js
:
// jest.config.js
module.exports = {
setupFiles: ['./jest.polyfills.js'],
}
请注意,这是 setupFiles
选项,而不是 setupFilesAfterEnv
。缺少的 Node.js 全局变量必须在环境之前注入(例如 JSDOM)。
¥Pay attention it’s the setupFiles
option, and not setupFilesAfterEnv
.
The missing Node.js globals must be injected before the environment (e.g.
JSDOM).
如果你发现此设置很麻烦,请考虑迁移到现代测试框架,例如 Vitest,它没有任何 Node.js 全局变量问题并提供开箱即用的原生 ESM 支持。
¥If you find this setup cumbersome, consider migrating to a modern testing framework, like Vitest, which has none of the Node.js globals issues and provides native ESM support out of the box.
找不到模块 ‘msw/node’ (JSDOM)
¥Cannot find module ‘msw/node’ (JSDOM)
此错误由你的测试运行器抛出,因为 JSDOM 默认使用 browser
导出条件。这意味着当你导入任何第三方包(如 MSW)时,JSDOM 会强制将其 browser
导出用作入口点。这是不正确且危险的,因为 JSDOM 仍在 Node.js 中运行,并且无法保证完全的浏览器兼容性。
¥This error is thrown by your test runner because JSDOM uses the browser
export condition by default. This means that when you import any third-party packages, like MSW, JSDOM forces its browser
export to be used as the entrypoint. This is incorrect and dangerous because JSDOM still runs in Node.js and cannot guarantee full browser compatibility by design.
要解决此问题,请将 jest.config.js
中的 testEnvironmentOptions.customExportConditions
选项设置为 ['']
:
¥To fix this, set the testEnvironmentOptions.customExportConditions
option in your jest.config.js
to ['']
:
// jest.config.js
module.exports = {
testEnvironmentOptions: {
customExportConditions: [''],
},
}
这将强制 JSDOM 在导入 msw/node
时使用默认导出条件,从而实现正确的导入。
¥This will force JSDOM to use the default export condition when importing msw/node
, resulting in correct imports.
multipart/form-data is not supported
Node.js 中的错误
¥multipart/form-data is not supported
Error in Node.js
早期版本的 Node.js,如 v18.8.0,没有对 request.formData()
的官方支持。请升级到最新的 Node.js 18.x,其中已添加此类支持。
¥Earlier versions of Node.js, like v18.8.0, didn’t have official support for request.formData()
. Please upgrade to the latest Node.js 18.x where such a support has been added.