前端单元测试实战指南:从Vitest选型到组件测试覆盖率落地
最近带前端小组做技术基建发现很多同学一听到“写单测”就皱眉觉得是给项目拖后腿。但只要把工具链和流程理顺前端单元测试反而是我目前回报率最高的一笔技术投入——它不光是验证某个函数返回值更多是在帮你守住组件行为、接口约定和重构手感。这篇文章把我踩过的坑和实际跑通的做法整理出来覆盖 Vitest、Jest、Testing Library 的选型对比组件交互与异步用例怎么写以及覆盖率怎么定才不扯淡。不管你是刚接触单测的新手还是已经在项目里写过一堆脆弱用例、准备重建信心的老手都可以照着这套路子直接落地。1. 为什么前端单元测试值得写先看清楚投入产出比1.1 单元测试到底在测什么很多人对前端单元测试的第一反应是“测工具函数”。比如把日期格式化、金额转换、数组去重单独抽出来测一遍确实有意义但这只是最基础的部分。前端项目的核心资产是组件和页面状态真正容易出问题的不是纯函数而是“点击按钮触发接口→接口返回后更新页面→加载态和错误态切换”这一整条链路。单元测试要守住的是这些组件级行为在改动后依然符合预期。我习惯把单元测试理解为“给组件拍一张行为快照”。不是快照测试那个 snapshot而是把用户能感知到的关键交互固化成一个可重复的验证按钮点了会变输入框填错会报错数据加载中不出现空白页。这样理解之后你就不会纠结“要不要测 CSS”“要不要测视觉还原”这种问题单元测试本来就不负责这些。1.2 哪些场景适合写哪些场景别硬写拿到一个新页面或新组件先判断它值不值得写单测。我的经验是逻辑密度大于 UI 密度单测价值就高。比如表单校验、分页状态、权限控制、购物车计算这种逻辑多且容易在重构时被改坏必须优先覆盖。纯展示型组件比如一个只接受 props 的徽标、图标测它的渲染内容和快照意义不大写多了反而变成“为了改断言而改断言”。还有三类场景我明确不建议硬写单测一是强依赖大量 ECharts、Canvas 拖拽、WebRTC 这类浏览器能力jsdom 里模拟成本太高二是组件内部塞了几百行业务代码、拆不出来这种应该先做代码拆分而不是硬写用例三是需求还在频繁变动的原型阶段今天按钮文案明天就换这时候写测试属于给沙子打地基等交互定版再补。2. 工具链选型Vitest、Jest、Testing Library 怎么选2.1 主流测试框架对比现在前端圈用到最多的就是 Vitest 和 Jest 两套。Vitest 因为和 Vite 天生一套启动速度和 HMR 体验都很舒服Jest 生态老、社区大很多老项目里已经跑得好好的迁移也不是不行但要掂量一下成本。维度VitestJestMocha Chai启动速度快基于 Vite 按需加载慢全量收集 transform快但断言和 mock 要自己拼配置复杂度低Vite 项目基本零配置需要 babel/ts-jest 或 swc高中低全靠手动组装内置 Mock支持 vi.fn/vi.mock支持 jest.fn/jest.mock不支持组件测试生态vue/test-utils、React Testing Library 都兼容同样兼容要自己接 adapter适合场景新项目、Vite 工程、追求快反馈老 Jest 项目、习惯稳定生态纯 Node 工具库、不想引入太重框架另外组件测试还需要搭配 Testing Library 或 Vue Test Utils。Testing Library 的核心思想是“从用户视角测组件”你操作 DOM、断言的也是 DOM 上的内容不直接访问组件实例的内部状态。这套理念我建议尽量遵守因为凡是直接访问 data、wrapper.vm 的用例最后都容易和实现细节耦合死一旦内部重构就崩。2.2 我的推荐组合如果今天从零开始我会直接用 Vitest testing-library/vue或 testing-library/react jsdom。Vue 项目也可以用 vue/test-utils但我个人更偏爱 Testing Library因为它的 find 和 user-event 组合非常贴近真实操作。如果项目是 Vue 2 Jest 的老组合别急着推翻。Jest 在 Vue 2 里跑得很稳需要补的就是 vue/test-utils v1 和 jest-environment-jsdom 的版本对齐。迁移到 Vitest 等下次大版本升级再说不要让“换工具”这种动作混进“写测试”的需求里。注意Vitest 和 Jest 的配置有个常见坑——alias 解析。Vite 项目里指向 src但 Vitest 默认不会自动读取 Vite 的 alias 配置在新版本里支持不够彻底需要在 vitest.config.ts 里单独配一遍resolve.alias否则一跑测试就报Failed to resolve import。3. 从零搭一个可跑的单测环境3.1 环境配置和首屏踩坑先演示 Vue 3 Vitest 的搭建方式。项目是 Vite 默认模板命令行执行npm i -D vitest testing-library/vue jsdom testing-library/user-event然后在 package.json 里加一段脚本{ scripts: { test: vitest run, test:watch: vitest } }再建一个 vitest.config.tsimport { defineConfig } from vitest/config import { fileURLToPath, URL } from node:url export default defineConfig({ test: { environment: jsdom, globals: true, setupFiles: [./src/test-setup.ts], include: [src/**/*.{test,spec}.{ts,tsx}], testTimeout: 10000 }, resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } } })字段解释一下environment: jsdom让测试跑在浏览器模拟环境里globals: true允许直接写 describe/test/expect 不显式 importsetupFiles用来加载全局 polyfillinclude控制哪些文件会被当作测试识别。这里我要单独提一句globals 开不开是个取舍。开了写起来短但会让代码显式依赖全局变量不开的话每个测试文件都要从 vitest 里 import。我更推荐显式 import因为这样单个文件拿给别人看也能知道依赖了什么对新人友好一点。3.2 第一个组件用例走一遍搭好环境后写一个最基础的计数组件看看整个执行链路是否通畅。先创建src/components/Counter.vuetemplate button>import { render, screen } from testing-library/vue import userEvent from testing-library/user-event import { expect, test } from vitest import Counter from ./Counter.vue test(点击按钮后计数增加, async () { const user userEvent.setup() render(Counter) await user.click(screen.getByTestId(counter-btn)) expect(screen.getByTestId(counter-btn)).toHaveTextContent(1) })注意这里我用的是getByTestId。很多人喜欢用getByText找按钮但在这个组件里按钮文案本身就是断言对象用getByText会在点击后因为文案变化而找不到节点。用>test(输入关键词后展示搜索结果, async () { const user userEvent.setup() render(SearchBox) const input screen.getByPlaceholderText(请输入关键词) expect(input).toBeInTheDocument() await user.type(input, vitest) await user.keyboard({Enter}) expect(await screen.findByText(搜索结果vitest)).toBeInTheDocument() })这里有个细节输入完关键词后不要立刻同步断言结果。如果是接口请求渲染是异步的要用findByText而不是getByText。findBy会等一段时间并自动重试直到元素出现或超时这是处理异步断言的标配。4.2 异步请求的 Mock 策略真实项目里组件几乎都要调接口单测环境绝不能真的发 HTTP 请求。主流做法是 mock 掉接口层。这里有个层次问题你到底是 mock axios/fetch还是 mock 业务 API 模块我建议 mock 业务 API 模块而不是 mock axios。原因很简单业务 API 模块是组件依赖的“接口边界”你 mock 它测试的关心点是“组件在接口返回后怎么渲染”而不是“某个 URL 被请求了没有”。如果直接 mock axios用例会和你前端层的具体请求方式耦合万一哪天从 axios 换成 fetch所有测试都要跟着改。比如项目里有src/api/user.ts导出一个fetchUserInfo函数组件里这样写import { fetchUserInfo } from /api/user const getUser async () { loading.value true const data await fetchUserInfo() user.value data loading.value false }测试里这样 mockimport { vi } from vitest import { fetchUserInfo } from /api/user import UserCard from ./UserCard.vue vi.mock(/api/user, () ({ fetchUserInfo: vi.fn() })) test(接口返回后显示用户名, async () { vi.mocked(fetchUserInfo).mockResolvedValue({ id: 1, name: 张三 }) render(UserCard) expect(await screen.findByText(张三)).toBeInTheDocument() })注意vi.mock有提升行为会把这个 mock 拉到文件顶部执行。如果你在用例内部再mockResolvedValue不受影响但如果你试图 mock 一个变量后再动态改变返回值很容易踩到“mock 作用域”的坑。每个用例之间记得vi.clearAllMocks()避免上一次 mock 的参数残留影响下一个用例。4.2.1 接口报错分支一定要测很多团队写单测只写“成功路径”错误分支永远不测。结果一到线上接口挂了页面直接白屏或者一直转菊花。正确做法是错分支至少要有一条用例test(接口失败时展示错误提示, async () { vi.mocked(fetchUserInfo).mockRejectedValue(new Error(network error)) render(UserCard) expect(await screen.findByText(加载失败请稍后重试)).toBeInTheDocument() expect(screen.queryByText(张三)).not.toBeInTheDocument() })这一个用例往往比五个正常用例更值钱因为它验证的是你项目里最容易烂掉的兜底逻辑。我之前在一个订单详情页补了类似用例立刻抓到两个问题一是错误 toast 提示被重复展示三次二是 loading 状态在异常分支没有关掉。这些靠人工点点点很难稳定复现单测一跑就现原形。4.3 定时器、transition 和浏览器 API 的坑4.3.1 定时器用 fake timers组件里有倒计时、轮询或者防抖逻辑时真实等待会让测试又慢又飘。Vitest 的vi.useFakeTimers()可以把setTimeout/setInterval替换成可手动推进的假定时器。典型用法vi.useFakeTimers() test(倒计时到 0 后显示过期, () { vi.setSystemTime(new Date(2024-01-01T00:00:00)) render(Countdown) act(() { vi.advanceTimersByTime(10000) }) expect(screen.getByText(已过期)).toBeInTheDocument() })但要注意开启 fake timers 后userEvent.setup()也会受影响某些版本的 userEvent 会和 fake timers 打架。如果遇到user-event一直 pending可以临时不 fake或者改用fireEvent在少数场景下这是合理的再就是检查 userEvent 的白名单配置。真遇到这种我通常先vi.runOnlyPendingTimers()把当前挂起的定时器全部跑完再执行交互。4.3.2 Transition 和 Teleport 的坑Vue 的transition在测试环境里不会真正执行过渡动画但并不代表它没有副作用。组件里有v-show搭配 transition 时jsdom 环境下getComputedStyle往往拿不到正确的 transition 状态导致断言不稳定。我的做法是测试文件里全局禁用 transition。可以在 setup 文件里把Transition和TransitionGroup直接 mock 成一个穿透的插槽// src/test-setup.ts import { config } from vue/test-utils config.global.stubs { transition: false, transition-group: false }Teleport则要注意默认的 Teleport 会把内容挂到document.body上用screen查询时一般能查到但如果组件里有多个 Teleport 或 SSR 环境就建议指定teleport: true或者直接 mock 掉避免内容被“传送”到你搜不到的地方。4.3.3 window 属性不是全都有jsdom 虽然模拟了浏览器环境但它不是完整的浏览器。localStorage现代版本有但matchMedia、ResizeObserver、IntersectionObserver、getBoundingClientRect这些不一定全。我第一次跑弹窗组件测试时ResizeObserver直接报is not defined排查了半天才发现是没补 polyfill。公共 setup 文件里补一下// src/test-setup.ts import { vi } from vitest Object.defineProperty(window, matchMedia, { writable: true, value: vi.fn().mockImplementation((query) ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn() })) }) class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} } Object.defineProperty(global, ResizeObserver, { writable: true, value: ResizeObserverMock })5. 常见问题与排查技巧实录5.1 Vue 项目里的高发报错速查报错信息常见原因解决思路Failed to resolve import /xxxVitest 没读到 Vite alias在 vitest.config.ts 里配置 resolve.aliasTypeError: wrapper.vm is undefinedVue Test Utils mount 返回对象被误用确认 mount 后拿到了组件实例别在 setup script 里直接访问闭包变量Cannot find module vuemonorepo 里多版本 Vue 冲突把 vue 加入resolve.dedupe或者用 peerDependenciesRequest is not defined组件内用了 fetch但 jsdom 未启用environment 设为 jsdom并安装 whatwg-fetch polyfillHydration node mismatch测试环境和运行环境 HTML 不一致检查 SSR 组件用createSSRApp或者 mock 掉依赖 window 的代码ReferenceError: IntersectionObserver is not defined组件懒加载依赖观察器在 setup 里补 mock见上文You are using the runtime-only build测试环境没解析 templateVite 下调整 vitejs/plugin-vue 的配置参与标准 Vue 模板编译Element is not attached to the document组件内部使用了 getElementById 或希望元素挂载到 body用 Testing Library 的 render 已自动挂载别手动 appendChild这张表是我从一个真实项目里“整理”出来的不是网上复制的泛泛清单。每个报错背后都对应一种“测试环境和真实浏览器不一致”的问题排查时先问一句这个 API 在 jsdom 里到底实现没实现这是主线。5.2 测试不稳定的两大元凶时序和状态残留单测最怕“这次绿下次红”这种 flaky 状态。我归纳下来九成问题出在两处。第一是异步时序。一个用例里同时出现了await user.type、mockResolvedValue、nextTick如果没有正确等待断言可能跑在渲染之前。解决办法很粗暴能用findBy就别用getBy能等用户的交互结束再断言就不要在trigger后立刻读文本。findBy默认有 1 秒等待窗口足够覆盖大部分异步渲染。第二是测试间状态残留。全局的 pinia store、vue-router、国际化 locale 都是单例一个用例 set 了 locale 为英文下一个用例没重置中文文案断言就挂了。我建议在每个afterEach里做干净的重置import { afterEach } from vitest afterEach(() { vi.clearAllMocks() vi.resetModules() vi.useRealTimers() document.body.innerHTML })这里resetModules会清掉模块缓存避免某个 mock 文件被其他用例污染。代价是重新加载模块会慢一点但换来的稳定性非常值。5.3 覆盖率怎么定才不扯淡很多公司会给前端团队压覆盖率指标比如“核心模块要 80%”。我的意见是覆盖率只适合作为流程参考线不适合当 KPI。我见过团队为了把行覆盖率从 75% 怼到 90%硬是给一堆模板里的空标签和纯展示组件写了几十个expect(wrapper.exists()).toBe(true)。这种覆盖率数据纯属自欺欺人真正有价值的覆盖率是“关键业务分支有没有被覆盖”的审计工具不是管理层打分的账单。实际操作上我是按模块分级设定阈值的基础库、通用工具函数行覆盖不低于 85%分支覆盖不低于 70%业务组件涉及表单、接口、权限行覆盖不低于 70%但要求interaction分支点按钮、填表单、错误提示至少有 1 条用例纯展示组件不设覆盖率门槛只看渲染是否正常遗留代码只做“接触式”补测把重构时最容易碰到崩溃的核心入口覆盖到即可在 vitest 配置里可以这样写test: { coverage: { provider: istanbul, reporter: [text, html, lcov], thresholds: { lines: 60, functions: 50, branches: 40, }, include: [src/**/*.{ts,vue}], exclude: [src/main.ts, src/router/**] } }注意provider: istanbul或v8都行Vite 5 以上建议用v8跑得更快。覆盖率数据只是给你一个“哪里完全没碰过”的清单真正的判断标准是你最近改的一个核心函数有没有对应用例守在那里。写在最后的一段实战心得踩了这么多坑之后我最大的体会是前端单元测试和写业务代码其实是相辅相成的。当你发现一个函数很难测通常不是测试的问题而是函数设计有问题当你发现一个组件写用例特别费劲它多半在组件边界上堆了太多不该有的副作用。与其硬写一堆 mock 去绕不如回头把组件拆得更干净——把纯函数抽出去、把接口调用收敛成 API 模块、把状态更新用 computed 或 reactive 规范化。这样单测顺了业务代码的维护成本也跟着降。最后再分享一个小技巧别把测试文件写到组件旁边就完事我最开始就这么干习惯性“顺手跳过”测试文件的时候特别容易误伤。现在我都统一放到src/__tests__下并在提交前让 CI 强制跑一遍vitest run这样单测才会真正变成项目的地基而不是某个模块可有可无的点缀。

相关新闻

DeepSeek Harness桌面端实操:大模型测试工作台从配置到批量执行

DeepSeek Harness桌面端实操:大模型测试工作台从配置到批量执行

DeepSeek Harness 官方桌面端终于发布了。作为从命令行时代一路配置过来的老用户,我的第一反应不是“又多了一个客户端”,而是“那套折磨人半年的 YAML 配置总算可以甩到后台了”。这个工具说白了就是给大模型做测试和评测的工作台:你把它装上…

2026/10/3 10:19:06 阅读更多 →
Python手写CFD求解器:从涡量-流函数到收敛可视化

Python手写CFD求解器:从涡量-流函数到收敛可视化

简介:本资源是西北工业大学(NWPU)计算流体力学课程高分大作业的完整Python实现方案,面向高校流体力学、航空航天或工程仿真方向的本科生与研究生,用于辅助理解偏微分方程数值解法、网格生成与流场可视化等核心内容。压…

2026/10/3 10:19:06 阅读更多 →
Go 多级排序实战:sort.Interface、稳定排序与泛型封装

Go 多级排序实战:sort.Interface、稳定排序与泛型封装

1. 从一次业务需求说起:为什么 Go 的排序这么“麻烦” 先说我最近遇到的一件事。后台管理系统要导出一张订单列表,排序规则大概是这样的:先按订单状态分组,状态相同就按金额降序,金额也一样就按创建时间升序&#xff0…

2026/10/3 10:19:06 阅读更多 →

最新新闻

AI工程化从零开始:手写神经网络与部署全链路实战指南

AI工程化从零开始:手写神经网络与部署全链路实战指南

别把“from scratch”理解歪了。它不是让你用某个流行框架三分钟训练一个模型,也不是什么“零基础转码 AI 速成”的噱头。在我眼里,ai-engineering-from-scratch 就是一句话:作为一个工程师,你究竟能不能不依赖任何封装&#xff0…

2026/10/3 10:52:53 阅读更多 →
Python机器学习量化投资研究:数据、算法集成与回测全流程解析

Python机器学习量化投资研究:数据、算法集成与回测全流程解析

简介:这是一套面向量化投资学习者和金融科技从业者的Python机器学习实战资源,以基本面量化投资为核心场景,集成了线性回归、决策树、随机森林、支持向量机、XGBoost、LSTM等多种算法,覆盖数据预处理、特征工程、模型训练、策略回测…

2026/10/3 10:52:52 阅读更多 →
机器学习+量化投资:从特征工程到多算法集成的完整实践

机器学习+量化投资:从特征工程到多算法集成的完整实践

简介:本资源为Python机器学习驱动的基本面量化投资研究项目,包含完整源码、配套数据集与项目说明文档,面向具备一定Python基础、希望将机器学习算法应用于金融量化场景的学习者和研究者。包内共219个文件,主要由167个csv数据文件、…

2026/10/3 10:52:52 阅读更多 →
从零开始的AI工程:Prompt、Harness与Agent实战拆解

从零开始的AI工程:Prompt、Harness与Agent实战拆解

1. 从零开始的AI工程:这个领域到底在解决什么问题1.1 当"会用AI"变成"能交付AI系统",差距就出来了这两年我一直在做AI相关项目的落地交付,接触过大量团队后感受特别深:真正卡住大家的地方,早就不再…

2026/10/3 10:52:52 阅读更多 →
AI工程从零开始:构建可维护的大模型应用完整链路

AI工程从零开始:构建可维护的大模型应用完整链路

你有没有过这样的体验——用ChatGPT写几段代码、让它帮你润色一封邮件,觉得AI也不过如此?但当你接到一个真正的任务,比如“给公司做一个AI客服助手”“把工单系统接入大模型自动分类”,你会发现事情完全不一样了。模型吐出来的内容…

2026/10/3 10:52:52 阅读更多 →
Jev本地部署实战:从模型权重到Codex接入与RAG系统

Jev本地部署实战:从模型权重到Codex接入与RAG系统

最近我的几个技术群突然被同一个名字刷屏了:Jev。有人把它描述成“新出的编程模型”,有人在 Codex 工作流里已经接上它跑起了代码生成,还有人在到处问 Windows 能不能本地部署。我看了一圈社区讨论、GitHub 仓库和几个公开的实操案例&#xf…

2026/10/3 10:51:52 阅读更多 →

日新闻

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南 【免费下载链接】ex-skill 前任 skill 项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill 前任.skill 是一个运行在 Claude Code 上的开源 Skill:导入微信、iMessage、短信、…

2026/10/3 0:00:27 阅读更多 →
45个经典Linux面试题:从命令到网络排障的完整考点解析

45个经典Linux面试题:从命令到网络排障的完整考点解析

刚开始带应届生的时候,我最头疼的就是他们拿着一摞Linux面试题背得滚瓜烂熟,一上机全露馅。后来自己从被面的人变成面别人的人,才慢慢摸清楚:Linux面试题考的根本不是答案本身,而是你面对一个不确定的系统问题时&#…

2026/10/3 0:01:28 阅读更多 →
SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

简介:本资源是一份面向SAP ABAP开发人员、生产计划专员及ERP实施顾问的实操型操作指南,聚焦SAP生产预留核心业务场景,系统解决物料预留创建、查询、校验与批量处理等高频问题。文档以结构化方式覆盖预留背景原理、OMC2编码规则、工厂级参数配…

2026/10/3 0:01:28 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/3 9:14:33 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/3 9:47:50 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/10/3 9:42:31 阅读更多 →

月新闻

我发现了一个新思路:用 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/2 10:36:31 阅读更多 →
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/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练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/3 9:42:36 阅读更多 →