React 18 服务器错误恢复机制深度解析:Suspense 兜底、水合回退与 onRecoverableError 完整指南
前端【免费下载链接】rfcsRFCs for changes to React项目地址https://gitcode.com/gh_mirrors/rfc/rfcs点击查看免费下载React 18 引入了一套全新的服务器渲染错误恢复机制当组件在服务端抛出异常时React 不再让整个页面崩溃而是输出最近的Suspense边界 fallback 的 HTML再在客户端水合阶段自动重试渲染水合过程中发现的节点缺失、多余或文本不匹配也会触发同样的回退到干净客户端渲染路径。本文以 RFC《Server Errors in React 18》仓库中的 text/0215-server-errors-in-react-18.md为核心结合同仓库的 React 18 发布总览 与 React 18 中的 Suspense 等关联文档完整讲解这套机制的设计动机、底层流程、onRecoverableError上报 API以及它在实际 SSR 项目中的落地方式与注意事项。读完本文你将理解可恢复错误recoverable error的确切定义掌握如何用Suspense边界为服务器渲染提供优雅的容错分层并能正确配置错误上报来捕获这些不打断用户体验的异常。背景React 18 之前服务器渲染错误无处可逃在 React 18 之前服务器端渲染SSR的容错能力几乎为零组件在服务器上渲染时一旦抛出异常renderToString会直接抛错整页渲染失败。客户端有错误边界error boundaries但错误边界在服务器上不可用——因为它们依赖组件 state 才能工作而服务器渲染是一次性的无状态输出。因此在旧版 React 中任何服务器端组件错误都是致命的要么整个应用被迫降级为纯客户端渲染丢失 SSR 的全部收益要么根据应用自身的部署方式直接整体崩溃。与此同时水合hydration阶段也埋着隐患当客户端渲染结果与服务器 HTML 不完全一致时例如某个节点在服务器上渲染出来了、客户端却没有或文本内容不同旧版 React 会尝试逐个打补丁——在客户端插入或删除单个节点来强行匹配服务器标记。这种做法不仅难以保证渲染结果一致在某些极端情况下还可能引发隐私或安全漏洞例如服务器与客户端渲染的分支不同导致敏感内容被错误地挂载进 DOM。RFC《Server Errors in React 18》正是为了解决这两类问题而提出为服务器错误提供一种自然的、基于已有概念的恢复机制同时把水合不匹配从警告升级为错误级处理。核心机制三条规则看懂整个设计该 RFC 的 Summary 部分给出了整套机制的三条核心规则服务器错误 → fallback HTML 客户端自动重试当错误在服务器上抛出时React 会从服务器输出 fallback 的 HTML即距离出错组件最近的那个Suspense边界所声明的加载态内容随后在客户端水合阶段自动重试渲染该边界内的内容。水合错误 → 丢弃服务器 HTML回退到干净客户端渲染当水合过程中发生错误时React 会丢弃服务器渲染出的 HTML从最近的Suspense边界开始做一次干净的客户端重新渲染如果没有Suspense边界则从根节点整体回退。可恢复错误的定义只要客户端重试渲染成功原始错误就被视为可恢复的recoverable——它没有直接暴露给用户。但 React 在开发模式和生产模式都会记录这些错误以便你的错误上报系统捕获它们。另外还有一个行为变化缺失/多余节点和文本不匹配现在被当作错误而不是警告。React 不再尝试打补丁式地逐节点修正服务器标记而是直接回退到客户端渲染。这样做是为了保证不一致情况下的渲染结果确定且安全——水合错误不应被忽略而应被开发者当作真正的错误来处理。基础示例用Suspense包一层服务器错误即可恢复原 RFC 给出了一个最直观的示例。以前如果某个组件在服务器渲染时抛错renderToString会直接抛出异常整页失败现在你只需把可能出错的部分包进Suspense边界Suspense fallback{Spinner /} Comments / /Suspense如果Comments或其内部的任何组件在服务器上抛错React 会在服务器输出的 HTML 中用Spinner /的渲染结果替换失败内容同时写入一个特殊标记special marker告知客户端这个 Suspense 边界的内容在服务器上失败了客户端在水合阶段识别到这个标记后自动重试渲染Comments /成功则展示真实内容。水合不匹配hydration mismatch也走完全相同的重试路径客户端发现服务器 HTML 与自己的渲染结果不一致时不再逐节点修补而是触发一次客户端重新渲染。这个模式之所以优雅在于它复用了你已经为加载状态设计好的Suspense边界加载时显示Spinner /服务器出错时也显示Spinner /用户视角下只是这部分内容稍微晚了一点加载出来仍然看到一个有意图的、符合设计的视觉状态。只有客户端重试也失败时React 才会像普通渲染错误那样抛出异常交给最近的错误边界处理。动机为什么需要一套新的恢复机制错误边界依赖 state在服务器上无法工作客户端错误边界error boundary之所以能工作是因为它们基于组件实例的 state例如componentDidCatch记录错误、getDerivedStateFromError更新 state。服务器渲染是一次性输出没有 state、没有重试的机会所以错误边界在服务器上天然失效。虽然 RFC 提到未来不排除推出独立的服务器端错误边界 API但在那之前复用Suspense边界是让应用自然恢复的最佳途径。服务器与客户端运行同一份代码客户端可能成功由于服务器和客户端执行的是同一套组件代码很多错误本质上源于服务器环境的特殊性比如某个只在服务器上存在的数据源、某个依赖浏览器 API 的库。这类错误在服务器上抛出但客户端环境完全可能渲染成功。因此与其在服务器上直接放弃整页不如把是否真的失败的判断推迟到客户端——客户端能成功就展示内容不能成功再抛给错误边界。水合警告升级为错误的三条理由原 RFC 明确指出整个提案中最具争议的部分是把水合警告按错误同等对待。理由是React 无法可靠地修补不匹配在最坏情况下一次不一致可能导致隐私或安全漏洞现有的修补启发式规则与新增的 SSR 组件懒加载支持并不总是兼容过去没有自动但细粒度的错误恢复机制而现在有了即基于Suspense边界的回退路径。换句话说与其维护一套复杂的修补启发式不如让 React 面对不一致时干脆利落地整体重试用可预期的行为换取正确性与安全性。详细设计一错误如何从服务器传输到客户端服务器端错误恢复的核心是一个标记 重试协议服务器端如果某个Suspense边界的内容在服务器上渲染失败React 会输出该边界最近 fallback 的 HTML用户看到的是加载态而不是出错内容并附带一个特殊标记。客户端水合时 React 识别到这个失败标记会为该边界调度一次重试渲染。重试成功 → 展示结果重试再次抛错 → 按普通渲染期异常处理由最近的错误边界接管。值得注意的是这套协议与 React 18 的流式 SSR 架构天然衔接。React 18 的服务器渲染器renderToPipeableStream/renderToReadableStream详见 React 18 发布总览支持按Suspense边界分段输出HTML慢的内容先输出 fallback就绪后再在同一流中输出真实内容并附带一个内联script把内容插入正确位置。错误恢复复用的正是这条边界粒度的通道——Suspense RFC 中描述的流式渲染、选择性水合selective hydration能力让客户端不必等所有代码块加载完就能开始水合也使得某个边界失败、其他部分照常工作成为可能。详细设计二水合不匹配的两种处理路径水合不匹配在新机制下被分为两类处理方式截然不同属性不匹配Attribute mismatch保持原状属性attribute不匹配的处理方式与之前完全一致仅在开发模式下给出警告不做任何修补尝试。因为属性不一致通常不影响 DOM 结构本身不值得为此回退整棵子树。文本内容、缺失/多余节点不匹配回退到客户端重渲染这是本次行为变化的重点旧行为React 尝试修补整棵树以匹配客户端渲染结果——逐节点插入、删除。新行为React 丢弃到最近Suspense边界为止的服务器 HTML然后同步地从该边界做一次干净的客户端重新渲染。若出错点上方没有任何Suspense边界则从根节点整体重试并丢弃全部服务器 HTML。这个回退的代价是显而易见的比原来的打补丁更慢而且会丢弃一部分 DOM 状态例如用户已经在一个input里输入的内容。但它的收益是输出结果始终一致——这在正确性和安全性上远比尽力修补可靠。suppressHydrationWarning 的语义变化某些文本不匹配在实践中难以避免最典型的是时间戳服务器渲染和客户端渲染发生在不同时刻渲染出的时间文本必然不同。旧的suppressHydrationWarningprop 就是为此设计的——它把某个节点标记为服务器与客户端的文本内容刻意不同。问题在于在新机制下水合不匹配会触发客户端恢复重渲染而suppressHydrationWarning正好阻止这个恢复行为——也就是说它现在拥有了生产环境下的实际行为而不再仅仅是一个压制开发警告的开关。命名因此变得令人困惑RFC 明确表示未来版本大概率会把它改名见下文遗留问题。一个需要避免的反模式Suspense fallback{null}你可能会想为了让水合不匹配的恢复更细粒度在更多子树外包一层Suspense但又没有合适的加载状态于是写Suspense fallback{null}。RFC 明确警告这是不好的模式同一个空洞边界会被其他场景复用——错误恢复时会用到它、懒加载组件时也会用到它。一个fallback{null}的边界在这些场景下都会表现为一整块空白体验很差。React 团队计划未来引入一种可以标记为仅作最后手段last resort的 Suspense 边界类型在那之前只应在有刻意设计的加载状态如 spinner时才添加Suspense边界。详细设计三错误上报——onRecoverableError回调可恢复错误不会打断用户体验但它们仍然值得被记录。为此createRoot和hydrateRoot两个 API 都新增了onRecoverableError选项hydrateRoot(container, App /, { onRecoverableError(error) { // 将错误上报到你的错误监控服务 report(error); } });需要理解可恢复错误与普通错误的区别普通错误会导致用户体验彻底损坏可恢复错误不会。原 RFC 明确了两种可恢复错误的触发场景hydrateRoot场景服务器端发生了错误但客户端已重试渲染成功或水合时发生了错误但客户端已重试渲染成功createRoot场景在使用并发特性如startTransition时组件抛出异常React 会以防万一同步重试一次渲染——这常常能绕开与尚未兼容并发特性的库之间的并发 bug。若这次重试渲染成功该错误即被报告为可恢复错误。如果未指定onRecoverableError默认实现会在浏览器支持reportError时调用它把错误交给浏览器全局的错误处理通道否则回退到console.error。RFC 同时说明未来大概率会扩充更多类型的可恢复错误。权衡与缺点这套机制不是没有代价原 RFC 坦诚地列出了四个主要缺点水合错误修复不及时会带来更大的性能回退如果你不修复水合错误每次触发回退到客户端重渲染的开销远大于旧版打补丁的代价。而人们往往因为 React 的水合错误信息不够清晰而难以调试suppressHydrationWarning语义混乱它如今拥有了生产环境行为阻止恢复重渲染却顶着压制警告的名字存量 SSR 应用初期会整页回退目前绝大多数 SSR 应用没有任何Suspense节点因此升级后第一次遇到水合错误时会从根节点整体重试客户端渲染——这对没有心理预期的团队会是一次不愉快的惊喜添加Suspense边界需要刻意的加载状态没有刻意加载状态就到处加Suspense会对其他依赖 Suspense 的特性如懒加载、流式渲染造成负面影响直到last resort边界机制落地。备选方案为什么没有走这些路RFC 也记录了被否决的替代设计理解它们有助于把握这套机制的设计边界服务器错误导致整页失败维持旧行为不可接受因为大量错误本质上只在服务器环境发生在服务器上实现错误边界但错误边界解决不了水合不匹配的问题且依赖 state 的根本矛盾仍在继续用各种启发式修补内容不可靠、不安全正是本次要废弃的做法为客户端错误恢复设计一种独立的边界类型而不是复用Suspense会引入新的概念和 API 面而复用 Suspense 让特性开箱即用在当前版本就重命名suppressHydrationWarning而不是留到未来版本团队选择推迟以避免在 18 主版本里同时引入太多破坏性变化。采用策略与落地路线开箱即用无需修改现有代码这是本 RFC 最关键的落地优势该特性默认生效升级到 React 18 后不需要改动任何现有代码。无论你使用createRoot还是hydrateRoot新的错误恢复机制都已就位升级 API 的具体迁移见 React 18 发布总览 中react-dom/client与react-dom/server的新 API 清单。两个需要快速跟进的功能RFC 指出首发版本之后需要尽快跟进两个配套特性更好的水合错误消息让开发者更容易定位和修复水合不匹配一种把某些 Suspense 边界标记为last resort的方式以便在没有刻意加载状态的子树处也能安全地放置空 fallback。与 React 16 错误边界引入的类比整体上这次变更的推广难度与 React 16 引入错误边界时相当但因为它完全建立在既有概念Suspense之上更像是一次幕后行为升级而非需要大量教学的新 API。大部分改动在框架层自动消化——升级 React 18、切换到新渲染 API如renderToPipeableStream之后框架会在底层为你启用这些能力。对开发者而言主要的用户侧变化是两点水合错误不再被糊弄过去以及onRecoverableError成为错误上报的新入口。遗留问题两个尚未敲定的 API 决策截至该 RFC 发布仍有两个悬而未决的问题suppressHydrationWarning何时、以何种方式改名以匹配它新增的生产环境行为last resort Suspense 边界的 API 形态如何标记、如何与现有 fallback 机制共存。这两个问题都计划在后续版本中解决当前阶段建议遵循 RFC 的指引只在有刻意加载状态时添加Suspense边界并为水合错误配置好onRecoverableError上报。总结把服务器错误从崩溃降级为加载态React 18 的服务器错误恢复机制本质上是把服务器端渲染从单点失败即全盘崩溃的脆弱模型升级为与客户端并发能力对等的健壮模型以Suspense边界为粒度服务器输出 fallback客户端自动重试错误可记录但不必打断用户。它同时把水合不匹配从尽力修补的警告转变为干净重试的错误用性能上的些许代价换取了渲染结果的一致性与安全性。配合 React 18 的流式 SSR、选择性水合与startTransition这套机制让服务器错误在用户视角下只是这部分内容加载得稍慢了一点——这正是以 Suspense 为核心的 React 18 渲染架构想要传达的核心理念。延伸阅读本文核心依据text/0215-server-errors-in-react-18.mdSuspense 语义与流式 SSR 基础text/0213-suspense-in-react-18.mdReact 18 新渲染 API 与并发特性总览text/0212-react-18.md与水合正确性相关的useSyncExternalStore用于外部数据源在 SSR/水合期间保持一致text/0214-use-sync-external-store.md赞分享前端【免费下载链接】rfcsRFCs for changes to React项目地址https://gitcode.com/gh_mirrors/rfc/rfcs点击查看免费下载相关推荐用微信聊天记录微调出微信数字分身WeClone 完整实战指南用微信聊天记录微调出微信数字分身WeClone 完整实战指南 每天回答同类问题、发同样的回复真的很费时间。要是有人能“替你”应答就好了。WeClone 就是人工智能大模型微调LoRAAI 应用React 18 Suspense 机制深度解析与最佳实践React 18 Suspense 机制深度解析与最佳实践 前言 React 18 带来了许多令人振奋的新特性其中 Suspense 机制的增强尤为引人注目。前端Warp 远程服务器安装容错机制解析curl/wget 检测回退与 SCP 上传兜底APP-4386Warp 远程服务器安装容错机制解析curl/wget 检测回退与 SCP 上传兜底APP 4386 导读 本文以 Warp 仓库中 APP 4386 技桌面应用开发者工具人工智能AI 应用AI Agent代码智能体上一篇如何永久保存你的QQ空间青春记忆GetQzonehistory终极备份指南下一篇高效提取Google Maps数据Go语言开源爬虫工具实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

scope 仓库中的 critbitgo:Go 语言 Crit-bit Tree 实现原理与 IP 路由表应用指南

scope 仓库中的 critbitgo:Go 语言 Crit-bit Tree 实现原理与 IP 路由表应用指南

云原生可观测性容器编排运维 【免费下载链接】scope Monitoring, visualisation & management for Docker & Kubernetes 项目地址: https://gitcode.com/gh_mirrors/sc/scope 点击查看 免费下载 导读 本文围绕 vendor/github.com/k-sone/critbitgo 这份文…

2026/10/12 4:24:37 阅读更多 →
CC Switch:Claude Code 配置切换管理工具,告别手动改配置

CC Switch:Claude Code 配置切换管理工具,告别手动改配置

开始之前先问一句:你是不是也经历过这种场面——手里的 Claude Code 项目,昨天还在用一个模型服务,今天想换成另一家,结果得翻出配置文件,改 apiKey、改 baseURL、改 model 名,改完还要小心翼翼检查是不是漏…

2026/10/12 4:24:37 阅读更多 →
open-code-review:一种提升评审可审计性与协作透明度的轻量级实践范式

open-code-review:一种提升评审可审计性与协作透明度的轻量级实践范式

1. “open-code-review”不是个工具名,而是一套可落地的协作范式“open-code-review”这个词组乍看像某个开源项目或CLI工具的名称,但实际在技术社区里,它根本没注册过任何知名仓库,GitHub上搜不到同名主力项目,npm、P…

2026/10/12 4:24:37 阅读更多 →

最新新闻

throw/throws 关键字

throw/throws 关键字

一、throws 作用throws 写在方法声明处,用于声明该方法有可能抛出某种异常,把异常交给调用这个方法的人去处理。一句话区分:throws 是声明异常,自己不捕获,抛给上层调用者。 和 try-catch 的区别:try-catch…

2026/10/12 5:50:26 阅读更多 →
宠物门禁端侧AI落地解析:画质检测、活体甄别与离线推理

宠物门禁端侧AI落地解析:画质检测、活体甄别与离线推理

宠物门禁做不好的原因,多数被归到"识别准确率不够"。但从工程角度看,身份比对是链路最后一环,真正决定成败的是它前面的两段:输入质量与运行时环境。本文从宠物门禁的产品逻辑出发,分析其识别链路&#xff0…

2026/10/12 5:50:26 阅读更多 →
GCC 11.4.0源码编译指南:从configure到make的完整流程与避坑

GCC 11.4.0源码编译指南:从configure到make的完整流程与避坑

简介:gcc-11.4.0.tar.gz 是 GNU 编译器集合 11.4.0 版本的官方源码包,面向需要在 Linux/Unix 环境下自行编译安装 GCC 的开发者、系统工程师及编译原理学习者。它解决了从源码构建工具链、定制编译选项与优化策略的需求,适合具备一定 C/C 基础…

2026/10/12 5:50:26 阅读更多 →
Django REST framework实战解析:从序列化器到API治理的核心价值

Django REST framework实战解析:从序列化器到API治理的核心价值

前几天有位朋友问我:项目里已经用了Django,前后端分离时直接写个View函数返回JsonResponse不就行了,为什么还要再学一套Django REST framework?平时看文档总觉得它绕,序列化器、视图集、路由器一堆概念,不知…

2026/10/12 5:50:26 阅读更多 →
Neuphonic 开源 43MB 语音决策模型 NeuDecide:不经 ASR 语音调用工具;法国 Mallow 儿童无屏语音游戏音箱,AI 听孩子语音实时推进丨日报

Neuphonic 开源 43MB 语音决策模型 NeuDecide:不经 ASR 语音调用工具;法国 Mallow 儿童无屏语音游戏音箱,AI 听孩子语音实时推进丨日报

本期编辑:三水 鲍勃 01 有话题的技术 1、端侧 AI 模型公司 Liquid AI 开放两款多模态决策模型 d1:600M 首次支持音频输入,3B 可在 Jetson 上实现毫秒级决策 Liquid AI 发布 d1-3B 和实验性模型 d1-omni-600M,两款模型分别支持文…

2026/10/12 5:50:26 阅读更多 →
WinSxS 组件存储占用 C 盘?DISM 清理与修复指南

WinSxS 组件存储占用 C 盘?DISM 清理与修复指南

简介:面向Windows Server 2012 R2标准版运维人员的SXS源文件包,专门用于修复系统内置.NET Framework 3.5安装失败并需指定备用源路径的故障。资源按微软官方组件结构整理,聚合运行库、界面资源、配置项与数据库支持等多类依赖,可在…

2026/10/12 5:49:26 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 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/11 10:45:37 阅读更多 →
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/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练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/11 14:36:54 阅读更多 →