ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SurfSense Playwright E2E 配置实战:超时决策、项目编排、webServer 与反模式深度解析

SurfSense Playwright E2E 配置实战:超时决策、项目编排、webServer 与反模式深度解析 SurfSense Playwright E2E 配置实战超时决策、项目编排、webServer 与反模式深度解析【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense本文围绕 SurfSense 仓库内置的 Playwright 配置参考文档configuration.md展开系统讲解 Playwright 测试配置的核心决策方法超时参数选择、单/多 project 编排、webServer服务管理与globalSetup/Setup Project/Fixture 的分工。文中所有配置模式均来自该参考文档并结合 SurfSense 前端工程 surfsense_web/playwright.config.ts 的真实落地实现进行印证与深化读完后你可以直接产出一份生产级、可复制到 CI 的playwright.config.ts并能诊断本地/CI 环境的行为差异问题。一、配置参考文档的适用场景与 CLI 速查参考文档开头明确了适用时机新建项目、调整超时、添加浏览器目标、配置 CI 行为、管理多环境设置。当你的困惑属于改哪个参数、在哪个层级改时配置参考是第一入口而当困惑属于选择器怎么写、断言怎么等时则应转向同技能下的其他参考文档如 test-tags.md、fixtures-hooks.md。文档给出的 CLI 速查覆盖了日常配置工作的全部常用入口npx playwright init # scaffold config first test npx playwright test --configcustom.config.ts # use alternate config npx playwright test --projectchromium # run single project npx playwright test --reporterhtml # override reporter npx playwright test --grep smoke # run tests tagged smoke npx playwright test --grep-invert slow # exclude slow tests npx playwright show-report # open last HTML report DEBUGpw:api npx playwright test # verbose logging几个关键点值得注意--config允许为不同环境提供互斥的多份配置文件如custom.config.ts与后文环境特定配置模式配合使用--project与多 project 编排直接对应是PR 只跑 chromium、main 跑全浏览器策略的执行手段--grep/--grep-invert是标签化测试分流的底层机制CI 中PR 跑 smoke、夜间跑全量都依赖它DEBUGpw:api在排查page.goto()挂起、action 超时等问题时非常有用可看到每一次 API 调用的进出。在 SurfSense 仓库中这套 CLI 被封装成了surfsense_web的 npm scripts见 package.jsontest:e2edev server 快速迭代、test:e2e:prodCI1精确对齐 CI 行为、test:e2e:headed、test:e2e:ui、test:e2e:debug、test:e2e:report。其中test:e2e:prod通过cross-env CI1注入 CI 环境变量这正是下文中大量process.env.CI ? ... : ...分支的触发方式。二、决策指南四个高频配置决策的完整决策表文档的决策指南Decision Guide把配置决策拆成四个独立问题每个问题都给出了症状/场景 → 设置项 → 默认值 → 推荐值的对照表这是全文最有检索价值的部分。2.1 超时选择Timeout SelectionPlaywright 的超时不是单一参数而是四个作用域不同的层级。混淆它们是测试偶发超时最常见的根因症状设置项默认值推荐值测试整体耗时过长timeout30s30-60s最大 120s断言重试过长/过短expect.timeout5s5-10spage.goto()或waitForURL()超时navigationTimeout30s10-30sclick()、fill()超时actionTimeout0无限10-15sdev server 启动慢webServer.timeout60s60-180s层级关系可以这样理解timeout是单个 test 的总预算expect.timeout只控制自动重试断言的等待窗口navigationTimeout与actionTimeout分别限制导航类与交互类 APIwebServer.timeout则与测试无关是等待服务就绪的独立时钟。对照 SurfSense 的实际配置playwright.config.ts可以看到一种有意识的放宽断言、收紧总预算策略timeout: 30_000保持 30s 默认expect: { timeout: 15_000 }比推荐上限 10s 更宽容忍前端渲染/网络波动fullyParallel: true。这说明推荐值表应结合应用特性调整——对一个包含 Next.js 编译、Zero 数据同步的富前端宽断言超时是合理的取舍。2.2 服务管理Server Management场景方案应用与测试在同一仓库webServerreuseExistingServer: !process.env.CI应用与测试分离仓库手动启动或 Docker Compose测试已部署环境不配置webServer通过环境变量注入baseURL多个服务webServer使用数组配置多个条目reuseExistingServer: !process.env.CI是这套决策表的精髓本地开发时若端口上已有 dev server 就直接复用避免重复冷启动CI 上则强制 Playwright 自行拉起并负责回收进程。SurfSense 的 playwright.config.ts#L64-L80 完整实现了这一模式并且额外做了两处工程化增强webServer: process.env.PLAYWRIGHT_NO_WEB_SERVER ? undefined : { // Local stays on webpack dev (Turbopack caused stale-lock panics in E2E). command: process.env.CI ? pnpm build pnpm start : pnpm exec next dev, url: http://localhost:${PORT}, reuseExistingServer: !process.env.CI, timeout: process.env.CI ? 300_000 : 180_000, stdout: pipe, stderr: pipe, env: { /* 透传后端地址、AUTH_TYPE、Zero cache 地址 */ }, },PLAYWRIGHT_NO_WEB_SERVER开关实现了测试已部署环境场景前端 dev server 已存在或另有部署时置空该变量即可无需改代码CI 走pnpm build pnpm start、本地走pnpm exec next dev正是 4.3 节webServer with Build Step模式在真实项目里的落点注释还记录了本地不用 Turbopack 的原因Turbopack 在 E2E 中出现过 stale-lock panictimeout在 CI 上放宽到 300s因为 CI 要先完成完整构建——这印证了决策表dev server 慢 → 60-180s在含构建步骤场景下需要进一步上调。2.3 单 project 还是多 project场景方案早期开发单 project仅 chromium发布前验证多 projectchromium firefox webkit移动端响应式应用桌面 project 之外补充移动端 project认证 非认证测试带 dependencies 的 setup project紧张的 CI 预算PR 上跑 chromiummain 上跑全浏览器SurfSense 采用了单浏览器 setup project的精简形态playwright.config.ts#L50-L63setupproject 用testMatch: /.*\.setup\.ts/匹配tests/auth.setup.tschromiumproject 声明dependencies: [setup]并复用playwright/.auth/user.json的storageState。这与决策表认证 非认证测试 → Setup project with dependencies完全对应且PR 预算紧张 → chromium only的取舍也得到遵守未配置 firefox/webkit 目标。2.4 globalSetup vs Setup Project vs Fixtures需求使用一次性数据库播种globalSetup跨测试共享浏览器认证带dependencies的 Setup project每测试隔离状态test.extend()自定义 fixture全部测试结束后的清理globalTeardown这个表划定了三种机制的边界没有浏览器上下文的非浏览器工作建表、播种归globalSetup必须开浏览器才能得到的状态登录后 cookie/localStorage归 Setup project逐测试的隔离数据归 fixture。文档明确警告不要为浏览器认证写 globalSetup见第六节反模式表因为 globalSetup 阶段拿不到 browser context。SurfSense 的选择是第三条路的变体Setup project 里先通过 API 拿 tokentests/helpers/api/auth.ts 的acquireTestToken再用addCookies注入 cookie省去了真实 UI 登录流程——从源码结构看这既满足了共享认证状态的需求又规避了 UI 登录的脆弱性。三、生产级配置全解逐段注释文档给出了一份完整的生产级playwright.config.ts下面保留其原文并结合 SurfSense 实现补充逐段说明// playwright.config.ts import { defineConfig, devices } from playwright/test; import dotenv from dotenv; import path from path; dotenv.config({ path: path.resolve(__dirname, .env) }); export default defineConfig({ testDir: ./e2e, testMatch: **/*.spec.ts, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 50% : undefined, reporter: process.env.CI ? [[html, { open: never }], [github]] : [[html, { open: on-failure }]], timeout: 30_000, expect: { timeout: 5_000 }, use: { baseURL: process.env.BASE_URL || http://localhost:4000, actionTimeout: 10_000, navigationTimeout: 15_000, trace: on-first-retry, screenshot: only-on-failure, video: retain-on-failure, locale: en-US, timezoneId: America/Los_Angeles, }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] } }, { name: firefox, use: { ...devices[Desktop Firefox] } }, { name: webkit, use: { ...devices[Desktop Safari] } }, { name: mobile-chrome, use: { ...devices[Pixel 7] } }, { name: mobile-safari, use: { ...devices[iPhone 14] } }, ], webServer: { command: npm run start, url: http://localhost:4000, reuseExistingServer: !process.env.CI, timeout: 120_000, stdout: pipe, stderr: pipe, }, });按配置项分组解读执行策略fullyParallel: true允许所有 test 并行retries: process.env.CI ? 2 : 0是文档反模式表明确推荐的本地 0 重试暴露 flaky、CI 2 重试吸收偶发组合forbidOnly: !!process.env.CI防止test.only被误提交后在 CI 只跑一个测试workers: 50%在 CI 上控制并行度以匹配较弱的机器对应第七节本地过 CI 超时的排障方案。ReporterCI 下html { open: never }githubreporter结果写回 PR 注释本地open: on-failure失败时自动弹 HTML 报告。对照 SurfSense 的 reporter 配置它还额外加了listreporter让 CI 日志同时拥有逐条可读输出——这是文档模板没有但实践中常见的补充。use块baseURL从环境变量读取并给本地默认值测试内一律用相对路径跳转trace/screenshot/video三件套统一采用仅失败/仅重试时采集的低成本策略详见 4.7locale与timezoneId固定避免本地时区/语言差异导致的格式化断言抖动。webServerstdout/stderr: pipe让服务输出进入 Playwright 日志而不是散落终端url是就绪探测地址见 7.2 排障项。四、六个核心配置模式Patterns4.1 环境特定配置一套代码跑 local/staging/prod适用同一测试集需要指向 dev、staging、production 多个环境。// playwright.config.ts import { defineConfig } from playwright/test; import dotenv from dotenv; import path from path; const ENV process.env.TEST_ENV || local; dotenv.config({ path: path.resolve(__dirname, .env.${ENV}) }); const envConfig: Recordstring, { baseURL: string; retries: number } { local: { baseURL: http://localhost:4000, retries: 0 }, staging: { baseURL: https://staging.myapp.com, retries: 2 }, prod: { baseURL: https://myapp.com, retries: 2 }, }; export default defineConfig({ testDir: ./e2e, retries: envConfig[ENV].retries, use: { baseURL: envConfig[ENV].baseURL }, });TEST_ENVstaging npx playwright test TEST_ENVprod npx playwright test --grep smoke要点环境差异收敛到baseURL retries两个维度外加可选的超时差异TEST_ENVprod配合--grep smoke意味着对生产环境只允许跑冒烟子集——这是对生产最保守也最安全的使用姿势。SurfSense 用的是更轻量的一层实现playwright.config.ts#L3-L9 通过PLAYWRIGHT_BASE_URL与PLAYWRIGHT_USE_PROXY_ORIGIN两个变量决定直连本地端口还是统一走前端 origin 代理把多环境差异压缩到了环境变量效果与上例等价但更贴合 monorepo 单部署栈的现实。4.2 带依赖的 Setup Project共享浏览器认证态适用多数测试需要登录态不想每条测试都走一遍 UI 登录。// playwright.config.ts import { defineConfig, devices } from playwright/test; export default defineConfig({ testDir: ./e2e, projects: [ { name: setup, testMatch: /auth\.setup\.ts/, }, { name: chromium, use: { ...devices[Desktop Chrome], storageState: playwright/.auth/session.json, }, dependencies: [setup], }, { name: firefox, use: { ...devices[Desktop Firefox], storageState: playwright/.auth/session.json, }, dependencies: [setup], }, ], });// e2e/auth.setup.ts import { test as setup, expect } from playwright/test; const authFile playwright/.auth/session.json; setup(authenticate, async ({ page }) { await page.goto(/login); await page.getByLabel(Username).fill(testuserexample.com); await page.getByLabel(Password).fill(process.env.TEST_PASSWORD!); await page.getByRole(button, { name: Log in }).click(); await expect(page.getByRole(heading, { name: Home })).toBeVisible(); await page.context().storageState({ path: authFile }); });工作机制setup project 先执行把登录后的 cookie localStorage 序列化进session.json后续 project 通过storageState字段在 context 创建时直接加载实现零登录起步。注意.auth/目录应加入.gitignore见 4.5因为其中包含有效凭证。SurfSense 的 tests/auth.setup.ts 是这一模式的完整工业级实例比文档示例多了两处值得学习的细节认证来源不操作登录 UI而是调用acquireTestToken优先请求无速率限制的/__e2e__/auth/token测试端点由后端 E2E 入口 surfsense_backend/tests/e2e/run_backend.py 挂载404 时回落到/auth/desktop/login拿到 JWT 后通过page.context().addCookies注入会话 cookiecookie 名取自SESSION_COOKIE_NAME默认surfsense_sessionUI 干扰预屏蔽用addInitScript在页面加载前预写surfsense_announcements_state与surfsense-tour-userId两个 localStorage 键把公告弹窗和新用户引导提前标记为已读避免遮挡点击路径——这是共享认证之外 Setup project 的另一个合法用途预置任何会让旅程测试随机失败的 UI 状态最后page.context().storageState({ path: authFile })落盘到playwright/.auth/user.json与chromiumproject 的use.storageState精确对应。4.3 webServer 带构建步骤CI 与本地用不同命令适用测试需要一个由 Playwright 托管的应用服务器且 CI 上希望测生产构建产物。// playwright.config.ts import { defineConfig } from playwright/test; export default defineConfig({ testDir: ./e2e, use: { baseURL: http://localhost:4000 }, webServer: { command: process.env.CI ? npm run build npm run preview : npm run dev, url: http://localhost:4000, reuseExistingServer: !process.env.CI, timeout: 120_000, env: { NODE_ENV: test, DB_URL: process.env.DB_URL || postgresql://localhost:5432/testdb, }, }, });要点command支持串联构建与启动env字段给被测服务注入专属环境变量且可以引用父进程变量做条件默认值。SurfSense 的对应实现第二节已展示在此基础上做了等价的事并且通过env透传NEXT_PUBLIC_FASTAPI_BACKEND_URL、AUTH_TYPE等让 Next.js 编译时把后端地址正确打进客户端 bundle——从源码结构看若漏掉这一步前端会请求错误的后端端口E2E 会以连接被拒的假象失败。4.4 globalSetup / globalTeardown一次性非浏览器工作适用数据库播种等只需执行一次的非浏览器工作每次运行各跑一遍。// playwright.config.ts import { defineConfig } from playwright/test; export default defineConfig({ testDir: ./e2e, globalSetup: ./e2e/setup.ts, globalTeardown: ./e2e/teardown.ts, });// e2e/setup.ts import { FullConfig } from playwright/test; export default async function globalSetup(config: FullConfig) { const { execSync } await import(child_process); execSync(npx prisma db seed, { stdio: inherit }); process.env.TEST_RUN_ID run-${Date.now()}; }// e2e/teardown.ts import { FullConfig } from playwright/test; export default async function globalTeardown(config: FullConfig) { const { execSync } await import(child_process); execSync(npx prisma db push --force-reset, { stdio: inherit }); }注意 globalSetup 的局限它运行在独立进程没有 browser fixture跨进程传递状态只能靠process.env如示例中的TEST_RUN_ID或文件系统。凡是必须在浏览器里才能完成的准备工作登录、授权一律改走 4.2 的 Setup project。4.5 环境变量与 .env密钥与 URL 不硬编码适用管理密钥、URL、feature flag 而不写死在配置里。文档给出三文件的分工约定# .env.example (commit this) BASE_URLhttp://localhost:4000 TEST_PASSWORD API_KEY # .env.local (gitignored) BASE_URLhttp://localhost:4000 TEST_PASSWORDsecret123 API_KEYdev-key-abc # .env.staging (gitignored) BASE_URLhttps://staging.myapp.com TEST_PASSWORDstaging-pass API_KEYstaging-key-xyz# .gitignore .env .env.local .env.staging .env.production playwright/.auth/npm install -D dotenv约定是只提交.env.example作为模板所有含真实凭据的.env.*与playwright/.auth/认证态快照一律 gitignore。SurfSense 对这一约定做了反向取舍但同样安全测试账号凭证直接写进了配置文件默认值playwright.config.ts#L11-L16 中PLAYWRIGHT_TEST_EMAIL ?? e2e-testsurfsense.net从源码注释和 tests/README.md 可知这是因为该账号仅存在于每次重建的 E2E 测试库中属于低敏默认值 环境变量可覆盖的开放源码常见做法且所有默认值都通过??支持外部覆盖。4.6 基于标签的测试过滤CI 阶段化执行适用不同 CI 阶段PR vs 夜间跑不同测试子集。// playwright.config.ts import { defineConfig } from playwright/test; export default defineConfig({ testDir: ./e2e, // Filter by tags in CI grep: process.env.CI ? /smoke|critical/ : undefined, grepInvert: process.env.CI ? /flaky/ : undefined, });按 project 过滤是更强的形式把子集选择固化成 project 本身用--project直接切换// playwright.config.ts import { defineConfig, devices } from playwright/test; export default defineConfig({ testDir: ./e2e, projects: [ { name: smoke, grep: /smoke/, use: { ...devices[Desktop Chrome] }, }, { name: regression, grepInvert: /smoke/, use: { ...devices[Desktop Chrome] }, }, { name: critical-only, grep: /critical/, use: { ...devices[Desktop Chrome] }, }, ], });# Run specific project npx playwright test --projectsmoke npx playwright test --projectregression两种写法的区别顶层grep对全部project 生效适合CI 里只允许跑某类标签的硬约束project 级grep/grepInvert把子集命名成可寻址对象适合流水线中按 job 分发。两者可组合使用。标签机制本身的完整用法见 test-tags.md。4.7 产物采集策略Artifact Collectiontrace/screenshot/video 是失败诊断的关键证据但也是 CI 存储与上传时间的最大来源。文档给出的默认策略是平时关、失败时留设置本地CI理由traceoffon-first-retrytrace 体积大只在失败时采集screenshotoffonly-on-failure对 CI 调试有用videooffretain-on-failure录屏拖慢测试// playwright.config.ts import { defineConfig } from playwright/test; export default defineConfig({ testDir: ./e2e, use: { trace: process.env.CI ? on-first-retry : off, screenshot: process.env.CI ? only-on-failure : off, video: process.env.CI ? retain-on-failure : off, }, });on-first-retry的语义是首次运行不录首次重试即疑似 flaky的时刻才录——它同时是 flaky 检测器和取证工具。SurfSense 选择了统一策略而非环境分叉use 配置trace: on-first-retry、screenshot: only-on-failure恒定开启video则 CI 关闭、本地保留失败录像process.env.CI ? off : retain-on-failure并用extraHTTPHeaders: { x-playwright-test: true }给所有请求打标方便后端日志识别测试流量。五、SurfSense 全栈 E2E 配置的真实映射把上面的模式放到 SurfSense 的完整工程里可以看到一份文档模式 → 仓库实现的对照也是配置文档所说的生产级在真实 monorepo 中的样子配置总览见 playwright.config.ts 文件头注释其还说明了测试目录为何不会被打进 Next.js standalone 或 Electron 产物文档模式SurfSense 实现位置CI/本地分叉 reporterCI: html github list本地: html(on-failure) listplaywright.config.ts#L38-L40forbidOnly/retriesCI 分叉forbidOnly: !!process.env.CI、retries: CI ? 1 : 0playwright.config.ts#L35-L36webServer 构建步骤 复用CIpnpm build pnpm start本地next devreuseExistingServer: !CIplaywright.config.ts#L64-L80Setup project 依赖setup→chromiumstorageState: playwright/.auth/user.jsonplaywright.config.ts#L50-L63认证态获取token 铸造/登录 cookie 注入 localStorage 预置tests/auth.setup.ts、tests/helpers/api/auth.ts服务托管之外的依赖服务Postgres/Redis 用docker/docker-compose.deps-only.yml单独拉起对应决策表多服务场景docker/docker-compose.deps-only.yml、tests/README.md值得强调的是SurfSense 的 webServer 只托管 Next.js 前端而 FastAPI 后端与 Celery worker 由独立的 E2E 入口脚本拉起surfsense_backend/tests/e2e/run_backend.py、run_celery.py这正是 2.2 决策表应用与测试分离一行的仓库实例Playwright 只负责它确定要测试和回收的那一层其余依赖服务交给 Compose/脚本。完整的本地运行步骤起 Postgres/Redis → 起后端与 Celery → 注册 E2E 用户 → 跑pnpm test:e2e在 tests/README.md 中有逐步说明其中CI 设置HTTPS_PROXYhttp://127.0.0.1:1 哨兵 API key 阻断一切真实外呼的三层防护设计展示了配置之外的配套工程如何与测试配置协同保证确定性。六、反模式清单哪些配置是看起来能跑的坑文档的 Anti-Patterns 表是配置评审时的检查单完整保留如下不要做问题正确做法全局timeout: 300_000掩盖 flaky 测试拖慢 CI修根因保持 30s 默认硬编码 URLpage.goto(http://localhost:4000/login)其他环境直接失效baseURL 相对路径每次 PR 跑全部浏览器CI 时间 3 倍PR 上 chromiummain 上全浏览器trace: on常开产物巨大、上传慢trace: on-first-retryvideo: on常开存储爆炸测试变慢video: retain-on-failure测试文件里到处test.use({ viewport: {...} })分散、不一致在项目配置里定义一次本地retries: 3隐藏 flakiness本地retries: 0CIretries: 2CI 缺forbidOnly误提交的test.only让 CI 只跑单测forbidOnly: !!process.env.CI用globalSetup做浏览器认证该阶段没有浏览器上下文用带 dependencies 的 setup project提交带凭据的.env安全风险只提交.env.example这张表的内在逻辑是三层防线时间防线超时与重试设置是为了暴露问题而不是掩盖、隔离防线视口等一次性配置、forbidOnly防止执行面被意外收窄、安全防线凭据与认证态不入库。评审任何playwright.config.ts时可按这三层逐项对照。七、排障手册四个典型症状与对症解法7.1 baseURL 不生效成因page.goto()传了绝对 URL 时baseURL会被忽略。// Wrong - ignores baseURL await page.goto(http://localhost:4000/dashboard); // Correct - uses baseURL await page.goto(/dashboard);7.2 webServer 起来了但测试报 Connection Refused成因webServer.url与实际服务地址不符或健康检查端点未返回 200。webServer: { command: npm run dev, url: http://localhost:4000/api/health, // use real endpoint reuseExistingServer: !process.env.CI, timeout: 120_000, },7.3 本地通过、CI 超时成因CI 机器更慢。提高超时的同时降低 worker 数export default defineConfig({ workers: process.env.CI ? 50% : undefined, use: { navigationTimeout: process.env.CI ? 30_000 : 15_000, actionTimeout: process.env.CI ? 15_000 : 10_000, }, });7.4 Target page, context or browser has been closed成因测试超过timeout总预算Playwright 在动作执行中途拆掉了浏览器。解法不要调大全局超时用 trace 定位慢步骤npx playwright test --trace on npx playwright show-report这条建议与第二节超时决策呼应timeout超时的正确响应永远是找到慢的那一步并修它而不是把预算放宽到掩盖竞态。八、相关参考本配置文档是 SurfSense 仓库 playwright-testing 技能 的核心篇目之一其内链指向的相邻文档已转换为仓库根路径test-tags.md —— 测试标签与--grep过滤的完整用法fixtures-hooks.md —— 自定义 fixture 实现每测试隔离状态test-suite-structure.md —— 测试文件结构与命名约定authentication.md —— 基于 setup project 的共享认证进阶模式projects-dependencies.md —— 多 project 编排的高级模式。仓库内的真实配套实现可作为进一步阅读入口surfsense_web/playwright.config.ts生产配置全量、surfsense_web/tests/auth.setup.ts认证 setup 实例、surfsense_web/tests/README.md全栈 E2E 运行手册与防外呼防护设计。【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表