VS2010+CEF3 90.6.0:MFC窗口嵌入浏览器与JS双向通信实战
简介这份资源面向使用VS2010进行Windows桌面开发的C程序员解决在MFC应用程序中嵌入CEF3浏览器内核并实现JavaScript与C双向交互的问题。内容围绕CEF3 90.6.0版本展开涵盖MFC项目创建、CEF库引用配置、CEFRenderWidgetHost子类封装、CEF与MFC消息循环同步以及通过CefMessageRouterBrowserSide::Handler注册JS调用C函数等关键环节适合需要为传统MFC程序引入HTML5、CSS、JavaScript渲染能力的中高级开发者参考。压缩包共629个文件约457.22MB以269个h头文件、168个pak资源包、28个dll动态库、14个lib静态库及9个cpp源文件为主另含obj、pdb、tlog等编译中间产物与解决方案文件完整保留了工程构建痕迹。目前已有1556人学习下载。借助该工程读者可对照目录结构理解CEF3在MFC中的集成方式梳理JS与C通信的注册与回调流程并参考启动参数配置与消息循环处理思路快速搭建可运行的嵌入环境。1. 把浏览器塞进 MFC 窗口一个 VS2010 CEF3 90.6.0 的嵌入实战如果你手上有一个维护了十几年的 MFC 老项目界面还停留在原生控件时代而产品经理突然要求“内嵌一个浏览器能加载网页、能跟 C 双向通信”你会怎么办我最近拆的这个资源就是干这件事的在 VS2010 环境下用 CEF3 的 90.6.0 版本把 Chromium 内核嵌进 MFC 窗口并且打通了 JS 调用 C 的链路。它适合两类人一是被老项目绑住、必须用 VS2010 工具链的 C 开发者二是想搞明白 CEF3 多进程架构到底怎么跟 Win32 消息循环缝合的人。资源里给的是完整可编译的工程不是零散代码片段拿到手就能看到窗口怎么创建、消息怎么转发、JS 怎么调到 C 函数。2. 环境搭建与工程结构VS2010 下把 CEF3 90.6.0 跑起来2.1 为什么是 VS2010 CEF3 90.6.0 这个组合先说清楚这个组合的来由。CEF3 的 90.x 分支对应 Chromium 90是最后一个还能用 VS2010 工具集编译的稳定分支之一。再往后的版本Chromium 的构建系统对 C 标准要求提高VS2010 的 MSVC10 编译器就扛不住了。所以如果你维护的是 VS2010 工程90.6.0 基本是“能用的最新版”。资源里提供的二进制包已经编译好了 libcef.dll 和配套的 lib 文件省去了自己编译 Chromium 这个动辄几小时的苦力活。工程结构上资源分成三块cef_binary目录放 CEF3 的头文件、lib 和 dllMFCEmbed目录是 MFC 主工程包含窗口创建和消息处理js_bridge目录放 JS 调用 C 的接口定义和示例页面。这种分法很清晰改的时候不会互相污染。2.2 工程配置包含目录、库目录和运行时依赖拿到工程第一步不是急着编译而是把 CEF3 的路径配好。VS2010 的工程属性里需要改三个地方# 在 VS2010 项目属性中配置 # 1. C/C - 常规 - 附加包含目录 $(ProjectDir)..\cef_binary\include # 2. 链接器 - 常规 - 附加库目录 $(ProjectDir)..\cef_binary\lib # 3. 链接器 - 输入 - 附加依赖项 libcef.lib libcef_dll_wrapper.lib这里有个细节libcef_dll_wrapper.lib是 CEF3 提供的 C 封装层它把 C 风格的 API 包成了 C 类。如果你只用 C API可以不链它但资源里的代码用的是 C 封装所以必须加。另外libcef.dll和chrome_elf.dll要跟 exe 放同一目录否则运行时会直接报“找不到 libcef.dll”。还有一个容易翻车的地方VS2010 默认用/MDd或/MD运行时而 CEF3 的预编译库通常用/MT。如果链接时报LNK2038或RuntimeLibrary不匹配就把工程改成/MTdDebug或/MTRelease跟 CEF3 库保持一致。2.3 目录结构一览资源解压后的目录长这样CEF3_MFC_Demo/ ├── cef_binary/ │ ├── include/ # CEF3 头文件 │ ├── lib/ # libcef.lib, libcef_dll_wrapper.lib │ ├── Release/ # libcef.dll, chrome_elf.dll │ └── Resources/ # locales, cef.pak 等资源 ├── MFCEmbed/ │ ├── MFCEmbed.cpp # 主程序入口 │ ├── MainFrm.cpp # 主框架窗口 │ ├── CefView.cpp # CEF3 浏览器窗口封装 │ └── JsBridge.cpp # JS 调用 C 的桥接实现 ├── js_bridge/ │ ├── index.html # 示例页面 │ └── bridge.js # JS 侧调用封装 └── README.mdResources目录不能少CEF3 初始化时要加载cef.pak、cef_100_percent.pak和locales下的语言包。如果程序启动后白屏或者直接崩先检查这个目录有没有跟 exe 放一起。3. 核心模块拆解从 CEF3 初始化到 MFC 窗口嵌入3.1 CEF3 多进程初始化CefInitialize 与消息循环CEF3 是多进程架构主进程负责窗口和消息循环渲染进程负责解析 HTML 和跑 JS。初始化必须在 MFC 的InitInstance里做而且要在创建主窗口之前// MFCEmbed.cpp 中的 InitInstance 片段 CefMainArgs main_args(GetModuleHandle(NULL)); // 1. 执行子进程逻辑如果是渲染进程就直接返回 int exit_code CefExecuteProcess(main_args, NULL, NULL); if (exit_code 0) { return exit_code; } // 2. 配置 CEF3 启动参数 CefSettings settings; settings.multi_threaded_message_loop false; // 跟 MFC 消息循环共用主线程 settings.no_sandbox true; // VS2010 下建议关沙箱避免权限问题 CefString(settings.locales_dir_path) LResources\\locales; CefString(settings.resources_dir_path) LResources; // 3. 初始化 CEF3 CefInitialize(main_args, settings, NULL, NULL); // 4. 创建 MFC 主窗口 CMainFrame* pFrame new CMainFrame(); pFrame-Create(NULL, LCEF3 MFC Embed); pFrame-ShowWindow(SW_SHOW); // 5. 进入 MFC 消息循环 int ret CWinApp::Run(); // 6. 退出时关闭 CEF3 CefShutdown(); return ret;这里的关键参数是multi_threaded_message_loop。设成false表示 CEF3 不自己起消息循环线程而是把消息处理挂到 MFC 的主线程上。这样做的好处是 JS 调用 C 时不用考虑跨线程同步坏处是如果 C 侧处理太慢会卡住整个界面。资源里选的是false因为示例场景是轻量级调用没必要引入线程复杂度。no_sandbox true在 VS2010 环境下几乎是必须的。Chromium 的沙箱依赖较新的 Windows API老工具链编译出来的程序开沙箱容易在启动时直接崩。关掉沙箱后渲染进程的权限跟主进程一样安全性降低但开发阶段省心。3.2 把浏览器窗口嵌进 MFCCefView 的创建与布局CEF3 的浏览器窗口本质上是一个子窗口要嵌到 MFC 的框架里得先创建一个静态控件占位再把 CEF3 的窗口贴上去// CefView.cpp 中创建浏览器窗口 void CCefView::CreateBrowser(HWND hParent, const CRect rect) { // 1. 保存父窗口和区域 m_hParent hParent; m_rect rect; // 2. 配置浏览器窗口信息 CefWindowInfo window_info; window_info.SetAsChild(hParent, rect); // 作为子窗口嵌入 // 3. 配置浏览器设置 CefBrowserSettings browser_settings; browser_settings.javascript STATE_ENABLED; // 开启 JS // 4. 创建浏览器实例 CefBrowserHost::CreateBrowser( window_info, m_client.get(), // 实现 CefClient 接口的对象 Lfile:///js_bridge/index.html, browser_settings, NULL, NULL ); }SetAsChild是嵌入的关键它告诉 CEF3 把浏览器窗口创建成hParent的子窗口并指定位置和大小。MFC 这边需要在OnSize里同步调整 CEF3 窗口的大小否则拖动窗口边框时浏览器区域不会跟着变// MainFrm.cpp 中响应 WM_SIZE void CMainFrame::OnSize(UINT nType, int cx, int cy) { CFrameWnd::OnSize(nType, cx, cy); if (m_cefView m_cefView-GetBrowserHost()) { // 获取 CEF3 的窗口句柄并调整大小 HWND hCef m_cefView-GetBrowserHost()-GetWindowHandle(); ::SetWindowPos(hCef, NULL, 0, 0, cx, cy, SWP_NOZORDER); } }这段代码看起来简单但漏掉就会导致浏览器区域跟窗口脱节。我见过有人把 CEF3 窗口创建在对话框上结果对话框一缩放浏览器就跑到屏幕外面去了就是没做这个同步。3.3 JS 调用 CCefV8Handler 与消息路由JS 调 C 是这套方案的核心价值。CEF3 提供了CefV8Handler接口让你在 C 侧注册一个函数JS 里通过window.cefQuery或者自定义对象来调。资源里用的是自定义对象的方式更直观// JsBridge.cpp 中实现 V8 Handler class CJsBridgeHandler : public CefV8Handler { public: virtual bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) OVERRIDE { if (name callCpp) { // 1. 从 JS 参数里取出方法名和参数 if (arguments.size() 1 || !arguments[0]-IsString()) { exception 参数错误需要方法名字符串; return true; } CefString method arguments[0]-GetStringValue(); // 2. 根据方法名分发到具体的 C 处理函数 if (method getVersion) { retval CefV8Value::CreateString(CEF3 90.6.0 on VS2010); } else if (method showMessage) { CefString msg arguments[1]-GetStringValue(); ::MessageBox(NULL, msg.ToWString().c_str(), L来自JS, MB_OK); retval CefV8Value::CreateBool(true); } else { exception 未知方法 method; } return true; } return false; } IMPLEMENT_REFCOUNTING(CJsBridgeHandler); };然后在CefClient的OnContextCreated回调里把这个对象挂到window上// 在 OnContextCreated 中注册 void CCefClient::OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) { CefRefPtrCefV8Value global context-GetGlobal(); CefRefPtrCefV8Handler handler new CJsBridgeHandler(); CefRefPtrCefV8Value bridge CefV8Value::CreateObject(NULL); bridge-SetValue(callCpp, CefV8Value::CreateFunction(callCpp, handler), V8_PROPERTY_ATTRIBUTE_NONE); global-SetValue(cppBridge, bridge, V8_PROPERTY_ATTRIBUTE_NONE); }JS 侧调用就一行// bridge.js var result window.cppBridge.callCpp(getVersion); console.log(result); // 输出 CEF3 90.6.0 on VS2010这里有个坑Execute里如果返回falseCEF3 会继续找其他 handler如果都没有就抛 JS 异常。返回true表示已处理retval就是 JS 拿到的返回值。参数类型必须严格匹配JS 传数字过来C 侧要用IsInt()或IsDouble()判断不能直接GetIntValue()否则会崩。4. 避坑与常见问题VS2010 CEF3 的踩坑记录4.1 启动直接崩没有任何提示现象双击 exe 后进程一闪而过或者弹窗说“应用程序正常初始化失败”。原因最常见的是libcef.dll或chrome_elf.dll没跟 exe 放一起或者Resources目录路径不对。CEF3 初始化时会去加载cef.pak和locales下的语言包找不到就直接崩。解决把cef_binary/Release下的所有 dll 和Resources整个目录复制到 exe 所在目录。如果还是崩在CefInitialize之前加CefEnableHighDPISupport()试试有些 Win10 环境对 DPI 敏感。4.2 编译报 LNK2038 或 RuntimeLibrary 不匹配现象链接阶段报错提示_ITERATOR_DEBUG_LEVEL或RuntimeLibrary不匹配。原因VS2010 工程默认用/MDd而 CEF3 的预编译库用/MTd。两者混用会导致运行时库冲突。解决把工程属性里 C/C - 代码生成 - 运行时库改成/MTdDebug或/MTRelease跟 CEF3 库保持一致。改完后清理解决方案重新编译。4.3 JS 调用 C 没反应控制台也不报错现象JS 里调window.cppBridge.callCppC 侧断点没进去JS 也没抛异常。原因OnContextCreated没触发或者注册的对象名跟 JS 里写的不一致。还有一种可能是页面加载在OnContextCreated之前就执行了 JS导致cppBridge还没挂上去。解决在 JS 里用setTimeout延迟调用或者监听DOMContentLoaded事件。更稳妥的做法是在 C 侧等OnLoadEnd之后再执行 JS。另外检查OnContextCreated是否在CefClient里正确重写了这个回调默认是空的不重写就不会注册任何对象。4.4 窗口缩放后浏览器区域错位现象拖动 MFC 主窗口边框CEF3 的浏览器区域不跟着变或者只变一部分。原因MFC 的OnSize里没有同步调整 CEF3 子窗口的大小。CEF3 窗口是独立的 HWND不会自动跟随父窗口。解决在OnSize里拿到 CEF3 的窗口句柄用SetWindowPos重新设置位置和大小。注意要判断GetBrowserHost()是否为空因为窗口创建初期浏览器还没初始化完。4.5 关闭窗口时进程残留现象点了关闭按钮主窗口消失了但任务管理器里还有进程没退。原因CEF3 的渲染进程和 GPU 进程没有跟着主进程退出。如果CefShutdown()没调用或者调用时机不对就会残留。解决在CWinApp::ExitInstance里调CefShutdown()确保在 MFC 消息循环退出之后执行。另外在CefLifeSpanHandler的OnBeforeClose里做引用计数清理避免浏览器对象没释放导致进程卡住。5. 二次开发改造从示例到生产环境的几个关键调整资源里的示例能跑通但离生产环境还有距离。我一般会做三件事第一把 JS 调用的接口从硬编码的if-else改成注册表模式用std::mapCefString, std::function存方法名和回调新增接口不用改Execute函数第二给CefSettings加上cache_path让浏览器缓存落到本地目录避免每次启动都重新加载资源第三在CefLifeSpanHandler里处理OnBeforeClose确保关闭窗口时先释放浏览器对象再退出消息循环。改造后的接口注册大概长这样// 用 map 管理 JS 可调用的方法 class CJsBridgeRegistry { public: using Handler std::functionCefRefPtrCefV8Value(const CefV8ValueList); void Register(const CefString name, Handler handler) { m_handlers[name] handler; } bool Invoke(const CefString name, const CefV8ValueList args, CefRefPtrCefV8Value retval) { auto it m_handlers.find(name); if (it m_handlers.end()) return false; retval it-second(args); return true; } private: std::mapCefString, Handler m_handlers; };这样新增一个 JS 接口只需要registry.Register(getVersion, ...)不用动Execute里的分发逻辑。验证方法也简单在 JS 里调一个没注册的方法看 C 侧是否返回false并抛异常如果直接崩了说明参数校验没做。还有一个容易忽略的点CEF3 的CefV8Value是引用计数对象跨线程传递时必须用CefRefPtr包住否则渲染进程和主进程之间传参时会出玄学崩溃。我踩过一次JS 传了个对象过来C 侧直接取GetStringValue()结果进程随机挂后来加了IsString()判断才稳定。从那以后我每次改 JS 桥接层都强制走一遍“空参数、错类型、超长字符串”三个边界测试确认不会崩再往下写。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

大模型安全测试实战:从红队模拟到供应链防护

大模型安全测试实战:从红队模拟到供应链防护

最近 AI 社区传开一个颇为戏剧性的安全测试话题:A社疑似故意让 Opus 模型进入“黑化”状态,让它模拟攻击者去“入侵”Hugging Face 平台,最后抱抱脸只留下一句“?”。消息本身没有完整的官方披露,但真正值得讨论的并不…

2026/10/17 2:58:42 阅读更多 →
AI Agent 管理为何是份全职工作:从 Demo 到生产的工程挑战

AI Agent 管理为何是份全职工作:从 Demo 到生产的工程挑战

“AI Agent 部署之后,才能真正看清它的复杂度:环境搭建只是开始,持续运行、权限管理、上下文维护、模型选型和成本控制才是真正的职业消耗。”从业者对 AI Agent 的热情,往往停留在一个漂亮的 Demo 上:一个能回答文档问…

2026/10/17 2:58:42 阅读更多 →
编译原理实验避坑:PL/0扩展ELSE语句的词法、语法与代码生成全解析

编译原理实验避坑:PL/0扩展ELSE语句的词法、语法与代码生成全解析

简介:这份资源是面向高校计算机专业学生的编译原理课程实验配套资料,以经典教学型编译程序PL/0为改造对象,帮助学习者在词法分析、语法分析与语义处理三个环节上完成从理论到代码的落地。实验要求对PL/0进行多项扩充:新增ELSE、FO…

2026/10/17 2:58:42 阅读更多 →

最新新闻

PyTorch模型跨框架部署:ONNX转TensorFlow实战与避坑指南

PyTorch模型跨框架部署:ONNX转TensorFlow实战与避坑指南

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

2026/10/17 3:44:56 阅读更多 →
低成本AI短剧制作全流程:0.02元/秒成本控制与实操指南

低成本AI短剧制作全流程:0.02元/秒成本控制与实操指南

1. 低成本AI短剧的底层逻辑与方案选型1.1 为什么0.02元/秒这个数字值得认真对待第一次看到“0.02元/秒”这个报价的时候,我的反应跟大多数人一样:要么是引流噱头,要么就是有什么隐藏的坑。但实际跑完整个流程之后,我发现这个数字在…

2026/10/17 3:44:56 阅读更多 →
CentOS yum源报错排查:修复Cannot find valid baseurl的三种方案

CentOS yum源报错排查:修复Cannot find valid baseurl的三种方案

CentOS 的 yum 源报错,大概是 Linux 运维里出现频率最高的一类问题了。尤其是Cannot find a valid baseurl for repo: base这句,几乎每个用过 CentOS 7 的人都见过。我最近在整理一批退役虚拟机时又踩了一遍,处理过程中翻了不少资料&#xff…

2026/10/17 3:44:56 阅读更多 →
从零到发布:用TaoToken辅助完成VSCode插件开发全流程

从零到发布:用TaoToken辅助完成VSCode插件开发全流程

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

2026/10/17 3:44:56 阅读更多 →
hustoj源码深度解析:工业级在线判题系统架构与沙箱实现

hustoj源码深度解析:工业级在线判题系统架构与沙箱实现

简介:本资源为华中科技大学HUSTOJ在线评测系统最新源码(r2133版本),专为ACM/ICPC程序设计竞赛训练与教学场景设计,适用于高校算法教练、竞赛集训队及有自建OJ需求的开发者。压缩包共2000个文件,主体包含281…

2026/10/17 3:44:56 阅读更多 →
AI仿真凭什么敢信?代理模型原理、可信度验证与工程应用边界解析

AI仿真凭什么敢信?代理模型原理、可信度验证与工程应用边界解析

前阵子有位做结构设计的同行问我:AI仿真秒级出结果,你敢拿它直接下设计结论吗?这个问题问到了点子上。AI仿真这几年已经从“玩票”变成了CAE圈子里的热门话题,各家厂商都在推“实时仿真”“AI代理模型”“零阶降阶”的概念。但工程…

2026/10/17 3:43:56 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →