1. Playwright请求拦截的核心价值想象一下你正在测试一个在线文档预览系统。用户点击查看按钮时系统默认会在新窗口展示PDF内容。但产品经理突然提出需求如果用户设备是移动端需要自动触发下载而非预览。这种非标准交互场景正是Playwright的route功能大显身手的时刻。我在去年参与金融系统测试时就遇到过类似需求。银行要求交易记录PDF在iOS设备上必须强制下载而安卓设备保持预览。传统测试方案需要反复修改后端代码而Playwright让我们能直接在测试脚本中动态操控网络交互。这种中间人能力本质上是在浏览器和服务器之间架设了一个可编程的代理层。与Selenium等传统工具相比Playwright的拦截机制有三大优势精准定位支持URL通配符和正则表达式匹配比如**/*.pdf能捕获所有PDF请求双向控制既能修改发出的请求如添加认证头也能篡改服务器响应如替换JS函数性能无损不需要启动额外代理服务所有操作在浏览器上下文内完成2. 拦截机制深度解析2.1 路由注册原理page.route()的工作原理类似高速公路的收费站。当浏览器发起请求时Playwright会先检查是否注册了匹配的路由规则。这个匹配过程支持多种模式// 精确匹配 page.route(https://example.com/api/v1, handler) // 通配符匹配 page.route(**/*.jpg, handler) // 正则表达式匹配 page.route(/.*\.(png|jpe?g)/, handler)我在测试电商网站时发现个有趣现象即使页面已经加载完成后续动态触发的请求依然会被拦截。这意味着我们可以捕获那些由懒加载或用户交互产生的请求这对SPA应用测试特别有用。2.2 请求生命周期控制每个被拦截的请求都面临三种命运选择继续传递route.continue_()保持原样发送到服务器直接响应route.fulfill()用本地数据立即返回中止请求route.abort()模拟网络失败实测中发现一个性能优化技巧对于静态资源拦截使用abort比返回空响应体节省约30%的测试时间。比如禁用图片加载可以这样实现async def handle_route(route): if route.request.resource_type image: await route.abort() else: await route.continue_() await page.route(**/*, handle_route)3. 实战修改PDF预览行为3.1 篡改JS响应体最近在测试某文档管理系统时遇到个典型场景需要将预览行为改为强制下载。通过Chrome开发者工具分析发现关键逻辑在名为popupPdf()的JS函数中。解决方案是拦截JS文件响应用字符串替换修改函数体modified_js function popupPdf() { window.location.href this.href; // 改为直接跳转下载 return; } async def modify_script(route): response await route.fetch() body await response.text() # 使用正则替换原函数 new_body re.sub(rfunction popupPdf\(.*?\{.*?return;.*?\}, modified_js, body, flagsre.DOTALL) await route.fulfill( responseresponse, bodynew_body, headersresponse.headers ) await page.route(**/preview.js, modify_script)这里有个坑要注意某些JS文件可能经过压缩函数名会被缩短。这种情况下需要先格式化代码或者直接定位压缩后的函数标识符。3.2 修改响应头PDF文件本身还需要调整Content-Disposition头。通过拦截PDF请求我们可以动态修改响应头async def modify_pdf_header(route): response await route.fetch() headers dict(response.headers) headers[content-disposition] attachment; filenamedocument.pdf await route.fulfill( responseresponse, bodyawait response.body(), headersheaders ) await page.route(**/*.pdf, modify_pdf_header)在金融项目实践中我们发现某些旧版IE浏览器对附件名的编码有特殊要求。这时可以增加兼容性处理filename 中文文档.pdf encoded_filename urllib.parse.quote(filename) headers[content-disposition] fattachment; filename*UTF-8{encoded_filename}4. 高级应用场景4.1 模拟慢速网络通过拦截请求我们可以人为制造网络延迟测试页面在弱网环境下的表现async def slow_down(route): await asyncio.sleep(2) # 延迟2秒 await route.continue_() # 只对API请求限速 await page.route(**/api/**, slow_down)在移动端测试时这个技巧特别有用。我通常会用不同延迟级别测试用户界面的加载状态显示是否合理。4.2 接口Mock数据当后端服务不可用时可以直接用本地数据响应async def mock_api(route): await route.fulfill( status200, content_typeapplication/json, bodyjson.dumps({data: mocked}) ) await page.route(**/getUserInfo, mock_api)最近测试CRM系统时我用这个方式模拟了5种不同的用户权限场景比等后端准备测试数据效率高得多。4.3 请求参数验证拦截器还能用来检查发出的请求是否符合预期async def check_payload(route): request route.request if request.method POST: payload await request.post() assert payload.get(amount) 100.00 await route.continue_() await page.route(**/transfer, check_payload)这个技巧在测试金融交易时帮我们发现了多个前端校验漏洞包括金额格式化问题和重复提交缺陷。5. 调试技巧与常见问题5.1 请求日志记录在复杂场景下建议记录完整的请求/响应信息async def log_request(route): print(fRequest: {route.request.method} {route.request.url}) print(fHeaders: {route.request.headers}) try: print(fBody: {await route.request.post_data()}) except: pass response await route.fetch() print(fResponse Status: {response.status}) await route.fulfill(responseresponse)最近在排查一个文件上传问题时就是靠这种详细日志发现前端没有正确设置Content-Type头。5.2 性能优化建议选择性拦截避免使用**/*全局拦截精确指定URL模式复用响应对于相同响应可以缓存fetch()结果并行处理多个不相关的拦截器可以并行执行在压力测试中发现不当的全局拦截会使测试速度下降40%以上。最佳实践是为每个测试用例只启用必要的拦截器。5.3 常见陷阱循环拦截拦截器内部又触发新请求导致死循环内存泄漏未正确释放拦截器资源时机问题拦截器注册时机晚于页面加载有个记忆深刻的案例测试支付流程时因为忘记移除拦截器导致后续测试用例全部使用了Mock数据。现在我的习惯是使用try/finally确保清理try: await page.route(**/api, handler) # 执行测试 finally: await page.unroute(**/api, handler)