Playwright网络操作指南:从监听、拦截到Mock与性能测试实战
1. 项目概述为什么网络操作是Playwright的“王牌”如果你用过Selenium或者Puppeteer可能会觉得自动化测试的核心就是“定位元素”和“模拟点击”。但当你开始用Playwright处理现代Web应用时很快就会发现真正的“硬骨头”往往藏在网络请求里。页面加载慢、接口数据不对、第三方资源阻塞、甚至需要模拟特定网络环境……这些问题单纯靠UI操作是搞不定的。这就是为什么Playwright把网络相关的API做得如此强大和精细它让你能像外科医生一样精准地观察、干预甚至“伪造”整个浏览器的网络活动。我最近在做一个电商平台的自动化巡检项目就深刻体会到了这一点。页面上的一个“加载中”转圈圈背后可能是某个商品详情接口挂了或者一个巨大的营销活动图片没加载出来。如果只检查页面元素你只能得到一个“页面显示异常”的模糊结论。但如果你能监听网络请求就能立刻定位到是哪个具体的API返回了500错误或者哪个静态资源下载超时了。这种从“现象”到“根因”的直达能力是提升自动化脚本健壮性和排查效率的关键。简单来说Playwright的网络操作指南就是教你如何用好这套“监听与干预”系统。它不仅仅是用来做Mock测试的更是你进行性能分析、安全测试、兼容性验证甚至是爬虫开发的利器。接下来我会结合大量实战代码带你从监听、拦截、修改、Mock到高级代理配置把Playwright的网络能力彻底摸透。2. 核心能力全景Playwright网络API的四大支柱Playwright的网络API体系可以清晰地划分为四个核心功能模块它们共同构成了对浏览器网络行为的全方位控制。理解这个结构能帮助你在遇到具体问题时快速找到正确的工具。2.1 监听与观察成为网络的“旁观者”这是最基础也是最常用的功能。在你动手修改任何东西之前你得先知道浏览器里发生了什么。Playwright提供了非常灵活的事件监听机制。核心APIpage.on(request)和page.on(response)这两个事件让你能捕获到每一个HTTP请求和响应。一个常见的用法是在测试开始时挂上监听器用于调试或收集性能数据。// 监听所有请求和响应 page.on(request, request { console.log( 请求发出: ${request.method()} ${request.url()}); // 你甚至可以打印出请求头 // console.log(Headers:, request.headers()); }); page.on(response, response { console.log( 收到响应: ${response.status()} ${response.url()}); // 对于重要的API你还可以检查响应体注意这可能会影响性能 // if (response.url().includes(/api/user)) { // response.json().then(body console.log(User data:, body)); // } }); await page.goto(https://your-app.com);实战技巧过滤噪音现代页面请求太多全部打印会刷屏。我通常会用URL或资源类型进行过滤。// 只监听API请求 page.on(request, request { if (request.url().includes(/api/) || request.url().includes(/graphql)) { console.log(API Request: ${request.method()} ${request.url()}); } }); // 只监听图片或样式表等特定资源 page.on(response, response { const type response.request().resourceType(); if (type image response.status() ! 200) { console.warn(图片加载失败: ${response.url()} - Status: ${response.status()}); } });page.waitForResponse()精准等待特定响应这是自动化脚本中同步操作的关键。比如你点击了一个“提交订单”按钮必须等到订单创建接口返回成功后才能进行下一步断言。// 方法1使用URL模式最常用 const responsePromise page.waitForResponse(**/api/orders); await page.locator(button#submit-order).click(); const response await responsePromise; // 这里会阻塞直到匹配的响应到达 expect(response.status()).toBe(201); const orderData await response.json(); console.log(订单创建成功ID: ${orderData.id}); // 方法2使用正则表达式 const responsePromise page.waitForResponse(/\.json$/); // 方法3使用自定义判断函数最灵活 const responsePromise page.waitForResponse(response response.url().includes(/search) response.request().method() POST );注意page.waitForResponse()必须在触发请求的动作之前定义好Promise但在动作之后再await。这个顺序很重要否则可能等不到。2.2 拦截与修改成为网络的“交通警察”监听是看拦截是管。你可以中断、修改或替换任何请求和响应。这是实现Mock、性能测试如模拟慢速网络和安全测试的核心。核心APIpage.route(url, handler)或context.route(url, handler)两者的区别在于作用域page.route()只对当前页面生效context.route()对该浏览器上下文Context下的所有页面、弹出窗口、iframe都生效。根据你的Mock范围来选择。基础操作中止请求最直接的拦截就是不让某些请求发生常用于加速测试屏蔽非关键资源或测试资源缺失时的页面表现。// 拦截并阻止所有图片加载加速页面渲染测试 await page.route(**/*.{png,jpg,jpeg,webp,gif,svg}, route route.abort()); // 或者根据资源类型判断 await page.route(**/*, route { const req route.request(); if (req.resourceType() stylesheet || req.resourceType() font) { route.abort(); } else { route.continue(); } }); await page.goto(https://news-site.com); // 页面将以无图无样式的方式加载修改请求扮演“中间人”你可以在请求发出前修改其内容比如添加认证头、修改POST数据。// 为所有请求添加一个自定义Token头 await page.route(**/api/**, async route { const headers { ...route.request().headers(), X-Auth-Token: my-secret-token-123, }; await route.continue({ headers }); }); // 修改特定请求的URL或方法慎用可能破坏逻辑 await page.route(**/old-endpoint, route route.continue({ url: https://api.new.com/v2/endpoint }));修改响应伪造数据这是API Mock的精髓。你可以不请求真实后端直接返回预设的数据。// 场景Mock登录接口直接返回成功 await page.route(**/api/login, route route.fulfill({ status: 200, contentType: application/json, body: JSON.stringify({ success: true, user: { id: 1, name: 测试用户 } }), })); // 场景Mock一个列表接口返回固定的测试数据 const mockProductList [ { id: 1, name: 测试商品A, price: 99 }, { id: 2, name: 测试商品B, price: 199 }, ]; await page.route(**/api/products, route route.fulfill({ status: 200, json: mockProductList, // Playwright 1.20 支持直接传json对象 })); // 然后进行测试 await page.goto(https://your-app.com/login); await page.fill(#username, test); await page.fill(#password, test); await page.click(#login-btn); // 页面会立即显示用户“测试用户”因为登录请求被我们Mock了高级技巧基于真实响应进行修改有时你需要修改真实响应的部分内容而不是完全替换。这时可以先获取原响应再加工。await page.route(**/api/user/profile, async route { // 1. 获取原始响应 const response await route.fetch(); // 这里会真的发请求到服务器 // 2. 获取原始响应体 let body await response.text(); const profile JSON.parse(body); // 3. 修改数据例如隐藏敏感信息或注入测试标记 profile.email testexample.com; profile.tags [...(profile.tags || []), automated-test]; // 4. 返回修改后的响应 await route.fulfill({ response, // 保留原响应的状态码、头信息等除body和content-type body: JSON.stringify(profile), headers: { ...response.headers(), content-type: application/json } }); });2.3 身份验证与代理应对复杂网络环境真实项目往往不是运行在干净的本地环境。你可能需要访问需要HTTP基础认证的内网环境或者公司网络要求所有流量走代理。HTTP基础认证对于那种弹出一个浏览器原生用户名密码框的网站Playwright可以预先配置好凭证。// 在创建浏览器上下文时配置 const context await browser.newContext({ httpCredentials: { username: admin, password: secret, // origin: https://intranet.company.com // 可选指定生效的来源 } }); const page await context.newPage(); await page.goto(https://intranet.company.com/secure-page); // 会自动带上认证信息注意这种方式只对服务器返回401 Unauthorized并要求Basic或Digest认证的页面有效。对于页面内嵌的表单登录仍需用page.fill()和page.click()来模拟。HTTP/HTTPS/SOCKS5代理配置这是处理公司网络策略或进行区域测试的必备技能。Playwright支持在浏览器启动或上下文创建时配置代理。// 方式一全局代理启动浏览器时设置所有上下文都走这个代理 const browser await chromium.launch({ proxy: { server: http://my-proxy-server:8080, // 代理服务器地址 username: proxy-user, // 如果代理需要认证 password: proxy-pass, bypass: localhost, 127.0.0.1, *.internal.company.com // 绕过代理的地址逗号分隔 } }); // 方式二按上下文设置代理更灵活可为不同测试用例设置不同代理 const context1 await browser.newContext({ proxy: { server: http://proxy-for-test-env:3128 } }); const page1 await context1.newPage(); // page1的流量走代理 const context2 await browser.newContext({ // 不设置proxy或设置为null则使用系统直连或浏览器的默认设置 }); const page2 await context2.newPage(); // page2的流量不走代理踩坑记录代理服务器慢或不可用我曾遇到一个坑设置了代理后脚本启动浏览器特别慢甚至超时。原因是代理服务器地址错误或网络不通。Playwright在启动时会尝试通过代理连接。务必确保代理服务器是可达的。如果只是想测试无代理环境就不要设置proxy参数。2.4 WebSocket与Service Worker处理“非典型”网络流量现代应用大量使用WebSocket进行实时通信而Service Worker可以拦截和控制页面请求。Playwright对它们也有很好的支持。监听WebSocketWebSocket的监听是事件驱动的。// 监听WebSocket连接建立 page.on(websocket, ws { console.log(WebSocket已连接: ${ws.url()}); // 监听发送的消息 ws.on(framesent, event { console.log(客户端发送: ${event.payload}); }); // 监听接收的消息 ws.on(framereceived, event { console.log(服务端推送: ${event.payload}); // 这里可以基于消息内容做出断言比如收到“订单支付成功”消息后检查页面状态 if (event.payload.includes(PAYMENT_SUCCESS)) { console.log(支付成功消息已收到); } }); ws.on(close, () console.log(WebSocket已关闭)); }); await page.goto(https://live-chat-app.com); // 页面加载后建立的WebSocket连接都会被监听到处理Service Worker的干扰这是一个非常隐蔽的坑。如果你用了像Mock Service Worker (MSW)这样的库来做前端Mock或者网站本身注册了Service Worker你可能会发现page.route()拦截不到某些请求。因为请求被Service Worker接管了。解决方案禁用Service Worker推荐用于纯测试环境在创建浏览器上下文时直接禁止。const context await browser.newContext({ serviceWorkers: block // 阻止任何Service Worker注册和运行 });如果必须使用Service Worker那么page.route()可能对由Service Worker发出的请求无效。你需要直接去Mock Service Worker的配置或者考虑使用Playwright更底层的browserContext.route()并确保在Service Worker之前进行路由注册。3. 实战场景拆解从Mock到压测的完整应用理论说再多不如看实战。下面我通过几个完整的场景把上面的API串起来用。3.1 场景一构建稳定的前端API Mock测试环境需求前端开发完毕后端接口还没好或者后端环境不稳定。你需要一个完全可控的测试环境来验证前端功能。解决方案使用context.route()全局Mock关键API。const { test, expect } require(playwright/test); // 在测试文件或全局Setup中为所有测试设置Mock test.beforeEach(async ({ context }) { // Mock 用户信息接口 await context.route(**/api/user/profile, route route.fulfill({ status: 200, json: { name: Mock用户, avatar: https://example.com/avatar.png, role: admin } })); // Mock 商品列表接口并模拟网络延迟 await context.route(**/api/products, async route { await new Promise(resolve setTimeout(resolve, 500)); // 模拟500ms延迟 route.fulfill({ status: 200, json: [ { id: 1, title: Mock商品1, price: 100, inStock: true }, { id: 2, title: Mock商品2, price: 200, inStock: false } ] }); }); // Mock 一个总是失败的接口测试前端错误处理 await context.route(**/api/unstable, route route.fulfill({ status: 500, json: { error: Internal Server Error } })); }); test(前端页面使用Mock数据正常渲染, async ({ page }) { await page.goto(https://your-app.com); // 页面应该立即显示“Mock用户” await expect(page.locator(.user-name)).toHaveText(Mock用户); // 商品列表应该显示两个商品 await expect(page.locator(.product-item)).toHaveCount(2); // 点击一个会调用失败接口的按钮 await page.click(#btn-fetch-unstable); await expect(page.locator(.error-message)).toBeVisible(); });心得全局Mock放在beforeEach里很好用但要注意清理。如果某个测试用例需要真实的网络请求你需要在那个用例里用page.unroute()取消路由或者使用page.route()覆盖更全局的路由。3.2 场景二性能分析与慢速网络模拟需求测试网站在慢速3G网络或高延迟下的用户体验找出加载瓶颈。解决方案结合网络监听、请求拦截和Playwright自带的网络模拟功能。const { chromium } require(playwright); (async () { const browser await chromium.launch(); // 关键模拟慢速网络 const context await browser.newContext({ // Playwright内置的网络配置文件 // Slow 3G 或 Fast 3G 或自定义 ...chromium.networkConditions[Slow 3G] // 也可以完全自定义 // offline: false, // downloadThroughput: 500 * 1024 / 8, // 500 Kbps // uploadThroughput: 500 * 1024 / 8, // latency: 400 // 400ms }); const page await context.newPage(); // 记录关键时间点 const metrics { domContentLoaded: null, load: null, largestContentfulPaint: null, apiRequests: [] }; page.on(domcontentloaded, () metrics.domContentLoaded Date.now()); page.on(load, () metrics.load Date.now()); // 监听所有请求记录耗时 page.on(request, request { if (request.url().includes(/api/)) { metrics.apiRequests.push({ url: request.url(), startTime: Date.now(), endTime: null }); } }); page.on(response, response { const req response.request(); if (req.url().includes(/api/)) { const apiReq metrics.apiRequests.find(r r.url req.url() !r.endTime); if (apiReq) { apiReq.endTime Date.now(); apiReq.duration apiReq.endTime - apiReq.startTime; console.log(API ${req.url()} 耗时: ${apiReq.duration}ms); } } }); // 开始导航 const start Date.now(); await page.goto(https://your-app.com, { waitUntil: networkidle }); const totalLoadTime Date.now() - start; console.log(总加载时间: ${totalLoadTime}ms); console.log(DOMContentLoaded 时间: ${metrics.domContentLoaded - start}ms); console.log(Load 事件时间: ${metrics.load - start}ms); // 分析最慢的API const slowestApi metrics.apiRequests.sort((a, b) b.duration - a.duration)[0]; console.log(最慢的API是 ${slowestApi.url}, 耗时 ${slowestApi.duration}ms); await browser.close(); })();技巧除了模拟整体网络条件你还可以用route.continue()配合setTimeout来精确模拟某个特定接口的延迟这对于测试加载状态、骨架屏等UI逻辑非常有用。3.3 场景三安全与合规性检查需求检查前端页面是否向不该发送的地方泄露了敏感信息如Token、用户ID或者是否加载了不安全的第三方资源。解决方案监听所有请求并对请求头和URL进行分析。test(检查页面无敏感信息泄露, async ({ page }) { const leakedUrls []; const sensitivePatterns [/password/i, /token/i, /ssn/i, /credit.?card/i]; page.on(request, request { const url request.url(); const headers request.headers(); // 检查URL中是否包含敏感词 if (sensitivePatterns.some(pattern pattern.test(url))) { leakedUrls.push({ type: URL, value: url, via: URL包含敏感词 }); } // 检查请求头是否包含敏感信息例如Authorization头被发送到非自家域名 if (headers[authorization]) { const requestHost new URL(url).hostname; const appHost new URL(https://your-app.com).hostname; if (!requestHost.endsWith(appHost)) { leakedUrls.push({ type: Header, value: Authorization头被发送到外部域名: ${requestHost}, via: url }); } } // 检查请求体对于POST请求需要更复杂的处理这里仅示意 // 注意获取postData可能影响性能且对于非文本格式处理复杂 // const postData request.postData(); // if (postData sensitivePatterns.some(pattern pattern.test(postData))) { // leakedUrls.push({ type: Body, value: 请求体包含敏感词, via: url }); // } }); // 执行用户操作触发各种请求 await page.goto(https://your-app.com/dashboard); await page.click(#load-report); await page.waitForTimeout(2000); // 等待异步请求 // 断言 expect(leakedUrls).toEqual([]); if (leakedUrls.length 0) { console.error(发现潜在敏感信息泄露:); console.table(leakedUrls); } });进阶你还可以结合route.continue()将含有敏感信息的请求头在发送前替换成测试用的假数据既保证了测试运行又避免了真实数据泄露的风险。4. 高级模式与性能优化当你的测试套件变得庞大网络操作变得复杂时就需要考虑一些高级模式和优化策略了。4.1 使用HAR文件录制与回放HAR (HTTP Archive) 文件可以记录浏览器会话中的所有网络请求和响应。Playwright支持导出和导入HAR文件这对于以下场景非常有用调试将用户反馈的问题页面访问过程录制成HAR你可以在本地精确复现当时的网络环境。测试数据准备先手动操作一遍录下所有API响应然后在自动化测试中直接回放实现“离线测试”。// 录制模式 const context await browser.newContext({ recordHar: { path: session.har } // 开启录制 }); const page await context.newPage(); await page.goto(https://your-app.com); // ... 执行一系列操作 ... await context.close(); // HAR文件会自动保存 // 回放模式模拟录制时的网络环境 const context2 await browser.newContext({ // 从HAR文件提供网络响应。如果请求匹配则使用HAR中的响应否则正常访问网络。 har: session.har }); const page2 await context2.newPage(); await page2.goto(https://your-app.com); // 页面加载所需的资源将从HAR文件中读取不会发出真实请求注意HAR回放是“尽力而为”的。如果页面请求的URL、方法或头部与HAR记录不完全匹配Playwright会回退到发起真实网络请求。4.2 路由匹配模式详解Glob vs. RegExp在route()和waitForResponse()中URL匹配是关键。Playwright主要支持两种模式简化Glob和正则表达式。简化Glob模式推荐用于简单匹配*匹配除/外的任意数量字符。https://example.com/*.js匹配https://example.com/script.js但不匹配https://example.com/path/script.js。**匹配包括/在内的任意数量字符。**/*.js匹配任何路径下的.js文件。?匹配一个单独的?字符。主要用于匹配查询字符串中的?本身。{}匹配一组选项。**/*.{png,jpg}匹配所有.png和.jpg图片。\转义字符。要匹配字面量的*或?需要用\*或\?。正则表达式模式用于复杂匹配当Glob不够用时直接使用RegExp对象功能最强大。// 匹配所有包含版本号v1或v2的API await page.route(/\/api\/v[12]\/.*/, handler); // 匹配特定域名下的特定路径 await page.route(/^https:\/\/api\.example\.com\/users\/\d\/profile$/, handler); // 匹配带有特定查询参数的URL await page.route(/\/search\?.*qplaywright.*/, handler);选择建议对于大多数静态资源拦截如图片、样式表或明确的API路径Mock使用Glob模式更直观。对于需要动态匹配如包含ID的路径或复杂逻辑判断使用正则表达式。4.3 网络拦截的性能影响与最佳实践虽然网络拦截功能强大但滥用会影响测试执行速度。避免全局过度拦截await page.route(**/*, handler)会拦截每一个请求包括图片、字体、小图标。除非必要否则尽量缩小拦截范围使用更精确的URL模式。谨慎处理响应体在response事件监听器里调用response.text()、response.json()或response.body()会强制将响应体缓冲到内存中。对于大文件如视频这会显著增加内存消耗和测试时间。只在需要断言时才去读取响应体。及时清理路由如果一个路由只在某个特定测试用例中需要在该用例结束后使用page.unroute()或context.unroute()将其移除避免影响后续测试。优先使用context.route()如果多个页面需要相同的Mock规则在context级别设置路由比在每个page上设置更高效。Mock数据放在内存中对于返回固定数据的Mock将数据以变量形式存储在内存中而不是每次都在route.fulfill()的handler里重新生成JSON字符串。5. 常见问题排查与调试技巧即使掌握了所有API实际使用中还是会遇到各种奇怪的问题。下面是我踩过的一些坑和解决方法。5.1 问题page.route()不生效请求没有被拦截可能原因及解决方案可能原因排查步骤与解决方案URL模式不匹配这是最常见的原因。首先在page.on(request)监听器里打印出所有请求的URL确认你试图拦截的URL是否真的被发起了以及它的完整格式是什么。Glob模式需要匹配整个URL。路由注册时机过晚路由必须在目标请求发起之前就注册好。对于页面初始加载时的请求必须在page.goto()之前调用page.route()。对于SPA单页应用后续的异步请求也需要在触发请求的动作如点击按钮之前确保路由已设置。Service Worker 干扰如果网站使用了Service Worker特别是像MSW这样的Mock库请求可能被它先拦截了。尝试在创建Context时设置serviceWorkers: block看问题是否消失。如果必须保留Service Worker可能需要调整你的Mock策略。请求来自 iframe 或弹出窗口page.route()只拦截当前页面主框架的请求。如果请求来自页面内的iframe你需要先获取这个iframe对象然后对iframe.route()进行路由。对于弹出窗口popup你需要在新页面对象上设置路由。使用了route.continue()但未await在异步的route handler中如果你调用了route.continue()、route.fulfill()或route.abort()必须确保await它们否则handler可能提前结束而请求继续以默认方式处理。5.2 问题page.waitForResponse()超时或等不到响应可能原因及解决方案可能原因排查步骤与解决方案Promise定义在动作之后确保const responsePromise page.waitForResponse(pattern)这行代码在触发请求的click()或fill()等操作之前执行。匹配模式太宽泛或太严格和route()一样检查你的URL模式或正则表达式是否能正确匹配到实际发出的请求URL。使用page.on(request)来验证。请求根本没有发生可能因为前端逻辑条件不满足如表单验证失败预期的点击操作并没有真正触发网络请求。在点击前后添加一些等待或状态断言。响应状态码不是2xx/3xxwaitForResponse只等待成功的响应吗不它等待任何匹配的响应包括4xx和5xx错误。如果你的接口返回了错误它也会等到。可以结合响应状态码进行判断const resp await responsePromise; if(resp.ok()) { ... }。请求被重定向了如果请求发生了重定向如301/302waitForResponse匹配的是最终响应的URL。你可能需要匹配初始请求的URL或者使用page.waitForRequest()来监听请求事件。5.3 问题修改响应后页面显示异常或JS报错可能原因及解决方案可能原因排查步骤与解决方案响应头不匹配当你使用route.fulfill()返回自定义响应时如果覆盖了content-type、content-length等关键头信息但数据格式不对浏览器可能无法正确解析。尽量使用route.fulfill({ json: {...} })或route.fulfill({ body: ... })这种简化形式Playwright会自动设置合适的头部。如果使用response参数传递原始响应注意修改body后可能需要重新计算content-length或者直接不传这个头浏览器会自动计算。响应数据格式错误Mock的JSON数据如果格式不符合前端预期缺少某个字段、字段类型不对会导致前端JS报错。仔细对照真实接口的返回数据结构。异步操作顺序问题在route handler中如果你先执行了一些异步操作如从文件读取数据然后再调用route.fulfill()要确保整个过程是同步的用async/await。否则请求可能会因为handler执行完毕而未调用fulfill/continue/abort中的任何一个而挂起。CORS跨域问题如果你Mock的响应来自不同的“源”Origin浏览器可能会因CORS策略而阻止。在Mock响应时可以添加CORS头headers: { access-control-allow-origin: * }。5.4 通用调试技巧开启Playwright的详细日志在运行测试时设置环境变量DEBUGpw:api可以打印出Playwright内部详细的API调用日志包括网络请求和路由匹配情况。DEBUGpw:api npx playwright test结合浏览器开发者工具在测试运行时你可以通过await page.pause()让脚本暂停然后打开浏览器自带的开发者工具F12在Network面板查看实时的请求和响应这比任何日志都直观。编写小而专的测试用例当网络Mock复杂时不要试图在一个测试用例里Mock所有东西。为每个关键的API交互编写独立的测试用例这样当某个Mock失败时更容易定位问题。网络操作是Playwright从“能用”到“好用”的关键分水岭。它把自动化测试从简单的界面操作提升到了可以深度控制应用行为、模拟各种边界场景的层次。刚开始接触可能会觉得概念有点多但只要你按照监听 - 拦截 - 修改 - Mock这个路径结合具体的业务场景去实践很快就能得心应手。记住最好的学习方式就是把你当前项目中遇到的网络相关问题用Playwright的这套工具尝试解决一遍踩过的坑都会变成宝贵的经验。