快马平台与Playwright结合:打造高效电商E2E自动化测试方案 1. 项目概述当电商测试遇上“快马”与Playwright最近在团队里搞自动化测试基建一个绕不开的痛点就是电商这类复杂业务系统的端到端E2E测试。页面多、流程长、状态复杂靠人工点点点回归一次就得大半天还容易漏。手动维护测试脚本开发同学写起来费劲测试同学维护起来头疼脚本还动不动就“脆断”。所以当“快马平台”这个号称能低代码生成测试用例的工具和我们正在评估的Playwright这个新兴的自动化框架碰到一起时我决定来一次深度的“实战演练”看看它们结合后能否真的为电商全流程测试带来质变。简单来说这个项目就是利用快马平台的智能录制与用例生成能力快速创建覆盖电商核心流程如登录、浏览、加购、下单、支付的测试场景然后将其转化为可维护、可扩展、高稳定性的Playwright测试脚本最终形成一个完整的、自动化的回归测试套件。这不仅仅是两个工具的简单拼接更涉及到如何将平台生成的“草图”打磨成工业级的“成品”其中关于元素定位策略、等待机制、数据驱动、异常处理等细节才是决定成败的关键。无论你是正在被繁重E2E测试困扰的测试工程师还是希望提升交付质量的前后端开发者这套组合拳都值得你深入了解。2. 核心思路与方案选型为什么是“快马”Playwright在决定技术栈时我们对比了市面上几种常见的方案。传统的“Selenium 手动编码”模式自由度最高但对编写者的编程和测试框架设计能力要求也高且脚本稳定性维护成本不小。而纯粹的“录制/回放”工具虽然上手快但生成的脚本往往像“胶水代码”可读性差、复用性低页面稍一改动就“全军覆没”。快马平台在这里扮演了一个“智能脚手架”的角色。它通过录制用户操作不仅能生成操作步骤更能尝试理解页面结构生成带有语义化定位如根据按钮文字、角色属性的测试步骤。这比单纯录制XPath或CSS选择器要健壮得多。更重要的是它提供了一个可视化的用例编辑和管理界面产品、测试、开发都可以基于同一份“用例描述”进行协作降低了沟通成本。而Playwright则是我们选定的“执行引擎”。相比于Selenium它的优势太明显了原生支持多浏览器Chromium, Firefox, WebKit且无需额外驱动、自动等待机制极大地减少了“flaky tests”不稳定的测试、强大的网络请求拦截与模拟能力、以及媲美原生开发工具的调试体验。对于电商测试中常见的弹窗、iframe、文件上传、地理位置模拟等场景Playwright都提供了优雅的API。所以我们的核心思路是用快马平台降低用例设计与生成的“门槛”和“初稿”成本然后用Playwright的强大与严谨对初稿进行“精装修”将其转化为结构清晰、易于维护、执行稳定的自动化资产。这个分工让工具各司其职人则专注于更重要的测试场景设计与异常逻辑处理。注意快马平台生成的是“步骤描述”和“定位建议”并非直接可用的Playwright脚本。我们需要一个“翻译”或“适配”的过程这个过程正是注入我们测试智慧和工程化思想的关键环节。3. 环境准备与工具链搭建工欲善其事必先利其器。在开始生成和编写用例之前一个可靠的本地开发与调试环境是基础。3.1 Playwright测试环境搭建首先我们需要一个Node.js环境建议版本16。然后在项目目录下初始化并安装Playwright。# 初始化npm项目如果已有package.json可跳过 npm init -y # 安装Playwright测试库 npm install --save-dev playwright/test # 安装Playwright支持的浏览器Chromium, Firefox, WebKit npx playwright install这里有个小技巧如果网络环境导致npx playwright install下载缓慢或失败可以采用手动安装。先去Playwright的GitHub Releases页面下载对应操作系统的浏览器套件压缩包然后解压到~/.cache/ms-playwright目录Linux/macOS或%USERPROFILE%\AppData\Local\ms-playwright目录Windows下对应的浏览器文件夹内。不过直接使用命令安装是最推荐的方式它能处理好所有依赖。接下来初始化Playwright的配置文件playwright.config.ts。这个文件控制着测试如何运行。npx playwright init初始化后我们需要根据电商测试的特点调整配置。一个针对电商全流程测试的配置核心如下// playwright.config.ts import { defineConfig, devices } from playwright/test; export default defineConfig({ // 全局测试超时时间电商流程长建议设置长一些 timeout: 120 * 1000, // 全局断言超时 expect: { timeout: 30 * 1000 }, // 全局每个测试用例超时 globalTimeout: 10 * 60 * 1000, // 重复运行策略用于排查偶发失败 retries: process.env.CI ? 2 : 1, // CI环境重试2次本地1次 // 报告生成器 reporter: [ [html, { outputFolder: playwright-report, open: never }], // HTML报告 [list] // 控制台简洁输出 ], // 项目配置可以定义多套环境如桌面Chrome、移动端模拟等 projects: [ { name: chromium, use: { ...devices[Desktop Chrome] }, }, { name: firefox, use: { ...devices[Desktop Firefox] }, }, // 可以添加移动端测试 // { // name: Mobile Chrome, // use: { ...devices[Pixel 5] }, // }, ], // 全局设置如每个测试开始前执行的钩子 use: { // 基础URL测试中可使用相对路径 baseURL: https://your-ecommerce-staging-site.com, // 自动截屏只在失败时截屏避免报告过大 screenshot: only-on-failure, // 录制视频只在失败时录制对诊断UI交互问题极有帮助 video: retain-on-failure, // 追踪文件记录完整的测试轨迹可用于Timeline查看 trace: retain-on-failure, // 忽略HTTPS错误应对一些内部测试证书问题 ignoreHTTPSErrors: true, }, });3.2 快马平台接入与录制准备快马平台通常是一个SaaS服务或企业内部部署的系统。你需要有一个账号并创建一个针对你的电商测试项目的空间。关键步骤是安装其浏览器扩展程序这个扩展程序用于录制你的操作。安装完成后在浏览器中打开你的电商网站最好是测试环境点击快马扩展图标开始录制。这时你就像正常用户一样去操作登录、搜索商品、查看详情、加入购物车、进入结算页、选择地址、支付通常模拟等等。快马平台会在后台默默地记录你的每一个点击、输入、跳转并分析页面DOM结构生成带有智能定位的测试步骤。录制完成后在快马平台的控制台你可以看到生成的测试用例。它通常包含步骤列表、每个步骤对应的操作click, fill、以及平台推荐的元素定位器可能结合了文本、角色、测试ID等。此时你可以给用例起个名字比如“用户完整下单流程”并将其导出。导出的格式可能是JSON、YAML或者平台自定义的格式。这就是我们的“原材料”。4. 从快马用例到Playwright脚本的转化策略拿到快马平台导出的用例数据后我们面临的核心任务就是“翻译”。这不是简单的格式转换而是需要将平台生成的、可能比较“泛化”的步骤转化为具有工程化水准的Playwright测试代码。这里分享我的转化策略和实操心得。4.1 解析用例结构与元素定位优化快马生成的定位器为了通用性可能会优先使用text或者比较冗长的CSS选择器。我们的首要任务就是将其优化为更稳定、更精准的Playwright定位器。原则优先使用显式的测试属性如>// 优化前依赖文本脆弱 await page.click(text加入购物车); // 优化后使用专用测试ID稳定 await page.click([data-testidadd-to-cart-btn]); // 或者使用Playwright推荐的getByTestId方法需要在config中配置testIdAttribute默认为data-testid await page.getByTestId(add-to-cart-btn).click();如果开发团队没有添加测试属性我们可以退而求其次使用Playwright强大的getByRole组合定位// 定位一个名为“加入购物车”的按钮 await page.getByRole(button, { name: 加入购物车 }).click(); // 或者更精确地定位在商品卡片区域内的按钮 const productCard page.locator(.product-card).first(); await productCard.getByRole(button, { name: 加入购物车 }).click();实操心得在转化初期花时间与前端开发团队沟通建立一套>// models/ProductPage.js class ProductPage { constructor(page) { this.page page; this.addToCartButton page.getByTestId(add-to-cart-btn); this.productTitle page.locator(.product-title); } async addToCart() { await this.addToCartButton.click(); // 可以在这里加入一些等待或断言比如确认商品已加入的Toast提示 await expect(this.page.locator(.toast-success)).toBeVisible(); } async getProductTitle() { return await this.productTitle.textContent(); } }2. 显式等待与断言Playwright虽然有自动等待但在关键状态转换点显式等待和断言能让测试更健壮。快马生成的步骤里通常没有这些。等待导航点击后页面跳转必须等待新页面加载完成。await Promise.all([ page.waitForURL(**/cart), // 等待URL包含/cart page.click([data-testidgo-to-cart]) ]);等待元素状态等待模态框出现、按钮变为可点击、加载动画消失。await page.locator(.checkout-modal).waitFor({ state: visible }); await page.getByRole(button, { name: 提交订单 }).waitFor({ state: enabled });关键断言在每一个流程节点加入断言验证业务状态。// 加入购物车后断言购物车角标数量增加 await expect(page.locator(.cart-count)).toHaveText(1); // 下单成功后断言跳转到成功页面或出现成功提示 await expect(page).toHaveURL(/order-success/); await expect(page.locator(.order-success-msg)).toBeVisible();4.3 实现数据驱动与配置化电商测试经常需要用不同用户、不同商品、不同地址进行测试。硬编码在脚本里是不可行的。我们需要将测试数据外部化。1. 使用环境变量和配置文件将基础URL、用户凭证等敏感或易变信息放在.env文件或config.json中。// config/test-data.json { users: { standard: { username: test_userexample.com, password: Password123! }, vip: {...} }, products: { sku_001: 测试商品A } } // 在测试中引入 const testData require(../config/test-data.json); await loginPage.login(testData.users.standard.username, testData.users.standard.password);2. 使用Playwright的test.describe和test函数实现数据驱动Playwright Test支持直接传入参数数组进行多组数据测试。import { test, expect } from playwright/test; const products [ { sku: SKU001, name: 普通商品 }, { sku: SKU002, name: 秒杀商品 }, { sku: SKU003, name: 缺货商品 }, ]; for (const product of products) { test(购买商品: ${product.name}, async ({ page }) { // ... 测试逻辑使用 product.sku 和 product.name await page.goto(/product/${product.sku}); await expect(page.locator(.product-name)).toContainText(product.name); // ... }); }5. 电商核心流程测试用例详解与脚本实现现在让我们聚焦电商最核心的“浏览-加购-下单”流程看看一个完整的Playwright测试用例应该如何编写。假设我们已经从快马平台获得了这个流程的原始步骤并按照上述策略进行了优化和重构。5.1 用户登录与鉴权处理登录是几乎所有流程的起点。我们不能在每个测试中都录制登录操作那样效率太低。通常有两种处理方式方式一全局前置登录使用Storage State这是Playwright推荐的方式。我们单独写一个setup脚本完成登录并将浏览器上下文的状态包括cookies、localStorage保存下来。其他测试直接加载这个状态就相当于已经登录了。// global-setup.js const { chromium } require(playwright/test); module.exports async config { const browser await chromium.launch(); const page await browser.newPage(); await page.goto(https://your-site.com/login); await page.fill(#username, testuser); await page.fill(#password, testpass); await page.click(button[typesubmit]); // 等待登录成功例如跳转到首页 await page.waitForURL(**/dashboard); // 将当前上下文的状态存储到文件中 await page.context().storageState({ path: storage-state.json }); await browser.close(); };然后在playwright.config.ts中配置globalSetup: require.resolve(./global-setup), use: { ...devices[Desktop Chrome], storageState: storage-state.json, // 所有测试项目共用登录状态 },方式二在每个测试文件中使用Fixture注入已登录的Page如果你需要测试不同角色的用户如普通用户、管理员可以使用Playwright的Fixture功能创建自定义的、已登录的Page对象。// fixtures.js import { test as base, chromium } from playwright/test; export const test base.extend({ loggedInPage: async ({ }, use) { // 启动浏览器并登录 const browser await chromium.launch(); const context await browser.newContext(); const page await context.newPage(); await page.goto(/login); // ... 执行登录操作 await page.fill(input[nameemail], userexample.com); await page.fill(input[namepassword], password); await page.click(button:has-text(登录)); await page.waitForURL(**/dashboard); // 等待登录成功 // 将这个page传递给测试用例使用 await use(page); // 测试结束后清理 await context.close(); await browser.close(); }, }); // 在测试文件中 import { test, expect } from ./fixtures; test(测试下单流程, async ({ loggedInPage }) { // loggedInPage 已经是一个登录状态的页面对象 await loggedInPage.goto(/products); });5.2 商品浏览与加入购物车这个环节的测试要点在于商品列表的渲染、筛选排序、商品详情页的元素完整性、加入购物车操作及反馈。import { test, expect } from playwright/test; import { ProductListingPage, ProductDetailPage } from ../pages; // 引入Page Object test(用户浏览并添加商品到购物车, async ({ page }) { const listingPage new ProductListingPage(page); const detailPage new ProductDetailPage(page); // 1. 访问商品列表页 await listingPage.goto(); await expect(page).toHaveURL(/products/); // 2. 断言列表加载成功 await expect(listingPage.productCards).toHaveCount.greaterThan(0); // 3. 进行筛选例如按价格排序 await listingPage.sortBy(price-low-to-high); // 可以加入断言验证排序是否正确例如获取前两个商品的价格进行比较 const firstPrice await listingPage.getFirstProductPrice(); const secondPrice await listingPage.getSecondProductPrice(); expect(parseFloat(firstPrice)).toBeLessThanOrEqual(parseFloat(secondPrice)); // 4. 进入第一个商品的详情页 await listingPage.goToFirstProductDetail(); await expect(page).toHaveURL(/product\/detail/); // 5. 断言详情页关键信息存在 await expect(detailPage.productName).toBeVisible(); await expect(detailPage.productPrice).toBeVisible(); await expect(detailPage.addToCartButton).toBeVisible(); // 6. 执行加入购物车操作 await detailPage.addToCart(); // 7. 验证加入成功的反馈 // 方式A验证页面上的成功提示 await expect(page.locator(.notification-success)).toContainText(已加入购物车); // 方式B验证购物车图标上的数量变化需要先获取初始数量 // const initialCount await page.locator(.cart-badge).textContent() || 0; // await expect(page.locator(.cart-badge)).toHaveText((parseInt(initialCount) 1).toString()); });5.3 购物车管理与结算流程购物车页面通常涉及数量修改、商品删除、优惠券使用、价格计算等复杂交互。test(用户管理购物车并进入结算, async ({ loggedInPage }) { const cartPage new CartPage(loggedInPage); // 0. 前置条件确保购物车有商品可以通过API或上一个测试添加 await cartPage.goto(); // 1. 验证购物车商品项 await expect(cartPage.cartItems).toHaveCount.greaterThan(0); const firstItemName await cartPage.getItemName(0); expect(firstItemName).toBeTruthy(); // 简单断言名称存在 // 2. 修改商品数量 const initialQuantity await cartPage.getItemQuantity(0); await cartPage.increaseItemQuantity(0); // 点击增加按钮 await expect(cartPage.getItemQuantity(0)).toHaveText((parseInt(initialQuantity) 1).toString()); // 3. 验证价格重新计算 const unitPrice await cartPage.getItemUnitPrice(0); const newSubtotal await cartPage.getItemSubtotal(0); // 计算期望的小计单价 * 新数量 const expectedSubtotal parseFloat(unitPrice) * (parseInt(initialQuantity) 1); expect(parseFloat(newSubtotal)).toBeCloseTo(expectedSubtotal, 2); // 允许微小浮点数误差 // 4. 应用优惠券这是一个很好的网络拦截用例 // 先监听优惠券验证的API请求 const couponResponsePromise loggedInPage.waitForResponse(response response.url().includes(/api/coupon/validate) response.status() 200 ); await cartPage.applyCoupon(TEST2024); const couponResponse await couponResponsePromise; const responseBody await couponResponse.json(); // 断言接口返回了正确的折扣信息 expect(responseBody.discount).toBeGreaterThan(0); // 再断言页面上的折扣金额显示正确 await expect(cartPage.discountAmount).toContainText(responseBody.discount.toString()); // 5. 进入结算页面 await cartPage.proceedToCheckout(); await expect(loggedInPage).toHaveURL(/checkout/); });5.4 订单提交与支付模拟支付环节通常需要模拟因为不可能用真实支付进行自动化测试。Playwright的网络拦截功能在这里大放异彩。test(用户提交订单并模拟支付成功, async ({ page }) { const checkoutPage new CheckoutPage(page); await checkoutPage.goto(); // 1. 填写/选择配送地址 await checkoutPage.selectAddress(默认地址); // 2. 选择配送方式 await checkoutPage.selectShippingMethod(标准快递); // 3. **关键拦截创建订单和支付请求** // 拦截创建订单的POST请求并模拟一个成功的响应 await page.route(**/api/order/create, async route { // 可以在这里对请求体进行断言验证传递的参数是否正确 const request route.request(); const postData request.postData(); // console.log(创建订单请求:, postData); // 模拟一个成功的响应返回一个模拟的订单号 await route.fulfill({ status: 200, contentType: application/json, body: JSON.stringify({ success: true, orderId: MOCK_ORDER_123456, totalAmount: 99.9 }), }); }); // 拦截支付请求模拟支付成功 await page.route(**/api/payment/submit, async route { await route.fulfill({ status: 200, contentType: application/json, body: JSON.stringify({ success: true, paymentId: MOCK_PAY_789, status: paid }), }); }); // 4. 提交订单 await checkoutPage.placeOrder(); // 5. 验证页面跳转到成功页并显示正确的订单号 await expect(page).toHaveURL(/order-success/); await expect(page.locator(.order-id)).toContainText(MOCK_ORDER_123456); await expect(page.locator(.success-icon)).toBeVisible(); }); // 同样我们也需要测试支付失败的场景 test(用户支付失败流程, async ({ page }) { const checkoutPage new CheckoutPage(page); await checkoutPage.goto(); // ... 填写前置信息 // 拦截支付请求模拟失败 await page.route(**/api/payment/submit, async route { await route.fulfill({ status: 200, // 注意接口可能本身是200但body里success是false contentType: application/json, body: JSON.stringify({ success: false, code: INSUFFICIENT_BALANCE, message: 余额不足 }), }); }); await checkoutPage.placeOrder(); // 验证页面显示了正确的错误提示 await expect(page.locator(.error-message)).toContainText(余额不足); // 验证页面没有跳转仍然在结算页或支付页 await expect(page).not.toHaveURL(/order-success/); });6. 高级技巧与稳定性提升实战将基础流程跑通只是第一步要让测试套件能在CI/CD流水线中稳定运行成为团队信任的“守门员”还需要更多工程化技巧。6.1 处理动态内容与异步加载电商页面充斥着动态内容推荐商品、实时库存、倒计时、弹窗广告等。等待特定请求完成对于依赖API加载数据的列表可以等待对应的网络请求完成。// 先导航到页面然后等待商品列表的API响应 await page.goto(/flash-sales); const response await page.waitForResponse(resp resp.url().includes(/api/flash-sale/products) resp.status() 200 ); // 然后再去断言页面元素此时数据已加载 await expect(page.locator(.flash-item)).toHaveCount.greaterThan(0);应对元素动态出现使用locator.waitFor()。// 点击按钮后等待一个动态生成的弹窗出现 await page.click(button[data-actionshow-modal]); const modal page.locator(.dynamic-modal); await modal.waitFor({ state: visible }); await modal.locator(button.confirm).click(); await modal.waitFor({ state: hidden }); // 等待弹窗消失6.2 测试数据清理与隔离测试不应该留下垃圾数据也不应该相互影响。每条测试都应该是独立的。API清理在test.beforeEach或test.afterEach钩子中调用后端API清理测试数据如刚创建的订单、测试用户。import { test } from playwright/test; test.beforeEach(async ({ request }) { // 使用Playwright的APIRequestContext清理上一个测试可能残留的数据 // 假设有一个清理测试订单的接口需要管理员权限 const cleanupResponse await request.delete(/api/admin/test-orders, { headers: { Authorization: Bearer ${adminToken} } }); expect(cleanupResponse.ok()).toBeTruthy(); }); test.afterEach(async ({ request }, testInfo) { if (testInfo.status ! passed) { // 如果测试失败截图并保留更多日志同时也可以尝试清理 console.log(Test ${testInfo.title} failed. Attempting cleanup.); // ... 清理逻辑 } });浏览器上下文隔离Playwright Test默认会为每个测试用例创建一个独立的浏览器上下文Context这天然实现了Cookie、LocalStorage的隔离。确保不要在测试中依赖全局的、共享的浏览器状态。6.3 集成CI/CD与生成测试报告自动化测试的价值在于持续反馈。我们需要将其集成到GitHub Actions、GitLab CI、Jenkins等CI/CD工具中。一个简单的GitHub Actions工作流示例# .github/workflows/playwright.yml name: Playwright E2E Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps chromium # CI上通常只安装一个浏览器以加快速度 - name: Run Playwright tests run: npx playwright test --projectchromium --reporterhtml,github env: BASE_URL: ${{ secrets.STAGING_BASE_URL }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} - name: Upload Playwright report if: always() # 即使测试失败也上传报告 uses: actions/upload-artifactv3 with: name: playwright-report path: playwright-report/ retention-days: 7报告分析Playwright生成的HTML报告非常直观可以看到每个测试的通过情况、耗时、截图、视频和追踪文件Trace。对于失败的测试点击Trace文件可以像使用开发者工具时间旅行一样一步步回放测试执行过程查看每个步骤时的页面状态、网络请求和Console日志这几乎是调试E2E测试最强大的功能。7. 常见问题排查与调试技巧实录即使有了完善的脚本在复杂多变的电商前端环境中测试失败仍是常事。以下是我在实践中总结的常见问题与排查思路。问题现象可能原因排查步骤与解决方案元素找不到 (TimeoutError)1. 页面未加载完成。2. 元素定位器失效前端代码更新。3. 元素在iframe或Shadow DOM内。4. 动态内容尚未出现。1.增加等待在操作前使用page.waitForLoadState(networkidle)或等待特定元素/请求。2.更新定位器使用开发者工具重新检查元素采用更稳定的定位策略如>操作不生效 (点击/输入无效)1. 元素被遮挡弹窗、遮罩层。2. 元素状态不可交互disabled, hidden。3. 页面有未处理的弹窗或脚本错误。1.强制点击尝试locator.click({ force: true })谨慎使用。2.检查元素状态先await expect(locator).toBeEnabled()或toBeVisible()。3.监听页面错误在config中设置use: { bypassCSP: true }并监听page.on(pageerror, ...)。测试在CI上失败本地却通过1. CI环境与本地环境差异数据、网络、配置。2. CI机器性能差超时时间不足。3. 测试存在竞态条件。1.环境一致性确保CI使用的测试环境与本地一致。使用Docker镜像。2.调整超时在CI配置中增加全局timeout和expect.timeout。3.消除竞态用Promise.all()处理并行导航用明确的等待替代sleep。4.启用重试在config中设置retries: 2。网络请求相关错误1. 接口响应慢或超时。2. 拦截mock的请求与实际请求不匹配。3. CORS问题。1.Mock慢请求使用page.route拦截并立即返回模拟数据避免等待。2.检查请求URL在测试中打印拦截到的请求URL确保模式匹配正确。3.忽略CORS在浏览器启动参数中添加--disable-web-security仅测试环境。截图/视频显示页面空白或错位1. 在页面过渡动画期间截图。2. CI服务器无头模式下的视图大小问题。1.截图前等待在截图前等待页面稳定如await page.waitForLoadState(networkidle)。2.设置视口在config或测试中明确设置viewport: { width: 1920, height: 1080 }。调试利器Playwright Trace Viewer当测试失败时第一时间打开生成的trace.zip文件在test-results目录下。通过命令npx playwright show-trace trace.zip可以在浏览器中打开一个强大的调试界面。你可以时间线浏览看到测试每一步的精确操作。查看快照点击时间线上任意点查看当时的页面UI、控制台日志和网络请求。性能分析检查每个操作的耗时。 这能帮你快速定位是“页面没加载出来”、“元素定位错了”还是“接口返回异常”。一个真实的踩坑记录我们曾有一个测试在本地始终通过但在CI上随机失败。通过Trace Viewer发现失败时页面上的一个核心JavaScript文件加载状态是(canceled)。最终排查出是因为CI服务器所在区域到某个CDN节点的网络不稳定。解决方案是在测试开始前通过page.route拦截对该JS文件的请求并直接返回一个我们预先下载好的本地稳定版本彻底消除了网络波动的影响。