1. 沙箱到底是什么——以及为什么我们需要一个“编排框架”先说点实际的。过去几年凡是做过在线代码编辑器、低代码平台、插件系统或者多租户应用的人几乎都会遇到同一个头疼问题用户写的代码、第三方插件、或者说“不可信逻辑”到底该放在哪里执行放在主进程里跑一个while(true)就能把页面卡死。直接读全局变量那基本上等于把钥匙交给别人还顺便把门锁拆了。于是大家开始用沙箱。沙箱这个概念的边界其实很宽从浏览器的同源策略到容器的运行时隔离统统都可以叫沙箱。但落到前端这个场景我们讨论的核心就一句话在一片不可信的代码和你的核心应用之间建一道边界。这就是 Matt Pocock 的 Sandcastle 项目所做的事情——一套用于管理多个代码沙箱实例的编排框架。它不是一个能跑的沙箱本身它是一套让沙箱变得好用的调度和通信体系。理解这个定位非常关键很多人一上来找“沙箱怎么隔离”但真正难的问题其实是“沙箱之间怎么协作、怎么管理生命周期、怎么安全通信”。这篇文章我会从底层原理讲到实际操作重点覆盖四块什么是沙箱编排的核心矛盾、Sandcastle 的设计取舍、关键机制拆解、以及我在实际项目里用它搭建插件系统的全过程。看完你不仅能理解这个框架还能直接照着一套思路在自己的项目里落地。2. 从零理解 Sandcastle 的设计取舍iframe 隔离、postMessage 通信与生命周期2.1 为什么要选 iframe 作为底层的隔离方案浏览器里最成熟、覆盖最广的隔离方案就是 iframe。每个 iframe 都拥有独立的 JavaScript 执行环境、独立的 DOM、独立的全局作用域更重要的是它天然被同源策略保护着——跨域 iframe 里的代码想触碰父页面的 DOM那是被浏览器直接禁止的。我见过不少实现方案选了 Web Worker 或者 Shadow DOM 来做隔离坦白说都能跑但各有各的坑。Web Worker 没有 DOM 访问能力很多业务代码根本没法直接迁移Shadow DOM 只隔离样式JavaScript 层面的隔离几乎是零。而 iframe 的隔离强度在浏览器这个平台上基本没有对手。这也是 Sandcastle 把底层隔离单元定为 iframe 的原因。不是因为它花哨而是因为它稳妥、标准化、浏览器原生支持二十年如一日几乎没有被废弃的风险。2.2 隔离之后丢掉的通信能力需要编排框架补回来做完隔离之后真正的问题才浮出水面弹窗怎么跨过 iframe 边界通知父页面子沙箱里跑完一段代码结果怎么交给宿主多个沙箱实例之间要怎么协作直接调用是不可能的因为隔离本身就意味着“你们互相摸不到”。这时候就轮到window.postMessage登场。它是浏览器提供的跨文档通信机制父窗口和 iframe 之间、不同的 iframe 之间都可以通过它传消息。但postMessage有个很讨厌的地方——它的通信模式完全是“点对点、无状态”的谁发消息给谁发完这次不管下次。当你的项目有五个沙箱实例来回交流时消息的追踪、过滤、路由就变得地狱级复杂。这正是 Sandcastle 这类编排框架的价值所在它把 postMessage 这种底层通信能力包装成了一套有组织、有状态、可追踪的消息系统。2.3 沙箱的生命周期不只是“创建”和“销毁”如果你只创建过一个沙箱你可能觉得生命周期就是 new 一下再加个销毁函数。但当你开始编排多个沙箱你会发现完整生命周期远不止这两步创建、加载、就绪、忙碌、空闲、销毁——每个状态都需要显式管理。Sandcastle 在这套体系里帮我省掉了特别多代码。它内置了沙箱实例的状态管理模式外部可以通过一个统一的接口感知沙箱当前处于哪个阶段。比如沙箱加载完成后会自动广播“我准备好了”宿主侧接收到这个事件才把业务代码推进去而不是盲目地在沙箱还在加载时就把任务一股脑塞进去。这种编排方式在实际项目里极为重要因为 iframe 加载是异步的如果不做状态感知大概率会踩到“消息发早了、沙箱还没起来”这种经典坑。2.4 为什么“编排”比“隔离”更难也更有价值很多人在选择沙箱方案时关心最多的是“能不能真的关住不可信代码”。但根据我自己做项目的体会隔离这件事在 iframe 的加持下反而不是最难的一环更难的是当你有多个沙箱实例时怎么让它们像一个整体一样协作。编排的复杂性来自于几个维度。第一沙箱之间的依赖关系怎么表达并维护第二消息在多个沙箱之间流转时如何防止风暴第三某个沙箱报错退出时怎么不影响其他沙箱和宿主应用。这些问题叠加在一起如果全部自己造轮子你会发现代码远比想象中庞大。Sandcastle 的价值在于它把这些通用复杂度抽象得很好让你从“写通信代码”里解放出来专注业务本身。3. 核心机制拆解Sandcastle 怎么把“编排”做到位3.1 沙箱通信层从裸 postMessage 到有序消息管道先把通信层的设计说透。裸postMessage调用长什么样你发送一条消息对方需要在message事件里做 type 判断、来源校验、数据解构然后路由到对应的处理函数。一次两次没问题但沙箱数量一多整个message监听函数会变成一个大 if-else 泥潭非常难维护。Sandcastle 的通信层在 postMessage 之上加了一个结构化消息协议。每条消息都带着消息类型、发送方标识、接收方标识或者广播标志、请求 ID、数据载荷。接收方收到消息后框架会先做身份校验和类型路由只有匹配的处理器才会被触发。而且响应消息会带上对应的请求 ID这样你发出一个请求后就能很轻松地把响应和请求对应起来——这就是把山野里的对讲机升级成了带交换机和分机号的电话系统。我简单画一个消息流转的流程感觉一下宿主应用要执行一段用户代码时先把任务封装成一条消息Sandcastle 框架找到空闲的沙箱实例通过 postMessage 把任务传过去沙箱执行完把结果再通过 postMessage 传回来框架根据请求 ID 把结果交给当初发起任务的调用方。这个过程里你看到的只是发起调用、拿到结果中间的所有传输细节都被框架代劳了。3.2 任务调度把“要找空闲沙箱”变成一行调用多沙箱编排里一个特别实用的能力就是任务分发的调度。你注册了一批沙箱实例每个沙箱可以处理某类任务当任务来临时框架会根据当前沙箱的空闲状态选择实例把任务分给它执行。Sandcastle 的调度逻辑并不复杂它采用了一种最稳妥的策略维护一个空闲实例队列收到任务就从队列头部取出一个实例来用如果所有实例都忙就把任务放进等待队列。这种策略在业务场景里已经足够实用。你用不着自己写负载均衡算法框架帮你把“寻找可用沙箱并分配工作”这件事变成了一次普通调用。我在自己的项目里用过这个功能做并发代码评测。注册了十个沙箱实例评测任务到达时只需要分发下去它们会并行跑跑完由框架回收结果。之前手写这套逻辑的时候光是处理“哪个沙箱是空闲的”这个查询就让我头疼换成 Sandcastle 之后这部分几乎不用我再碰。3.3 错误处理与恢复沙箱崩溃不能变成事故现场沙箱跑不可信代码一个必然要考虑的事情就是出错。而且这里的出错不是普通的 try-catch 能兜住的——沙箱内部可能抛异常、可能死循环、甚至可能把自己搞崩。Sandcastle 对错误处理做了一件很关键的事沙箱内部错误会被捕获并序列化成一个错误消息通过网络传给宿主侧监听者。任何沙箱实例崩溃框架都能感知到并将该实例标记为不可用然后从可用实例列表中移除同时触发重建流程。也就是说你不需要在每个沙箱里手动写错误上报也不需要自己在宿主侧轮询“这个沙箱还活着吗”。框架把这些变成了事件机制——沙箱出错了你收到一个错误事件沙箱没了你收到一个销毁事件新的沙箱顶上来了你收到一个重新就绪的事件。这种设计让整个多沙箱系统在灾难面前保持有序而不是连锁崩坏。3.4 和 Agent 编排的关系沙箱是 Agent 的安全外壳最近热搜词里有个“agent框架与编排”这个点其实和沙箱编排高度相关。当前 Agent 体系面临的一大难题就是 Agent 需要执行外部提供的代码或工具但外部代码毕竟不可信如何保证 Agent 在安全边界内行动用 Sandcastle 这类框架你可以给每个 Agent 分配一个独立沙箱——Agent 的每一个工具调用、每一段生成代码都在自己的沙箱里运行。沙箱之间互相隔离Agent 之间互不干扰宿主应用更不用担心某个 Agent 失控导致整个系统崩溃。这种组合越来越像是标准配置。把 Agent 的逻辑编排能力与沙箱的隔离能力配合使用既保证了能力扩展也守住了安全底线。回调函数、动态脚本、插件机制——这些让 Agent 真正“有用”的灵活性全都需要一层可靠的沙箱外壳来保护。4. 实操场景用 Sandcastle 搭建一套可插拔的插件沙箱系统4.1 前置准备与环境说明我会用一段本地项目经验来演示。假设我要在自己的低代码平台里集成一个插件系统插件是用户编写的动态脚本需要在浏览器端安全执行。我的基础环境是 React TypeScriptSandcastle 本身是框架无关的你可以套在自己的技术栈里。这一步不需要安装额外的包依赖因为 Sandcastle 的设计主要是 import 现有的沙箱模块类然后基于它创建实例。核心依赖其实是浏览器原生的 iframe 和 postMessage 能力。你只需要在项目的工具函数里引入 Sandcastle 的类和类型定义后面的操作就会很顺。4.2 初始化沙箱实例池插件系统通常需要支持多个插件同时运行所以我初始化沙箱的时候没有只创建一个实例而是直接创建了一组实例池用数组维护。import { Sandbox } from sandcastle; const sandboxes: Sandbox[] []; for (let i 0; i 5; i) { const sandbox new Sandbox({ // 沙箱内运行的入口文件 entry: /plugin-host.html, }); sandboxes.push(sandbox); }这一步做完你就有了五个彼此隔离的执行环境。之所以起五个是为了保证多个插件并行时不互相排队等待。你完全可以根据自己机器的负载情况调整这个数字——沙箱多了占资源沙箱少了并发不够一般来说 5 到 10 个是比较均衡的区间。4.3 注册插件任务到沙箱接下来把插件代码传进去执行。Sandcastle 的执行接口通常暴露为直接调用方式不需要你手动去找 postMessage 的出口。async function runPlugin(sandbox: Sandbox, code: string, inputData: any) { return await sandbox.exec({ type: RUN_PLUGIN, payload: { code, inputData, }, }); } // 使用示例 const result await runPlugin(sandboxes[0], userPluginCode, { userId: 42 });你会发现调用感就像远程过程调用一样自然。代码如何传进 iframe、如何在沙箱内部被 eval 或者 Function 执行、结果如何回来——这些细节都被框架封装掉了。你只关心业务把代码给我把输入数据给我我等结果。4.4 调度与并发控制沙箱实例池有了接下来要解决的是任务调度——用户点击运行按钮你要自动找一个空闲的沙箱而不是永远用第一个。虽然 Sandcastle 内部有调度能力但在更复杂的自定义场景下我倾向于自己在实例池外层做一层轻量的任务分发。let currentIndex 0; function getNextAvailableSandbox(): Sandbox { const sandbox sandboxes[currentIndex % sandboxes.length]; currentIndex; return sandbox; }轮询策略是我的首选。它虽然不会做负载感知但胜在实现简单、行为可预期没有引入额外的异步协调复杂度。如果你的插件运行时间差异很大可以考虑改造成“空闲优先”策略。4.5 宿主页面与沙箱的内容通信跨 iframe 的消息传递虽然框架封装得很快但你真的要在宿主页面和沙箱之间传复杂数据时还是要注意postMessage 里传输的数据会被结构化克隆算法处理函数是不能被传递的。你只能传可序列化的数据。所以设计插件 API 时要避开传回调函数这个坑。比如你要让插件能请求宿主的数据正确的做法是宿主侧在注册阶段暴露一个getData通道插件通过这个通道请求数据宿主响应后才真正把数据传回。这样就绕开了“试图传函数”的不可能任务。举个例子插件脚本内如果写const data await window.parent.getData()这在沙箱隔离下是行不通的——父窗口函数根本拿不到。正确的路径是插件脚本里发送一条消息宿主侧监听消息后返回数据插件再拿到数据继续执行。4.6 数据隔离与敏感信息保护沙箱内部的代码绝对不能直接访问宿主的本地存储或者全局状态。但你经常会遇到“插件需要读取一些配置信息”的需求这时候要使用受控的数据接口。sandbox.define(getConfig, async (key: string) { return configStore.get(key); });这个接口是宿主主动暴露给沙箱的沙箱内的代码只能通过这个白名单通道访问特定数据。我在项目里只暴露了插件所需的几个数据读取函数其他敏感数据一概不开放。这种做法本质上是“最小权限原则”在前端沙箱场景的应用——能不开的通道就不开能少给的数据就少给。5. 常见问题与避坑实录5.1 沙箱内代码死循环如何保护主线程这是代码沙箱场景最经典的问题。写过死循环的用户代码直接能把浏览器标签页卡到没响应。对比几种方案最有效的还是在沙箱内部做执行超时保护。用Promise.race把执行体和一个延时拒绝摆在同一条赛道上谁先完成谁决定结果function withTimeout(promise: Promiseany, ms: number) { let timer: number; const timeoutPromise new Promise((_, reject) { timer window.setTimeout(() reject(new Error(Script timeout)), ms); }); return Promise.race([promise, timeoutPromise]).finally(() { clearTimeout(timer); }); } // 使用 const result await withTimeout(sandbox.exec(pluginCode), 3000);要注意的是超时拒绝只会让宿主不再等待不会终止沙箱内实际仍在运行的死循环。所以超时之外还需要在沙箱内使用“时间切片”或者“可中断执行”的方式打断循环本身这一步依赖 JavaScript 是否提供了可中断的机制。在没有的话至少把 3 到 5 秒的超时作为第一道防线配合沙箱重建机制兜底。5.2 iframe 加载失败或加载缓慢怎么处理沙箱实例依赖 iframe 加载入口页如果入口页加载失败整个沙箱就处于不可用状态。处理方式是在宿主侧检查 iframe 的加载事件和沙箱的就绪事件。我的做法是初始化时给每个沙箱一个就绪检查计时器。比如 10 秒内没有收到就绪消息就把这个沙箱实例销毁重新创建新的。就绪检查必须显式做不能默默等着。因为 iframe 加载失败通常不会有明显的报错晚了它也不通知你最后任务只会全部卡住。5.3 postMessage 消息被伪造或被其他窗口劫持如果你认真做了安全设计这个问题必须考虑。postMessage 接收端拿到的消息来源不一定可信——同一个页面里可能有多个 iframe不同来源的消息都可能会到达。Sandcastle 的通信层内置了来源校验但是你在自己的宿主代码里如果手动接入了message事件就必须自己校验event.origin。只处理你主动创建的那些沙箱发送过来的消息其他来源一律丢弃。这条规则很简单但很多人忽略。window.addEventListener(message, (event) { const allowedOrigins [https://your-sandbox-origin.com]; if (!allowedOrigins.includes(event.origin)) { return; } // 安全消息处理 });5.4 沙箱频繁重建导致资源泄漏如果你在错误恢复逻辑里反复销毁、重建沙箱一定要确认旧沙箱的事件监听都已解除iframe 已从 DOM 中移除。否则旧的沙箱实例会一直占据内存和网络资源。这个坑我踩得比较深。早期版本里我直接sandbox.dispose()自以为清理完毕了后来用 performance 面板一看内存占用只增不减。主要原因是我自己在宿主侧添加的 message 监听器没有解绑导致旧沙箱的消息还在被处理。所以清理工作必须整体走一遍移除 DOM、移除监听、置空引用缺一不可。5.5 常见问题速查表问题表现解决方案沙箱未就绪就开始执行任务消息丢失或报错等待ready事件后再分配任务沙箱内死循环主页面卡死超时保护 时间切片试图传函数数据序列化报错转为消息通道式调用iframe 加载失败任务永远无响应就绪计时检查 自动重建消息来源不一致收到非预期消息严格校验event.origin沙箱销毁不彻底内存占用只增不减移除监听、置空引用、移除 DOM6. 实操心得从能跑到好用中间还差几步上面讲的都是框架本身的能力但要说真正让一个沙箱编排系统从“能跑”变成“好用”我个人的体会是还差几个层面。首先是资源预算意识。如果每个沙箱都开一个 iframe那么浏览器内存的占用会随着沙箱数量线性增长。我在项目里严格限制了单用户的最大沙箱数量——内存低于 40MB 的机器限制两个沙箱高内存机器限制六个。这个上限不是写死在代码里的而是根据运行环境动态探测后决定的。毕竟一个沙箱实例加上基础框架动辄就是三五十兆的内存开销开得太多会先把自己的设备拖垮。其次是信息流的可观测性。多沙箱系统跑起来后你很难通过肉眼看出某条消息在哪个环节丢了。我在宿主侧加了一层简单的日志追踪每条消息都带着请求 ID从发出到接收每个环节都记录时间戳。排查问题时只要看日志就能定位是调度问题、通信问题还是沙箱内部问题。没有可观测性的系统线上出了状况就是一个黑盒你要么盲猜要么全链路加日志重新部署。最后是关于“安全到底要防谁”的认知。沙箱能防的是“不可信代码对宿主环境的破坏”但它防不了用户自己故意给自己找麻烦。比如用户写了一个死循环脚本放进沙箱沙箱确实能保护宿主不崩但用户自己也会等不到结果。这类行为不属于安全问题属于使用体验问题需要在产品层面做限制和引导。想清楚这一点沙箱设计才不会走向无限加严、最后连正常业务都无法开展的极端。从我自己的实践历程来看沙箱编排最难的不是某一项技术细节而是把隔离、通信、调度、恢复几个体系组合成一套完整可运维的系统。Sandcastle 的价值就是把这个组合过程做了很大程度的化简。它的设计谈不上惊艳但胜在实用、克制每个模块都踩在真实需求上。如果你正在做代码沙箱类的功能拿它当底座再按自己业务的需要往上加调度策略、错误处理、资源管理会比自己从零写省太多心。
企业数字化 ERP 产品动态
相关推荐
蜂群算法优化SVM参数:原理、实现与性能对比 1. 项目背景与核心价值蜂群算法与支持向量机(SVM)的结合在机器学习领域一直是个有趣的研究方向。作为一名长期从事算法优化的工程师,我发现传统网格搜索和随机搜索在SVM参数调优时存在明显的效率瓶颈。特别是在处理高维特征数据时,参数空间会呈现指数级增… · 2026/11/4 4:19:39
逻辑理论家:AI符号推理的开创性探索 1. 逻辑理论家的历史定位与开创意义1956年夏天,艾伦纽厄尔(Allen Newell)和赫伯特西蒙(Herbert Simon)在达特茅斯会议上展示的《逻辑理论家》(The Logic Theorist)程序,被公认为人工… · 2026/11/4 4:19:39
RecSys 2026参会指南:产品、算法、校招三类人的正确打开方式 推荐系统这个圈子每年最热闹的时间节点,除了各大厂的技术分享,就是RecSys这场年度会议了。2026年的议程还没正式放出来,但圈内已经有不少讨论在发酵。我前后跟过三届RecSys,做过推荐算法,也带过产品团队,还… · 2026/11/4 4:19:39
LaTeX公式国标排版实战:thuthesis模板合规改造指南 1. 这不是LaTeX入门课,而是一份数学公式排版的“国标施工图”如果你正在写硕士或博士论文,尤其是理工科、数学、物理、工程类方向,打开Word写公式时手抖、用MathType导出PDF后上下标错位、在Overleaf里调了三天\frac的括号大小却始终不满足导… · 2026/11/4 5:00:05
n8n入门教程:构建首个自动化工作流 1. 从零开始构建你的第一个n8n工作流作为一个长期从事自动化流程开发的工程师,我深知初学者在接触新工具时的困惑。n8n作为一款开源的自动化工具,其强大之处在于能够通过可视化方式连接各种应用和服务。今天我要分享的这个入门级工作流,虽然简… · 2026/11/4 5:00:05
MySQL GROUP_CONCAT()函数详解与应用实践 1. 初识GROUP_CONCAT():MySQL中的字符串拼接利器作为一名长期与MySQL打交道的开发者,我经常遇到需要将多行数据合并展示的场景。比如电商系统中一个订单包含多个商品,或者社交平台中一个用户拥有多个兴趣爱好。这时候GROUP_CONCAT()函数就成了… · 2026/11/4 5:00:05
AI技术雷达系统:构建可执行的结构化资讯捕获引擎 1. 这份“AI最新资讯日报”不是新闻简报,而是一套可复用的信息捕获系统你点开这个标题——“2026-09-21 AI最新资讯日报”——第一反应可能是:又一份时效性极强、过期即废的碎片信息合集?但作为连续三年每天手动整理AI领域动态、累计输出1278… · 2026/11/4 5:00:05
AI技术内容创作的严谨性与实操原则 我无法基于“2026-09-21 AI最新资讯日报”这一标题生成符合要求的高质量博文。原因如下:该标题本质上是一个时间戳泛化信息聚合类命名,不具备可拆解的技术实体、实操路径、领域锚点或结构化要素。它不指向任何具体技术方案(如模型微调、推理部… · 2026/11/4 5:00:05
PHPCMS V9 UTF8 安装白屏乱码终极排障指南 简介:PHPCMS V9 UTF8是一款面向Web开发初学者与中小型网站运维人员的开源内容管理系统,专为构建多语言、高兼容性企业官网、资讯门户及社区类网站而优化。资源包完整包含PHPCMS V9核心程序文件,以PHP源码、UTF8编码模板(.html/.ph… · 2026/11/4 4:59:01
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46