网络请求拦截 route
本教程共 59 篇 · 第 30 篇 · 更新于 2026-08-04 · 约 12 分钟阅读
30. 网络请求拦截 route
本节目标:学完能拦截页面发出的任意网络请求,并决定让它放行、改掉还是直接拦下。
写自动化测试,最怕页面依赖外部接口。接口慢、不稳定、还常常超时,测试一下午跑不通。Playwright 给了你一把剪刀,能在请求出门前就拦住它。这把剪刀叫 page.route()。
为什么需要拦截请求
浏览器打开一个页面,背后会发起几十上百个请求:HTML、CSS、图片、接口数据。这些请求里有的是咱们的测试重点,有的纯粹是干扰。
举个例子,你想测一个列表页能不能正确渲染。可页面每次都去拉一张大图,图加载慢就把测试拖垮了。这时候把图片请求拦了,测试又快又稳。
Note
route(路由拦截)能监听、修改、甚至替浏览器伪造任意一个请求,HTTP 和 HTTPS 都行,包括 XHR 和 fetch。
三种处理方式
page.route() 接收一个 URL 匹配规则,和一个处理函数。处理函数里拿到的 route(路由)对象,有三个最常用的方法:
route.continue():按原样放行,也可以改头改尾再放行。route.fulfill():自己造一个响应返回,请求根本不发出去。route.abort():直接掐断,告诉浏览器请求失败。
这三个方法互斥。一条请求只能挑一个调,调完就定案了。
拦截 URL 的规则
匹配规则有两种写法。一种是 glob 通配符,上手简单:
// 拦掉所有 .css 文件
await page.route('**/*.css', route => route.abort());
// 一次拦多种图片格式,用花括号列选项
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
另一种是正则,逻辑更灵活:
// 拦截 URL 以 png 或 jpeg 结尾的请求
await page.route(/(png|jpeg)$/, route => route.abort());
glob 的规则就四条,记住少踩坑:
- 单个
*匹配任意字符,但不跨斜杠。 - 双
**匹配任意字符,包括斜杠。 - 花括号
{a,b}表示「其中之一」。 - 反斜杠
\用来转义特殊字符。
Tipglob 必须匹配整个 URL,不是匹配其中一段。所以
*.js命中不了https://example.com/path/file.js,得写**/*.js。规则再复杂一点,就别硬凑 glob 了,直接换正则。
拦下静态资源,让测试更稳
下面这个例子在每一条测试前拦掉 CSS,页面照样能测,渲染细节先不管。
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ context }) => {
// 每个测试前都拦掉 css 请求
await context.route('**/*.css', route => route.abort());
});
test('没有样式也能加载页面', async ({ page }) => {
await page.goto('https://playwright.dev');
// 这里测试页面结构,不关心样式
});
只拦当前页面,用 page.route();想连弹窗、新标签页一起拦,用 context.route()。两个都命中同一条请求时,page 上的先跑。
放行时顺手改一改
有时候请求得发出去,但你想动动手脚。比如删掉一个请求头:
await page.route('**/*', async route => {
const headers = route.request().headers();
delete headers['X-Secret']; // 删掉敏感请求头
await route.continue({ headers });
});
也能把请求改成 POST 再放行:
await page.route('**/*', route => route.continue({ method: 'POST' }));
直接掐断请求
图片太多拖慢测试,直接 abort:
// 按扩展名拦
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
// 或者按资源类型判断,比列扩展名靠谱
await page.route('**/*', route => {
return route.request().resourceType() === 'image'
? route.abort()
: route.continue();
});
resourceType() 返回的是浏览器自己的分类:document、stylesheet、image、script、xhr、fetch 等。按类型拦比拼扩展名准,因为很多接口 URL 上根本没有后缀。
造一个假响应
fulfill() 是三兄弟里最有用的一个:请求根本不出门,你直接把响应递给浏览器。
await page.route('**/api/user', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: '码上学', vip: true }),
}));
这一招的完整玩法是下一章的主题,这里先混个脸熟。
多个 route 撞车了怎么办
这是新手最容易翻车的地方。
Warning多个 route 命中同一条请求时,执行顺序是注册顺序的反过来——最后注册的那个先跑。
这么设计是有道理的:后写的规则能覆盖前面的。夹具里先铺一层通用拦截,测试里再写一条更具体的,具体的自然排在前面。
await page.route('**/*', route => route.abort()); // 注册第一,最后跑
await page.route('**/api/**', route => route.continue()); // 注册第二,先跑
上面这段里,接口请求会被第二条放行,压根轮不到第一条。
想让前面注册的处理函数也有机会跑,别用 continue(),改用 fallback()——「这单我接不了,往下传」。
// 注册第一:兜底,负责 GET
await page.route('**/api/**', async route => {
if (route.request().method() !== 'GET')
return route.fallback();
await route.fulfill({ json: { list: [] } });
});
// 注册第二:先跑,只管 POST
await page.route('**/api/**', async route => {
if (route.request().method() !== 'POST')
return route.fallback(); // 不是 POST,交给上面那个
await route.fulfill({ status: 201 });
});
continue() 是「我拍板了,直接发出去」,fallback() 是「我处理不了,往下传」。分清这两个,链式拦截就不会乱。
用完记得清掉
拦截规则会一直挂着。在夹具或 beforeEach 里注册的规则,容易漏到下一个测试里去,制造出莫名其妙的偶发失败。
只想拦一次,加 times:
await page.route('**/api/config', route => route.fulfill({ json: { beta: true } }), { times: 1 });
手动清理有两个方法:
await page.unroute('**/api/config'); // 清掉这个规则下的所有处理函数
await page.unrouteAll({ behavior: 'wait' }); // 全清,并等正在跑的处理函数收尾
Tip在夹具的收尾阶段清理时,
behavior: 'wait'是最稳的选择。直接清可能让还在执行的处理函数抛出没人接的异常。
什么时候该用它
请求拦截适合这几类场景:屏蔽无关资源提速、模拟接口异常、构造测试数据。它和下一章的 Mock(模拟响应)、第 32 章的响应断言是成套的组合拳。
我的原则是:能不改真实接口就别改。先想清楚到底要拦什么,别一上来就 **/* 全拦,那样测出来的东西离真实环境太远。
小结
page.route()拦当前页面,context.route()连弹窗和新标签一起拦,两者都命中时 page 的先跑。- 处理函数里三选一:
continue()放行、fulfill()造假响应、abort()掐断。 - URL 匹配用 glob 或正则;glob 里
*不跨斜杠、**跨斜杠,且必须匹配整个 URL。 - 按
resourceType()拦资源,比拼扩展名可靠。 - 多个 route 撞车时,后注册的先跑;想往下传用
fallback(),不是continue()。 - 规则会一直挂着,用
times限次或unroute/unrouteAll清理,避免污染后续测试。
下一章把 fulfill() 展开讲透,学会用假数据完全替掉后端。