Web组件集成:加载网页与JS交互(JavascriptProxy)(34)
在 HarmonyOS 的 ArkUI 开发中Web 组件是实现混合开发Hybrid的核心。通过 Web 组件开发者可以在应用内无缝嵌入网页并通过 JSBridge 实现原生端ArkTS与前端H5的双向通信。以下是 Web 组件集成与 JS 交互一、 Web 组件的四种加载方式Web 组件支持多种数据来源以适应不同的业务场景加载在线 URLWeb({ src: https://... })适用于加载网络页面。注意需在module.json5中申请ohos.permission.INTERNET权限。加载本地 rawfileWeb({ src: $rawfile(index.html) })适用于加载打包在src/main/resources/rawfile/目录下的本地 HTML 文件。resource 协议Web({ src: resource://rawfile/index.html })适用于运行时动态指定本地文件路径。loadData 渲染字符串通过controller.loadData(htmlStr, ...)直接渲染 HTML 字符串适用于加载富文本数据。import { webview } from kit.ArkWeb; import { BusinessError } from kit.BasicServicesKit; Entry Component struct WebLoadDemo { controller: webview.WebviewController new webview.WebviewController(); // 用于 loadData 渲染的 HTML 字符串 State htmlContent: string !DOCTYPE html htmlheadtitle动态内容/title/head body h1 stylecolor: #007AFF;这是动态生成的内容/h1 p无需任何文件直接渲染字符串/p /body/html; build() { Column() { Text(Web 组件四种加载方式演示) .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ bottom: 10 }) Row({ space: 10 }) { // 方式一加载在线 URL Button(加载在线URL) .onClick(() { try { this.controller.loadUrl(https://developer.huawei.com/); } catch (error) { console.error(ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}); } }) // 方式二加载本地 rawfile Button(加载本地Rawfile) .onClick(() { try { this.controller.loadUrl($rawfile(index.html)); } catch (error) { console.error(ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}); } }) // 方式三使用 resource 协议 Button(Resource协议) .onClick(() { try { this.controller.loadUrl(resource://rawfile/index.html); } catch (error) { console.error(ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}); } }) // 方式四loadData 渲染字符串 Button(LoadData渲染) .onClick(() { try { this.controller.loadData( this.htmlContent, text/html, // mimeType UTF-8 // encoding ); } catch (error) { console.error(ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}); } }) } .margin({ bottom: 10 }) // Web 组件初始化默认加载本地页面 Web({ src: $rawfile(index.html), controller: this.controller }) .width(100%) .layoutWeight(1) .javaScriptAccess(true) .domStorageAccess(true) } .width(100%) .height(100%) .padding(20) } }代码要点解析加载在线 URL点击按钮后通过controller.loadUrl()加载华为开发者官网。注意需在module.json5中添加网络权限requestPermissions: [{ name: ohos.permission.INTERNET }]加载本地 rawfile使用$rawfile(index.html)静态引用指向src/main/resources/rawfile/index.html。该方式支持在 HTML 中正常引用同目录下的 CSS、JS、图片等资源。resource 协议使用resource://rawfile/index.html字符串格式与$rawfile()指向同一目录但它是纯字符串可以在运行时动态拼接和构造路径。loadData 渲染字符串无需任何文件直接将 HTML 内容作为字符串传入controller.loadData()进行渲染。适合做富文本邮件、协议弹窗、动态内容展示等场景。二、 ArkUI 与 Web 的双向通信机制双向通信是 Hybrid 开发的基石主要分为“正向调用”和“反向注入”两个方向1. 正向通信ArkUI 调用 Web (runJavaScript)原生端通过WebviewController的runJavaScript方法可以直接执行网页中的 JS 代码并支持通过回调获取返回值。this.controller.runJavaScript(webFunction(Hello from ArkUI!), (error, result) { if (!error) { console.info(Web返回数据 result); } });注意该方法必须在页面加载完成后调用如在onPageEnd回调中否则可能执行失败。2. 反向通信Web 调用 ArkUI (JavaScriptProxy)这是最核心的 JSBridge 实现方式。通过javaScriptProxy将 ArkUI 对象注入到 Webview 的window对象中前端即可像调用本地方法一样调用原生能力。ArkTS 侧代码// 1. 定义原生对象 class NativeObj { makePhoneCall(number: string): void { console.info(准备拨打电话 number); } } // 2. 在 Web 组件中注入 Web({ src: $rawfile(index.html), controller: this.controller }) .javaScriptProxy({ object: new NativeObj(), name: native, // 前端通过 window.native 访问 methodList: [makePhoneCall], // 允许调用的方法白名单 controller: this.controller })Web (JS) 侧调用window.native.makePhoneCall(10086);三、 动态注册与注销除了初始化时静态注入ArkUI 还支持在运行时动态管理注入对象动态注册使用controller.registerJavaScriptProxy(obj, name, methods)。刷新生效动态注册后必须执行controller.refresh()刷新页面注入才会生效注销对象使用controller.deleteJavaScriptRegister(name)移除已注入的对象。ArkTS 侧代码import { webview } from kit.ArkWeb; import { BusinessError } from kit.BasicServicesKit; // 1. 定义需要注入到前端的原生对象 class NativeBridge { sayHello(): string { return Hello from ArkTS Dynamic Proxy!; } } Entry Component struct DynamicProxyDemo { controller: webview.WebviewController new webview.WebviewController(); State bridgeObj: NativeBridge new NativeBridge(); build() { Column() { Row({ space: 10 }) { // 2. 动态注册按钮 Button(动态注册) .onClick(() { try { this.controller.registerJavaScriptProxy( this.bridgeObj, nativeBridge, // 前端访问的对象名 [sayHello] // 允许调用的方法白名单 ); // ⚠️ 核心步骤动态注册后必须刷新页面才能生效 this.controller.refresh(); } catch (error) { console.error(ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}); } }) // 3. 注销对象按钮 Button(注销对象) .onClick(() { try { this.controller.deleteJavaScriptRegister(nativeBridge); } catch (error) { console.error(ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}); } }) } .margin({ bottom: 10 }) // 4. Web 组件 Web({ src: $rawfile(index.html), controller: this.controller }) .width(100%) .layoutWeight(1) .javaScriptAccess(true) } .width(100%) .height(100%) .padding(20) } }Web (JS) 侧代码 (index.html)!DOCTYPE html html head titleDynamic Proxy Test/title /head body button onclickcallNative()调用原生方法/button p idresult/p script function callNative() { // 检查对象是否已注册 if (window.nativeBridge) { const msg window.nativeBridge.sayHello(); document.getElementById(result).innerText msg; } else { document.getElementById(result).innerText 原生对象未注册; } } /script /body /html代码要点解析动态注册时机与初始化时使用的.javaScriptProxy()不同registerJavaScriptProxy可以在任意运行时阶段调用例如在onPageEnd回调中或根据用户登录状态动态注入。强制刷新动态注入的对象不会自动同步到当前已加载的页面上下文中必须紧接着调用controller.refresh()重新加载页面注入才会真正生效。内存管理与注销当 H5 页面不再需要调用原生能力或者组件即将销毁时务必调用deleteJavaScriptRegister移除注入的对象。这能有效断开 ArkTS 对象与 WebView 之间的引用链防止内存泄漏。四、 进阶JSBridge 分层架构设计在实际的企业级 Hybrid 应用中建议不要将业务逻辑直接写在JavaScriptProxy中而是采用分层设计以提高通用性和灵活性通信层负责 ArkTS 与 JS 之间的数据传递屏蔽底层通信机制。通常将数据序列化为 JSON 字符串进行传递。通道层 (Channel)允许注册多种方法通道。JS 侧负责将 API 信息打包ArkTS 侧负责解包并分发给具体的处理方法处理完毕后再通过runJavaScript执行 JS 侧的回调函数。方法层具体的业务 API 实现如获取设备信息、拉起支付、路由跳转等。这种架构类似于小程序的底层通信规范能够极大地降低原生端与前端代码的耦合度便于后续的业务扩展与维护。1. ArkTS 侧通道层与通信层实现import web_webview from ohos.web.webview; // 1. 方法层定义具体的业务 API class DeviceService { getInfo(): string { return JSON.stringify({ model: HarmonyOS Device, osVersion: 5.0 }); } } // 2. 通道层负责解包、路由分发与回调 class ChannelManager { private deviceService: DeviceService new DeviceService(); // 处理来自 H5 的调用 call(channelType: string, objectJson: string): string { const params JSON.parse(objectJson); let result: any {}; if (channelType device.getInfo) { result this.deviceService.getInfo(); } else { result { code: -1, message: API not found }; } return JSON.stringify(result); } } // 3. 通信层注入到 Web 中的代理对象 class BridgeProxy { private channelManager: ChannelManager new ChannelManager(); // 这是唯一暴露给 JS 的通信方法 nativeMethod(channelType: string, objectJson: string): string { return this.channelManager.call(channelType, objectJson); } } Entry Component struct HybridPage { private webController: web_webview.WebviewController new web_webview.WebviewController(); private bridgeProxy: BridgeProxy new BridgeProxy(); build() { Column() { Web({ src: $rawfile(index.html), controller: this.webController }) .javaScriptProxy({ object: this.bridgeProxy, name: JSBridge, // 注入到 window 的对象名 methodList: [nativeMethod], // 通信白名单仅暴露通信层方法 controller: this.webController }) .width(100%) .height(100%) } } }2. Web (JS) 侧通道层与通信层实现!DOCTYPE html html headtitleJSBridge Layered Demo/title/head body button onclickcallNativeApi()获取设备信息/button p idresult/p script // 通道层封装打包与解包逻辑 function nativeCall(channelType, params) { const objectJson JSON.stringify(params); // 调用通信层注入的方法 const resultJson window.JSBridge window.JSBridge.nativeMethod(channelType, objectJson); return resultJson ? JSON.parse(resultJson) : null; } // 方法层业务调用 function callNativeApi() { const res nativeCall(device.getInfo, {}); document.getElementById(result).innerText JSON.stringify(res); } /script /body /html五、 安全权限管控Permission 配置在真实业务中将原生能力暴露给 H5 存在极大的安全风险。HarmonyOS 提供了细粒度的权限控制机制允许在javaScriptProxy注入时通过 JSON 字符串限制访问来源。核心逻辑权限配置分为对象级和方法级。对象级权限指定哪些 URL 可以访问该对象的所有方法方法级权限则进一步细化定义特定 URL 对特定方法的访问权。.javaScriptProxy({ object: this.nativeObj, name: native, methodList: [makePhoneCall, getUserInfo], controller: this.controller, // 安全权限配置 permission: { javascriptProxyPermission: { urlPermissionList: [ { scheme: https, host: trusted-domain.com, port: , path: } ], methodList: [ { methodName: makePhoneCall, urlPermissionList: [ { scheme: https, host: trusted-domain.com, port: , path: }, { scheme: resource, host: rawfile, port: , path: } ] } ] } } })安全建议务必严格校验event.origin或来源 URL避免动态拼接 JS 字符串防止 XSS 注入攻击。六、 高级通信通道WebMessagePort对于高频交互、大数据量传输或复杂的实时业务如 IM 聊天、实时音视频状态同步传统的runJavaScript和JavaScriptProxy可能面临性能瓶颈。此时推荐使用基于 HTML5 标准的WebMessagePort机制。核心逻辑原生侧调用createWebMessagePorts()创建一对消息端口[port1, port2]。将port2通过postMessage发送给 H5H5 接收后缓存该端口并监听onmessage事件。双方后续均通过port.postMessage()进行双向异步通信无需反复注入对象或执行 JS 脚本。// ArkTS 侧创建并分发端口 Button(创建数据通道) .onClick(() { const ports this.controller.createWebMessagePorts(); // 将 port[1] 传递给 H5 this.controller.postMessage(init_port, [ports[1]]); // 监听来自 H5 的消息 ports[0].onMessageEvent (message: webview.WebMessage) { console.info(收到H5消息: message.data); }; })七、 渲染性能优化同层渲染与组件鸿蒙化在 Hybrid 应用中如果 H5 内嵌了大量的原生交互组件如视频播放器、地图、复杂表单传统的 WebView 渲染会导致严重的性能损耗和交互割裂感。核心优化策略同层渲染利用 ArkWeb 提供的同层渲染能力将部分 H5 节点替换为原生 ArkUI 组件。原生组件直接嵌入到 WebView 的渲染树中获得与原生应用一致的滑动跟手体验和渲染效率。API 鸿蒙化针对 H5 侧强依赖的平台相关 API提供一套完整的 HarmonyOS 版本实现参考成熟的小程序框架规范确保原有前端业务逻辑无需大幅改动即可无缝运行在鸿蒙环境中。生命周期管理确保所有的 JSBridge 通信都在 H5 页面加载完成onPageEnd之后发起避免调用未定义的函数或对象导致异常。同时在页面销毁时及时注销代理对象防止内存泄漏。八、 架构分层MVVM 模式下的 Web 容器设计在大型应用中直接在 UI 组件中处理复杂的 Web 交互逻辑会导致代码臃肿且难以维护。建议引入 MVVM 模式进行分层解耦View 层UI 组件Web组件仅负责渲染和展示不包含任何业务逻辑。它通过Link或Prop接收来自 ViewModel 的状态如 URL、加载进度并通过事件回调通知 ViewModel 用户的交互如页面开始加载、加载完成。ViewModel 层状态与逻辑作为 Web 容器的核心管理者ViewModel 持有WebviewController实例并管理页面状态如加载进度、错误信息。它处理所有业务逻辑如权限校验、URL 拦截、错误重试等并更新状态以驱动 UI 变化。Model 层数据源定义 Web 容器的数据结构和配置可以来自本地配置或远程接口实现 Web 容器的动态化配置。九、 通信协议标准化的 JSBridge 设计直接使用runJavaScript和JavaScriptProxy进行通信会导致代码耦合度高难以维护。建议设计一套标准化的通信协议统一消息格式定义一套统一的 JSON 消息格式包含action操作类型、params参数、callbackId回调 ID等字段。所有 ArkTS 与 H5 的通信都遵循此格式。通道管理器Channel Manager在 ArkTS 侧实现一个通道管理器负责接收 H5 发来的消息根据action字段分发给不同的业务模块处理。处理完成后通过callbackId找到对应的回调函数并将结果返回给 H5。H5 SDK 封装在 H5 侧封装一个 JS SDK提供统一的invoke方法。H5 开发者只需调用invoke(actionName, params).then(...)SDK 内部负责消息的序列化、发送和回调管理对 ArkTS 的通信细节完全透明。十、 性能优化渲染与交互体验当 Web 页面内嵌大量原生交互组件时传统的 WebView 渲染会导致严重的性能损耗和交互割裂感。同层渲染Peer Rendering利用 ArkWeb 提供的同层渲染能力将部分 H5 节点如视频播放器、地图替换为原生 ArkUI 组件。原生组件直接嵌入到 WebView 的渲染树中获得与原生应用一致的滑动跟手体验和渲染效率。WebMessagePort 高频通信对于高频交互、大数据量传输或复杂的实时业务如 IM 聊天、实时音视频状态同步传统的runJavaScript和JavaScriptProxy可能面临性能瓶颈。此时推荐使用基于 HTML5 标准的WebMessagePort机制建立双向异步通信通道避免反复注入对象或执行 JS 脚本。预加载与缓存策略对于核心业务页面可以在应用启动时预加载 Web 容器并缓存静态资源。通过WebviewController的缓存接口配置合理的缓存策略减少网络请求提升页面加载速度。

相关新闻

AirDropPlus:打破生态壁垒,Windows与iPhone实现无线文件传输的完美方案

AirDropPlus:打破生态壁垒,Windows与iPhone实现无线文件传输的完美方案

AirDropPlus:打破生态壁垒,Windows与iPhone实现无线文件传输的完美方案 【免费下载链接】AirDropPlus Effortless file transfer and clipboard sync between Windows and iOS — powered by Python and Apple Shortcuts. 项目地址: https://gitcode.c…

2026/10/17 14:16:48 阅读更多 →
第11章 管道(Pipe)——让命令开始合作

第11章 管道(Pipe)——让命令开始合作

如果前面十章是学习"工具",那么从这一章开始,我们学习 Linux 的"思想"。很多人说:Linux 最厉害的是命令。其实不是。Linux 真正厉害的是:把很多简单命令组合起来,完成复杂任务。这一章&#xff0c…

2026/10/17 13:42:05 阅读更多 →
Betaflight配置器:让无人机飞控调参变得像玩游戏一样简单

Betaflight配置器:让无人机飞控调参变得像玩游戏一样简单

Betaflight配置器:让无人机飞控调参变得像玩游戏一样简单 【免费下载链接】betaflight-configurator Cross platform configuration and management application for the Betaflight firmware 项目地址: https://gitcode.com/gh_mirrors/be/betaflight-configurat…

2026/10/17 12:25:13 阅读更多 →

最新新闻

仿QQ注册页:从表单到身份中台的工程化实践

仿QQ注册页:从表单到身份中台的工程化实践

1. 为什么一个“仿QQ注册页”值得单独开系列?——从界面表象到工程本质的三层拆解很多人看到标题第一反应是:“不就是个注册页面吗?拖几个控件、写点CSS,半小时搞定。”我试过——在某次内部工具开发中,团队真按这个思…

2026/10/17 20:32:37 阅读更多 →
ISO/IEC 20000-1:2018中文译稿解读:条款映射与体系文件实操指南

ISO/IEC 20000-1:2018中文译稿解读:条款映射与体系文件实操指南

简介:ISO/IEC 20000-1:2018中文版PDF是信息技术服务管理体系(ITSM)领域的国际标准正式译本,由北京中大华远认证中心翻译,面向认证审核人员、IT服务管理者及体系咨询从业者,用于依据新版标准实施审核与体系建…

2026/10/17 20:32:37 阅读更多 →
ISO/IEC 42001:AI时代组织级治理的“交规”与落地指南

ISO/IEC 42001:AI时代组织级治理的“交规”与落地指南

简介:ISO/IEC DIS 42001:2022是ISO与IEC联合发布的人工智能管理系统国际标准草案,由ISO/IEC JTC 1/SC 42人工智能分技术委员会制定,旨在为组织构建可信、可靠的人工智能管理体系提供统一指南。该草案面向企业AI治理负责人、标准合规人员、风险…

2026/10/17 20:32:37 阅读更多 →
仿网易云官网轮播图:HTML+CSS+JS 从零实现无限循环与自动播放

仿网易云官网轮播图:HTML+CSS+JS 从零实现无限循环与自动播放

1. 为什么一个轮播图值得单独拿出来做轮播图这玩意儿,几乎是每个前端新手绕不过去的一道坎。你打开任何一个内容型网站,首页顶部大概率都挂着几张来回切换的大图。看起来简单,不就是几张图轮流显示嘛,但真动手写起来,坑…

2026/10/17 20:32:37 阅读更多 →
OpenHarmony上Flutter网络请求实战:环境搭建、权限配置与JSON解析

OpenHarmony上Flutter网络请求实战:环境搭建、权限配置与JSON解析

1. 这个组合意味着什么:写给刚准备入坑的你先说结论:在 OpenHarmony 上跑 Flutter 的网络请求,并不是把 Android 那套代码直接搬过来就能完事的,它比你想的多几步,但只要摸清底层逻辑,整个过程其实相当顺手…

2026/10/17 20:31:36 阅读更多 →
100+套小程序源码拆解:双端项目从跑通到二次开发实战

100+套小程序源码拆解:双端项目从跑通到二次开发实战

简介:这份资源包面向小程序开发初学者与希望补齐全栈能力的开发者,汇集100多套微信小程序完整源码,每套均含后台管理与前端展示两部分,可用于对照学习页面布局、交互逻辑与接口设计。包内共176个文件,以136个zip源码包…

2026/10/17 20:31:36 阅读更多 →

日新闻

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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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 阅读更多 →