Cypress端到端测试实战:从原理到CI/CD集成全解析
1. 项目概述为什么是Cypress如果你是一名前端开发者或者正在向全栈测试工程师转型那么你一定对Web应用的自动化测试感到头疼。传统的Selenium框架虽然强大但配置复杂、运行不稳定、调试困难常常让测试脚本的编写和维护变成一场噩梦。我自己在团队里推动自动化测试时就深受其苦一个简单的元素定位失败就可能需要花费大量时间在环境、驱动和异步等待上。直到我遇到了Cypress。它彻底改变了我的测试工作流。Cypress不是一个简单的测试库它是一个完整的端到端测试框架运行在真实的浏览器中却提供了前所未有的开发体验。它内置了测试运行器、断言库、模拟请求、时间旅行调试等一系列开箱即用的功能。最吸引我的一点是它的测试脚本是用JavaScript或TypeScript写的和你开发应用用的是同一种语言这意味着前后端开发者都能快速上手真正实现了“测试即开发”。这个系列教程我将以一个“软测大玩家”的实战视角带你从零开始深入掌握Cypress。我们不只讲“怎么安装”更要讲清楚“为什么这么装”以及安装后如何高效地用它解决真实的测试难题。本篇作为第三部分我们将聚焦于Cypress的核心使用模式、高级特性以及如何将其集成到你的CI/CD流水线中让你从“会用”进阶到“玩转”。2. 核心架构与运行原理深度解析在开始写测试用例之前理解Cypress的底层工作原理至关重要。这能帮助你在遇到诡异问题时快速定位是脚本逻辑问题还是Cypress运行机制导致的。2.1 Cypress的独特架构为什么它这么快、这么稳与Selenium的“远程控制”模式不同Cypress采用了一种同源架构。当你运行npx cypress open时Cypress会启动一个本地服务器。你的测试代码和被测应用运行在同一个浏览器循环browser loop中而Cypress的Node.js后端进程则运行在另一个独立的进程中。这两个进程通过WebSocket进行实时、双向通信。这种架构带来了几个革命性的优势无网络延迟因为测试代码和应用程序代码在同一个上下文中执行所以命令执行是同步的你不需要写await或.then()来等待命令完成尽管Cypress命令本身是异步的但它帮你处理了。完全控制Cypress可以拦截和修改进出浏览器的每一个网络请求和响应也能直接访问DOM、window、document等对象这使得模拟和存根Stub变得极其简单。时间旅行Cypress Test Runner可以记录每一个命令执行时的快照。当测试失败时你可以像使用调试器一样回溯到之前的任意一个命令查看当时的页面状态、网络请求和Console日志。注意正因为这种同源架构Cypress无法在一个测试套件中测试跨域Cross-Origin的页面。这是它的一个设计限制。不过Cypress提供了cy.origin()命令来专门处理有限的跨域场景但这需要额外的配置和理解。2.2 命令队列与异步执行告别“回调地狱”这是新手最容易困惑的地方。看这段代码cy.get(.search-input).type(Cypress) cy.get(.search-button).click() cy.contains(h3, Cypress Documentation).should(be.visible)从写法上看这些命令是同步的、顺序执行的。但实际上Cypress的命令是异步的。它们不会立即执行而是被放入一个命令队列。Cypress会一个接一个地执行队列中的命令并且每个命令都会自动等待前一个命令成功完成同时也会等待应用程序达到某个“稳定”状态例如没有未完成的网络请求、定时器或动画。这意味着你几乎不需要手动添加cy.wait()。例如cy.get()命令会不断重试查找元素直到元素出现在DOM中或者超时。cy.type()会等待输入框变为可交互状态。这种“自动等待”机制是Cypress稳定性的基石。实操心得虽然Cypress帮你处理了等待但并不意味着你可以完全忽视异步问题。如果你的应用在某个操作后会触发一个长时间运行的setTimeout或者一个不通过XHR/Fetch发出的请求如WebSocketCypress可能无法自动检测到。这时你需要使用cy.intercept()来监听特定的网络请求或者使用cy.wait()配合别名来显式等待。3. 编写你的第一个健壮测试用例理解了原理我们动手写一个完整的测试。假设我们有一个简单的待办事项Todo应用。3.1 测试文件结构与基本语法Cypress的测试文件默认放在cypress/e2e目录下支持.js,.ts,.jsx,.tsx后缀。我们创建一个todo.cy.js文件。一个典型的测试用例结构如下// cypress/e2e/todo.cy.js describe(Todo App, () { // 测试套件描述一组相关测试 beforeEach(() { // 每个测试用例运行前的钩子用于准备测试环境 cy.visit(http://localhost:3000) // 访问被测应用 }) it(should add a new todo item, () { // 单个测试用例 // 测试步骤和断言 }) it(should mark a todo as completed, () { // 另一个测试用例 }) })3.2 一个完整的增删改查测试示例让我们实现一个包含添加、完成、筛选、删除操作的完整流程测试。describe(Todo App E2E Tests, () { beforeEach(() { // 假设我们的开发服务器运行在3000端口 cy.visit(http://localhost:3000) // 可选每次测试前清空本地存储或重置后端数据如果有点赞 // cy.request(POST, /api/test/reset) }) it(成功添加一个新的待办事项, () { const newItem 学习Cypress高级特性 // 1. 定位输入框并输入文本 cy.get([data-testidtodo-input]) // 最佳实践使用>it(拦截添加待办事项的API请求并验证载荷, () { const newItem 通过拦截添加的任务 const mockResponse { id: 999, text: newItem, completed: false } // 1. 拦截 POST 请求到 /api/todos cy.intercept(POST, /api/todos, (req) { // 可以在这里对请求进行断言或修改 expect(req.body).to.deep.equal({ text: newItem }) // 断言请求体 // 请求继续发送到真实服务器 // 如果想存根Stub则使用req.reply(mockResponse) }).as(addTodoRequest) // 给这个拦截起个别名 // 2. 执行UI操作触发请求 cy.get([data-testidtodo-input]).type(newItem {enter}) // 3. 等待特定的拦截完成并获取其响应 cy.wait(addTodoRequest).then((interception) { // interception 对象包含了请求和响应的所有信息 expect(interception.response.statusCode).to.eq(201) // 可以在这里对真实响应进行断言 }) // 4. 断言UI已根据响应更新 cy.get([data-testidtodo-list] li) .should(contain.text, newItem) })4.2 存根StubAPI响应更常见的情况是我们希望完全控制服务器的响应用于测试前端在各种情况下的表现。it(当添加待办事项API失败时前端显示错误信息, () { const newItem 会失败的任务 // 拦截并立即返回一个模拟的错误响应不发送真实请求 cy.intercept(POST, /api/todos, { statusCode: 500, body: { message: Internal Server Error }, delay: 1000 // 模拟网络延迟 }).as(failedRequest) cy.get([data-testidtodo-input]).type(newItem {enter}) // 断言加载状态或禁用状态如果UI有的话 cy.get([data-testidadd-button]).should(be.disabled) cy.wait(failedRequest) // 断言错误信息被显示在UI上 cy.get([data-testiderror-message]) .should(be.visible) .and(contain.text, 添加失败请重试) })4.3 使用夹具Fixtures管理测试数据当测试需要复杂或重复的数据时可以将数据放在cypress/fixtures目录下的JSON文件中。cypress/fixtures/todos.json:[ { id: 1, text: 买牛奶, completed: false }, { id: 2, text: 学习Cypress, completed: true }, { id: 3, text: 写博客, completed: false } ]在测试中加载和使用beforeEach(() { // 拦截GET请求并返回fixture数据 cy.intercept(GET, /api/todos, { fixture: todos.json }).as(loadTodos) cy.visit(http://localhost:3000) cy.wait(loadTodos) // 等待模拟数据加载完成 }) it(页面加载时显示来自fixture的待办事项, () { cy.get([data-testidtodo-list] li).should(have.length, 3) cy.contains(学习Cypress).should(exist) // 断言已完成的任务有特定的样式 cy.contains(学习Cypress) .parents(li) .should(have.class, completed) })实操心得cy.intercept()是一个非常灵活的命令。你可以用它来监听请求而不修改用于cy.wait(alias)。存根请求返回静态响应。动态响应根据请求内容返回不同的数据。修改真实请求或响应例如添加认证头。 合理使用拦截和存根可以让你构建出完全独立、快速且稳定的测试套件。5. 自定义命令与可复用逻辑当你在多个测试文件中重复相同的操作序列时就该考虑自定义命令了。这能极大提升代码的可维护性和可读性。5.1 创建自定义命令在cypress/support/commands.js文件中添加// 添加一个待办事项的通用命令 Cypress.Commands.add(createTodo, (todoText) { cy.get([data-testidtodo-input]).type(${todoText}{enter}) // 可选添加一个隐式断言确保项目被添加 cy.get([data-testidtodo-list] li).should(contain.text, todoText) }) // 登录命令假设你的应用有登录功能 Cypress.Commands.add(login, (username testuser, password testpass) { cy.session([username, password], () { // 使用cy.session缓存登录状态加速测试 cy.visit(/login) cy.get([data-testidusername]).type(username) cy.get([data-testidpassword]).type(password {enter}) cy.url().should(include, /dashboard) // 断言登录成功 }) })5.2 在测试中使用自定义命令describe(使用自定义命令, () { beforeEach(() { cy.visit(http://localhost:3000) }) it(通过自定义命令快速添加多个任务, () { cy.createTodo(任务A) cy.createTodo(任务B) cy.createTodo(任务C) cy.get([data-testidtodo-list] li).should(have.length, 3) }) })注意事项自定义命令虽然方便但不要过度使用。简单的、仅在本文件内复用的逻辑更适合提取成普通的JavaScript函数。自定义命令最适合那些具有副作用如访问DOM、发出网络请求且需要在多个测试套件中共享的复杂操作。6. 测试运行策略与CI/CD集成本地开发时我们使用cypress open打开图形化测试运行器。但对于持续集成CI环境我们需要无头Headless运行。6.1 命令行运行与参数配置在package.json中添加脚本{ scripts: { cy:open: cypress open, cy:run: cypress run, cy:run-chrome: cypress run --browser chrome, cy:run-firefox: cypress run --browser firefox, cy:run-record: cypress run --record --key your-record-key // 录制到Cypress Cloud } }运行所有测试npm run cy:run运行特定测试文件npx cypress run --spec \cypress/e2e/todo.cy.js\运行某个测试套件npx cypress run --spec \cypress/e2e/todo.cy.js\ --grep \添加\(需要安装cypress-grep插件)6.2 在GitHub Actions中集成Cypress创建一个.github/workflows/cypress-tests.yml文件name: Cypress E2E Tests on: [push, pull_request] jobs: cypress-run: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv4 - name: Setup Node.js uses: actions/setup-nodev4 with: node-version: 18 cache: npm - name: Install dependencies run: npm ci # 使用ci命令确保依赖锁一致 - name: Start development server (if needed) run: npm start # 后台启动你的应用服务器 env: NODE_ENV: test # 使用测试环境配置 - name: Wait for server to be ready run: npx wait-on http://localhost:3000 # 等待应用可访问 - name: Run Cypress tests uses: cypress-io/github-actionv6 with: browser: chrome headless: true record: false # 如果不需要录制到Cypress Cloud设为false # config-file: cypress.config.js # 指定配置文件 env: # 可以在这里传递环境变量给Cypress CYPRESS_BASE_URL: http://localhost:3000 # 可选上传测试结果或截图如果测试失败 - name: Upload Cypress screenshots (on failure) uses: actions/upload-artifactv4 if: failure() with: name: cypress-screenshots path: cypress/screenshots - name: Upload Cypress videos uses: actions/upload-artifactv4 if: always() with: name: cypress-videos path: cypress/videos关键配置解析wait-on这是一个非常实用的npm包用于等待某个端口、URL或文件就绪。确保在运行测试前你的应用服务器已经启动并可以访问。cypress-io/github-action这是Cypress官方提供的GitHub Action它帮你处理了浏览器安装、依赖缓存等繁琐步骤是最推荐的集成方式。环境变量通过CYPRESS_*前缀的环境变量可以覆盖cypress.config.js中的任何配置项这在CI环境中非常有用可以灵活切换测试环境地址、数据库连接等。6.3 并行测试与测试分组随着测试套件增长运行时间会变长。Cypress支持通过--parallel和--group参数在CI上进行并行测试但这通常需要配合Cypress Cloud服务或第三方工具如cypress-parallel来实现负载均衡。基本思路是将测试文件拆分到多个机器上同时运行。7. 常见问题排查与调试技巧实录即使有了Cypress测试也不会一帆风顺。以下是我在实际项目中踩过的坑和总结的技巧。7.1 元素定位失败最常见的问题问题cy.get(...)超时找不到元素。排查思路检查选择器打开Cypress Test Runner使用左上角的“选择器 playground”工具来验证你的选择器是否能唯一标识目标元素。优先使用>原因表现解决方案未处理的异步操作点击按钮后UI状态因动画或微任务延迟更新。使用.should()断言目标状态而不是cy.wait(毫秒数)。例如cy.get(.success-message).should(be.visible)。依赖第三方服务/API测试因天气API、支付网关不稳定而失败。使用cy.intercept()彻底存根所有外部API调用让测试完全可控。测试间状态污染测试A创建的数据影响了测试B。在beforeEach或afterEach钩子中清理状态。如果是Web应用使用cy.session()隔离登录状态如果是数据库调用重置接口。随机生成的数据测试依赖于随机数或当前时间。使用固定种子seed的随机数生成器或使用cy.clock()来控制和模拟时间。一个处理时间的例子it(测试具有时间敏感性的功能, () { // 1. 设置当前时间为一个固定时间点 const now new Date(2023, 9, 1, 10, 30, 0).getTime() // 2023-10-01 10:30:00 cy.clock(now) // 2. 进行测试操作... cy.visit(/dashboard) // 页面可能会显示“上午好”这个断言在固定时间下是稳定的 cy.contains(上午好).should(be.visible) // 3. 如果需要可以让时间“快进” cy.tick(1000 * 60 * 60) // 快进1小时 cy.contains(上午好).should(not.exist) cy.contains(中午好).should(be.visible) })7.4 调试利器.pause()与.debug()当测试行为不符合预期时不要盲目猜测。cy.pause()在命令链中插入此命令测试运行会暂停。你可以打开浏览器开发者工具自由地检查DOM、Console、Network就像在普通网页上一样。检查完毕后点击Cypress Test Runner中的“继续”按钮。cy.debug()此命令会暂停测试并将上一个命令产生的主体subject打印到Console。例如cy.get(button).debug()会暂停并打印出找到的jQuery对象你可以展开它查看所有属性。我个人最常用的调试组合是cy.get(...).then($el { debugger; })。这会在浏览器中触发一个真正的断点你可以使用所有熟悉的开发者工具功能进行单步调试。8. 从测试到质量守护构建健壮的测试策略掌握了Cypress的所有技巧后如何让它为你的项目创造最大价值关键在于构建一个分层的、高效的测试策略。不要试图用E2E测试覆盖一切。E2E测试运行慢、维护成本高。应该遵循经典的“测试金字塔”底层多、快、便宜单元测试Jest, Vitest。测试独立的函数、组件。中层中集成测试/组件测试Cypress Component Testing, React Testing Library。测试组件间或模块间的交互。顶层少、慢、贵端到端E2E测试Cypress E2E Testing。测试完整的、用户视角的关键业务流程。Cypress同时支持E2E测试和组件测试。对于前端项目我的建议是用组件测试覆盖UI交互逻辑例如一个按钮点击后是否调用了正确的回调函数表单校验是否正常工作。这比E2E测试快得多。用E2E测试覆盖核心用户旅程例如“用户从首页登录搜索商品加入购物车完成结算”。每个主要功能模块只写1-2个覆盖“快乐路径”的E2E测试。将E2E测试集成到CI/CD门禁确保合并到主分支的代码不会破坏核心功能。可以配置在每次Pull Request时自动运行E2E测试套件。定期运行完整的测试套件可以在每天夜间定时运行全部E2E测试生成测试报告监控测试稳定性。最后保持测试代码的质量和产品代码一样重要。及时重构重复的测试逻辑给测试用例和自定义命令起一个清晰的名字编写有意义的失败信息。当测试失败时你或你的同事应该能一眼看出是哪里出了问题以及为什么出了问题。这样Cypress才能真正成为你团队质量保障的“大玩家”而不是一个额外的负担。

相关新闻

围棋AI分析终极指南:LizzieYzy完整使用教程

围棋AI分析终极指南:LizzieYzy完整使用教程

围棋AI分析终极指南:LizzieYzy完整使用教程 【免费下载链接】lizzieyzy LizzieYzy - GUI for Game of Go 项目地址: https://gitcode.com/gh_mirrors/li/lizzieyzy 围棋AI分析工具LizzieYzy是一款功能强大的开源围棋软件,它基于Lizzie开发&#x…

2026/10/9 7:27:08 阅读更多 →
Sunshine游戏串流完整教程:从零开始打造你的专属云游戏平台

Sunshine游戏串流完整教程:从零开始打造你的专属云游戏平台

Sunshine游戏串流完整教程:从零开始打造你的专属云游戏平台 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 你是否曾梦想过在客厅的电视上畅玩高性能PC游戏&#xff0c…

2026/10/9 11:13:36 阅读更多 →
Unity虚拟人开发全链路解析:从核心架构到多平台部署实战

Unity虚拟人开发全链路解析:从核心架构到多平台部署实战

1. 项目概述:为什么虚拟人开发是当下的技术焦点? 最近几年,无论是线上发布会、虚拟主播,还是智能客服与数字员工,虚拟人(Virtual Human)的身影越来越频繁地出现在我们的视野中。这背后不仅仅是视…

2026/10/13 9:35:06 阅读更多 →

最新新闻

WSL 下 CPU 版 Caffe 与 Faster R-CNN 环境搭建实战

WSL 下 CPU 版 Caffe 与 Faster R-CNN 环境搭建实战

1. 为什么要在 WSL 里折腾 CPU 版 Caffe 和 Faster R-CNN先把场景说清楚。你手里大概率是一台 Windows 笔记本或者台式机,显卡要么是核显,要么是 N 卡但驱动环境不想动,总之短期内不打算碰 CUDA。可你又想跑通一个经典的目标检测流程&#xf…

2026/10/13 17:58:35 阅读更多 →
三种手眼标定方法实战对比:AX=XB、非线性优化与FOE

三种手眼标定方法实战对比:AX=XB、非线性优化与FOE

简介:这份PDF资料聚焦机器人视觉中的手眼标定技术,面向计算机视觉、双目视觉与机器人抓取方向的学习者和研究者,系统讲解末端执行器与摄像头坐标系转换关系的求解思路。内容从Euler角、Rodrigues表示、Cayley表示及FOE点等基础知识切入&#…

2026/10/13 17:58:35 阅读更多 →
交换机端口镜像SPAN全解析:从原理到排障实战

交换机端口镜像SPAN全解析:从原理到排障实战

抓包可以说是网络工程师最常用的排查手段之一,但很多刚入行的朋友第一次遇到“端口镜像”这个概念时,都会卡在一个问题上:明明交换机上连着好几台设备,我把电脑接在交换机上,为什么用抓包工具就是看不到别的机器在跑什…

2026/10/13 17:58:35 阅读更多 →
图书管理系统课程设计:从E-R图到SQL Server存储过程的完整实践指南

图书管理系统课程设计:从E-R图到SQL Server存储过程的完整实践指南

简介:一份软件工程与数据库课程设计任务书,围绕“图书管理系统”展开完整数据库设计流程,适合高校计算机、软件工程专业学生作为课程设计参考。文档覆盖需求分析、概念结构设计(E-R图)、逻辑结构设计(关系模…

2026/10/13 17:58:35 阅读更多 →
自然语言优化求解系统:NLP与运筹学融合的工程实践

自然语言优化求解系统:NLP与运筹学融合的工程实践

1. 这不是又一个“大模型微调教程”,而是一套可落地的NLPSolver工程实践“从零构建一个自己的自然语言优化求解系统”——这个标题里藏着三个容易被忽略但极其关键的词:自己的、优化求解、系统。它不指向LLM API调用封装,不等于Prompt Engine…

2026/10/13 17:58:35 阅读更多 →
PS中英文对照表制作指南:从菜单导出到PDF生成与版本维护

PS中英文对照表制作指南:从菜单导出到PDF生成与版本维护

简介:这份PS中英文对照表PDF面向Photoshop初学者及需要查阅英文界面术语的设计从业者,帮助解决中文版与英文版菜单名称对应困难、切换语言环境后找不到功能入口的问题。资源包共1个PDF文件,大小约18KB,轻量易存,适合打…

2026/10/13 17:57:33 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →