12k
All articles

使用 MSW 3 模拟页面导航与表单提交

使用 MSW 3 的 http 处理器模拟页面导航和原生表单提交,了解 HTML 响应、formData 解析及升级时的关键变更。

OpenReplay Team
OpenReplay Team
使用 MSW 3 模拟页面导航与表单提交

Mock Service Worker(MSW)3.0.0 于 2026 年 9 月 28 日发布,新增了对拦截页面导航和表单提交的支持(#2721)。这意味着,现在点击链接或提交原生 HTML 表单时,同样可以通过你已经在编写的 http handler 获得模拟响应。

在此版本之前,在 Storybook 或 Playwright 环境中,原生表单提交会完全绕过 MSW。要测试这类流程,你要么专门为它运行一个真实后端,要么干脆跳过测试。在一次 2023 年的讨论中,维护者表示 MSW 是有意跳过导航请求的,目的是防止模拟响应被误当作真实 HTML 页面返回。

本文将介绍这项变更让哪些场景变得可测试、升级时最先会遇到的 v3 破坏性变更,以及针对页面和表单提交的 handler 编写模式。补丁版本 3.0.3 已于 2026 年 10 月 10 日发布。关于如何安装 MSW 并编写第一个 handler,请参阅使用 Mock Service Worker 模拟 API 服务器。

核心要点

  • MSW 3.0.0(2026 年 9 月 28 日)新增了对页面导航和表单提交的拦截,但官方并未提供专门的 API 文档,因此需要使用标准的 http.get 和 http.post handler。
  • 原生 HTML 表单以 URL 编码格式而非 JSON 格式发送字段,因此应使用 request.formData() 而不是 request.json() 读取请求体。
  • MSW 3 仅支持 ESM,要求 Node.js 22 及以上版本、TypeScript 5.9 及以上版本。
  • onUnhandledRequest 更名为 onUnhandledFrame,且在 handler 内部 request.headers.get('cookie') 现在返回 null。

MSW 3 的导航与表单模拟能实现什么?

借助 MSW 3,你无需运行后端即可测试服务端渲染的表单或指向其他页面的链接。handler 返回下一个页面的 HTML 文档,浏览器会像渲染真实服务器响应一样渲染它。

在版本 3 之前,MSW 只能覆盖由 JavaScript 通过 fetch 或 XHR 发起的请求。渐进增强的注册表单、会重新加载页面的搜索表单,以及指向服务端渲染页面的”查看详情”链接,都不在其覆盖范围之内。现在,你可以将这些场景分别描述为 handler,并对用户最终看到的内容进行断言。

受益最大的是错误路径。表单流程的会话回放经常显示,用户点击提交后会跳转到空白页面或原始错误页面。通过模拟一个返回校验错误 HTML 或 500 状态码的 POST 请求,你可以在发布前就在测试中复现该页面。

升级到 MSW 3:最先遇到的破坏性变更

大多数测试套件最先出问题的地方,是运行时最低版本要求和一个被重命名的选项。在了解新功能之前,请先检查 v3.0.0 发布说明中的以下变更:

  • MSW 现在仅支持 ESM(#2765)。
  • 最低支持的 Node.js 版本为 v22(#2729)。
  • TypeScript 最低版本要求为 5.9。
  • graphql 现在通过 msw/graphql 导出(#2691),GraphQL 也已成为真正意义上的可选 peer dependency。此外,GraphQL handler 现在需要在 graphql.link() 上定义,因为 graphql.query() 和 graphql.mutation() 已从该命名空间中移除。
  • onUnhandledRequest 选项已更名为 onUnhandledFrame。
  • worker.stop() 现在返回一个 Promise(#2724)。只有在仍有请求处于进行中的关键场景下,才需要 await 它。
  • msw/native 已被移除,改用 @msw/react-native。一个 GitHub issue 指出,@msw/react-native 尚未发布到 npm,且其源码仍以 MSW 2 为目标,因此在它正式发布之前,React Native 项目暂无可行的升级路径。

其余变更请参阅发布说明和 2.x → 3.x 迁移指南。本文中的代码片段使用新的 msw/http 入口。根据 MSW 3.0 发布公告,为保持向后兼容,根级 msw 导出仍然保留,但下一个主版本将移除所有可以从指定入口导入的内容。

如何使用 http.get 模拟页面导航?

要模拟页面导航,需要为页面路径编写一个 http.get handler,并返回一个带有 HTML 响应体和 text/html 内容类型的 Response。v3 发布说明虽然宣布了导航和表单拦截功能,但无论是发布说明还是迁移指南,都没有记录专门的 API 或配置步骤。因此,下面的示例仅使用标准 handler。

每个 MSW 请求 handler 都由两部分组成:一个用于过滤传入请求的谓词(predicate),以及一个用于处理通过过滤的请求的响应解析器(response resolver)。请求拦截文档指出,像 /signup 这样的相对谓词会基于 location.href 进行解析。正如请求处理文档所述,如果解析器返回一个 Response 实例,该实例即成为模拟响应。这就是为什么这里直接使用 new Response 即可,无需任何 HTML 专用的辅助函数。

import { http } from 'msw/http'

export const html = (body: string, status = 200) =>
  new Response(`<!doctype html><html><body>${body}</body></html>`, {
    status,
    headers: { 'Content-Type': 'text/html' },
  })

export const pageHandlers = [
  http.get('/signup', () =>
    html(`<form method="post" action="/signup">
            <input name="email" type="email">
            <button>Sign up</button>
          </form>`),
  ),
]

使用 method="get" 的搜索表单行为有所不同。以 method="get" 提交的表单会将字段放在查询字符串中。由于 MSW 谓词中不能包含查询参数,因此只需匹配路径,然后从 new URL(request.url).searchParams 中读取字段:

http.get('/search', ({ request }) => {
  const q = new URL(request.url).searchParams.get('q') ?? ''
  const safe = q.replace(/&/g, '&amp;').replace(/</g, '&lt;')
  return html(`<h1>Results for ${safe}</h1>`)
})

如何使用 http.post 模拟表单提交?

原生表单提交由针对表单 action URL 的 http.post handler 处理。解析器接收的 request 是一个标准的 Fetch API Request,同时还会接收 params 和 cookies。解析器返回的任何 Response 都将成为浏览器接下来显示的页面。

与你熟悉的 MSW 2 模式相比,唯一的区别在于请求体格式。当由你自己的 JavaScript 提交表单时,通常发送的是 JSON:

import { http, HttpResponse } from 'msw/http'

http.post<never, { email: string }>('/api/signup', async ({ request }) => {
  const body = await request.clone().json()
  return HttpResponse.json({ ok: true, email: body.email }, { status: 201 })
})

而原生 HTML 表单则以 URL 编码的表单数据而非 JSON 发送字段,因为 application/x-www-form-urlencoded 是默认的表单编码方式。对这种请求体调用 .json() 会失败,应改用 Request.formData() 读取。MSW 的文件上传指南中也展示了在被拦截的请求上使用 formData() 的方式。

import { http } from 'msw/http'
import { html } from './pages'

export const formHandlers = [
  http.post('/signup', async ({ request }) => {
    const form = await request.clone().formData()
    const email = form.get('email')

    if (!email) {
      return html('<p role="alert">Email is required.</p>', 422)
    }

    return html('<h1>Check your inbox</h1>')
  }),
]

为每一种需要测试的服务端结果分别返回一个独立分支:成功、校验失败以及 500 错误页面。这样,你的断言就能覆盖用户实际看到的文档。

表单流程通常依赖会话 cookie。在 v3 中,handler 内部的 request.headers.get('cookie') 不再返回模拟的 cookie 字符串,而是在所有环境中都返回 null。因此,任何基于 cookie 的逻辑都应使用解析器的 cookies 参数。

MSW 3 的导航与表单拦截在哪些环境中运行?

v3 发布说明并未说明哪些环境支持导航和表单拦截,因此在依赖该功能之前,请对照 PR #2721 和官方文档,在你自己的环境中进行验证。页面导航是由浏览器执行的行为,因此基于浏览器的测试运行器是尝试这些 handler 的理想场所。

这一点可以从 MSW 的历史中找到原因。正如维护者在 Discussion #1578 中所解释的,在版本 3 之前,worker 脚本会跳过 mode 为 navigate 的请求。如果你的 Vitest 或 Jest 测试套件在 Node 中运行组件测试(例如使用 Vitest 在 Vue 测试中模拟 API 调用一文中的做法),fetch 模拟仍会像以前一样正常工作,但不要想当然地认为原生表单提交在该环境中也会有相同表现。此外,该版本还新增了一个 Vite 插件 msw/vite(#2781),它会自动为你提供 worker 脚本,因此你不再需要将 mockServiceWorker.js 复制到 public 目录中。

总结

MSW 3 在页面和表单模拟方面没有引入任何新概念:用 http.get 处理页面,用 http.post 处理提交,用 formData() 读取请求体,并用带有 text/html 的 Response 返回结果。建议按以下顺序升级:先迁移到 Node.js 22、TypeScript 5.9 和 ESM,再将 onUnhandledRequest 重命名为 onUnhandledFrame,最后将导入切换为 msw/http。完成之后,为你最重要的表单的每一种结果添加 handler,并从错误页面开始。

常见问题

request.formData() 是否适用于使用 enctype multipart/form-data 的表单?

适用。Request.formData() 可以将 application/x-www-form-urlencoded 和 multipart/form-data 两种请求体都解析为 FormData 对象,因此无论表单是否包含文件输入框,都可以使用相同的 handler 代码。通过 form.get() 获取的文本字段为字符串,而文件输入则返回 File 对象,你可以检查其 name、size 和 type 属性。

如何在表单 handler 中读取复选框组或多选字段?

使用 form.getAll('name') 而不是 form.get('name')。复选框组或带有 multiple 属性的 select 会以相同的字段名为每个选中值发送一个条目,而 get() 只返回第一个。getAll() 会返回包含所有值的数组;若未选中任何项,则返回空数组,因为浏览器根本不会发送未勾选的复选框。

在 MSW 解析器中返回 passthrough() 与不返回任何内容有什么区别?

返回 passthrough()(在 MSW 3 中从 msw/utils/passthrough 导入)会将请求原样发送到真实网络,并将其标记为已处理,因此后续任何 handler 都无法再影响它。而不返回任何内容则不会对请求做任何处理,MSW 会继续尝试下一个匹配的 handler。如果你希望某个特定页面或表单提交有意地发送到真实服务器,请使用 passthrough()。

升级到 MSW 3 后是否需要重新生成 mockServiceWorker.js?

需要,除非你使用了新的 msw/vite 插件,它会自动为你提供 worker 脚本。否则,在安装 MSW 3 之后,请运行 npx msw init 并在后面加上你的 public 目录,然后提交新生成的文件,以确保 worker 脚本与 3.x 版本的包相匹配,而不是沿用旧的 2.x 副本。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.