news 2026/9/9 3:56:58

跨窗口通信设计指南:从编辑器到运行时的实时同步方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
跨窗口通信设计指南:从编辑器到运行时的实时同步方案

先说清楚:这篇文章里的“从编辑器到运行时”,不是讲编译器,也不是讲IDE插件的内部机制,而是Web前端架构里特别常见、但又特别容易被写成一坨if/else的一类问题——一个可视化编辑器窗口,要把内容或配置实时同步给另一个负责执行的运行窗口,两个窗口之间的“跨窗口通信”到底该怎么设计,才不至于在项目中期开始失控。

典型的场景你肯定不陌生:H5可视化编辑器左边是配置面板、右边是实时预览的页面;低代码平台把画布渲染区和表单设计器隔离成两个iframe;在线代码Playground的编辑区和结果展示区,天然就是两个窗口。它们之间有很清晰的边界:编辑器负责交互、组态、生成可运行的结构;运行时负责解释、渲染、接收用户操作并回调结果。边界拆开了,通信就得跟上,否则两边就是两个信息孤岛。

这篇文章适合谁看?如果你正在做在线编辑器、低代码、可视化大屏、IDE系产品,或者只是想在多层iframe、多标签页之间传数据但不想整天提心吊胆,我建议你花十分钟把这套思路过一遍。我会从“为什么必须拆成两个窗口”讲起,盘点可用的通信手段,给出一个我自己在项目里验证过的架构方案,最后把真实项目里大概率会踩的坑一并列出来。内容不绕弯子,看完能直接用。

1. 编辑器与运行时为什么必须拆开?跨窗口通信解决的到底是什么问题

1.1 同一套代码里的“边界线”:为什么要隔离

先明确两个概念,避免后面混淆。这里的“编辑器”,指的是负责交互操作的界面,比如表单设计器、页面搭建器、Markdown编辑面板;“运行时”,是实际承载用户内容、执行渲染逻辑的页面,比如预览区、沙箱环境、消费端展示页。

为什么不直接把编辑和渲染放在同一个页面?不是炫技,是四个现实问题逼着你拆:

第一,样式隔离。运行时渲染的是用户的可变内容,如果和编辑器在同一个DOM树里,一条全局样式覆盖就足以让整个编辑UI面目全非。第二,JS错误隔离。运行时里跑的代码不一定是你自己写的,一旦出现JavaScript运行时报错并冒泡到顶层,编辑器自身的逻辑也会跟着停止工作。第三,安全隔离。运行时可能需要加载第三方内容,甚至执行用户自定义脚本,放进沙箱iframe并配合sandbox属性,能极大降低风险。第四,生命周期独立。运行时可能需要刷新、重建、销毁,如果和编辑器绑在同一个页面里,编辑器的状态也会被一起拖下水。

打个比方:厨房和餐厅当然可以放在一个屋里,但油烟会乱窜。分开之后,中间需要一道传菜窗口——跨窗口通信干的就是这个传菜窗口的活。

1.2 哪些场景会用到跨窗口通信

拆个清单出来,你对照着看:

  • 在线编辑器的实时预览。CodePen、JsFiddle这类形态,编辑区源码变化,预览iframe立即更新。
  • 低代码/无代码平台。配置器和画布渲染器分离,用户在画布里拖拽组件,运行时容器负责真正渲染。
  • 可视化大屏项目。编辑器负责配置数据源和布局,运行端只做展示,两者在不同窗口,甚至可以是两个标签页。
  • 前端微应用。主应用与子应用被隔离在不同iframe里,需要互相传递路由信息或状态。
  • 多标签页协同。同一个编辑器打开多个标签页,共享同一个运行时状态,用户在一个标签页里改了内容,其他标签页要能感知。

这些场景的共同点是:通信对象之间存在明确边界,但业务上又要紧密配合。跨窗口通信就是这层“松散耦合”的纽带。

1.3 不统一设计会怎样:散装通信的四个隐患

很多项目一开始图省事,直接在业务代码里随手postMessage,结果项目到中期就会遇到四类问题。

一是消息类型全靠字符串,没有统一约束。今天写“editor.update”,明天写“Editor.Update”,大小写一错,消息静默丢失,排查起来非常痛苦。二是没人校验消息来源。任何窗口都能给你的消息事件发数据,如果你不检查event.origin,等于把系统大门打开,只要有人往你页面里塞消息你就处理。三是iframe重载后没有重连机制。用户F5了一下预览区,你的编辑器还在往旧窗口引用上发消息,全部石沉大海。四是数据格式各写各的。A模块发的payload是数组,B模块接的时候以为是个对象,出了问题互相甩锅。

这些问题累积到一定程度,就是典型的“通信架构缺陷”。与其后面填坑,不如在设计阶段就把跨窗口通信当成和业务同等级的基础模块来做。

2. 跨窗口通信的备选方案:六种方式,怎么选

2.1 先看一张对比表

浏览器提供的跨窗口通信手段,常用的有这么几种:postMessage、BroadcastChannel、SharedWorker、MessageChannel、storage事件,以及需要服务端参与的WebSocket。我把它们的核心差异整理成一张表:

通信方式是否要求同源能否跨域典型数据量复杂度适用场景
postMessage不要求可以中等到大(结构克隆)父子窗口/iframe一对一通信
BroadcastChannel要求同源不能中小同源多标签页广播
SharedWorker要求同源不能中高多标签页共享状态、消息中转
MessageChannel不要求可以(配合postMessage)建立专用一对一通信管道
storage事件要求同源不能小(约5MB限制)极低简单的跨标签页状态同步
WebSocket不要求可以跨设备、跨域实时通信

从表格能看出来,没有一个方案是万能钥匙。关键看你的业务模型是“一对一”还是“一对多”,是“同源”还是“跨域”,是“同窗口内通信”还是“跨标签页”。下面逐个拆开说。

2.2 postMessage:基石方案

postMessage是跨窗口通信的地基。它的调用方式是targetWindow.postMessage(message, targetOrigin),接收方通过window.addEventListener('message', handler)监听。

为什么它是基石?因为它是少数不要求同源的浏览器原生通信方式。父窗口和iframe之间,即使一个是a.example.com一个是b.example.com,只要你知道对方的window引用,就能发消息。

有两个关键细节必须注意。第一,targetOrigin一定要写对方的具体origin,不要图省事写*。写*虽然也能发出去,但等于向所有能拿到该window引用的窗口广播,存在安全隐患。第二,接收方一定要校验event.origin,只处理来自白名单的消息。这两点同时做到,通信链路才算是“门关上了”。

另外有个容易忽略的点:postMessage的数据走的是结构化克隆算法,能传输普通对象、数组、ArrayBuffer,但传不了函数、DOM节点、Symbol这类东西。一旦你试图传输这类值,浏览器会直接抛DataCloneError

2.3 BroadcastChannel:同源多页面的广播频道

BroadcastChannel的使用体验很像订阅一个电台频道。创建频道后,所有同源且监听同一频道名的页面都能收到消息。

// 页面A const channel = new BroadcastChannel('content-update'); channel.postMessage({ type: 'config.modified', payload: { id: 1 } }); // 页面B const channel = new BroadcastChannel('content-update'); channel.onmessage = (event) => { console.log('收到消息', event.data); };

它最大的优势是简单,多标签页广播不需要你维护一堆window引用。限制也很明确:同源才行,跨域就废了;消息是即时广播,不会持久化,如果某个标签页没开启,它错过消息就是错过了。

在编辑器场景里,我一般用它做多标签页之间的状态通知,比如“配置已保存”“组件已更新”,但不建议用它传大图或者大量base64数据,广播模式下性能衰减很快。

2.4 SharedWorker、MessageChannel、storage事件的边界

SharedWorker是浏览器在后台维护的一个共享JS线程,多个标签页可以通过它进行消息中转。它的优点是可以做消息路由和状态协调,缺点同样突出:生命周期不好管理,标签页关闭时worker不一定会立刻退出,调试时DevTools对SharedWorker的支持也不如普通Worker。除非你的业务有“消息中心”这类强需求,否则前期先不要上。

MessageChannel适合做“专属管道”。它创建一对端口port1和port2,双方各持一个端口,消息只在这两个端口之间流动,不会像postMessage那样被所有message监听器看到。实际使用时,通常先通过postMessage把其中一个端口传给对方,然后双方用MessageChannel直接通信。

storage事件是最轻量的方案。localStorage发生变化时,浏览器会触发storage事件,但有一个让人困惑的细节:触发事件的是“其他标签页”,发起修改的标签页本身不会收到事件。它只能用于同源场景,数据量也有限,适合做简单的跨标签页同步。如果你在同一个页面里监听storage事件想收到自己修改的value,那是收不到的。

2.5 我的选择:postMessage搭架子,BroadcastChannel做补充

在“编辑器↔运行时”这个具体场景里,我的经验组合是:主力通信用postMessage,因为编辑器与预览区通常是一对一关系,postMessage天然支持跨域且成本最低;当项目里出现多标签页协作、或者多个编辑器实例需要同步同一个运行时状态时,再叠加BroadcastChannel做广播通知。

SharedWorker和MessageChannel不是不能用,而是要看ROI。如果只是传内容同步消息,用SharedWorker等于杀鸡用牛刀,还要额外承担生命周期管理的复杂度。MessageChannel适合对安全性要求更高的私有通道场景,比如编辑器里要跑第三方插件、不想让无关的message监听器看到插件消息时,可以考虑。

3. 跨窗口通信架构的核心设计:协议、握手、心跳、路由

3.1 先约定消息结构,别再用裸字符串

跨窗口通信的双方是独立的代码模块,消息结构就是双方唯一认可的契约。如果这个契约不清晰,后面任何一个字段的改动都会引发连锁反应。我常用的消息基础结构长这样:

{ "protocol": "wm", "version": 1, "id": "editor_update_1720000000000_abc123", "type": "editor.update", "sourceId": "editor-main-001", "targetId": "runtime-frame-001", "timestamp": 1720000000000, "payload": {}, "refId": null, "error": null }

各个字段的用途如下表:

字段用途设计理由
protocol协议标识避免把非本协议的消息误当成业务消息处理
version协议版本后续升级协议时做兼容判断
id消息唯一ID用于请求响应配对、日志追踪、幂等去重
type消息类型业务层分发依据,必须使用常量枚举
sourceId来源窗口ID知道消息从哪个实例来
targetId目标窗口ID多实例场景下定向投递
timestamp发送时间排查消息延迟、乱序
payload消息体业务数据
refId关联请求ID响应消息关联到原请求
error错误信息请求失败时携带错误码和错误描述

id的生成策略建议用类型_时间戳_随机数组合。只用一个时间戳在并发场景下会重复,只用一个随机数又不利于日志里快速识别消息类型,组合起来最稳妥。

有了这个结构,双方只需要约定type的枚举值集合,就完成了“消息协议”的骨架。

3.2 握手和ready机制:解决首包消息丢失

postMessage一个很隐蔽的坑:如果你在iframe还没加载完成时就把消息发过去,消息会直接丢。因为接收方window对象的文档环境还不存在,消息事件根本没有地方派发。

解决方法是握手机制。运行时就绪后,主动向外发一条runtime.ready类型的消息,编辑器收到后回一条handshake_ack。双方都把“通道就绪”的标志置为true。发送方在未就绪时,不应该直接发消息,而是把消息塞进pending队列,等握手完成后再按顺序flush出去。

这里多说一句为什么不能只依赖iframe的load事件。load事件只能覆盖首次加载,一旦你改了iframe的src或者运行时内部执行了局部刷新,load事件不一定再触发;而且跨域iframe的load事件在部分场景下还会受浏览器安全策略影响。协议层的握手更通用,它不关心window是何时加载的,只关心“通信通道是否可用”这个业务事实。

3.3 心跳机制:如何感知对端还活着

跨窗口通信还有一个常见场景:运行时iframe被浏览器回收了,或者用户直接改了地址栏导航走了,但编辑器这边还傻傻地持有旧window引用,继续发消息还以为发成功了。

心跳机制是低成本解决方案。编辑器侧定时发送heartbeat消息,运行时收到后回heartbeat_ack。如果连续N次没收到ack,就判定通道断开,标记通信状态为不可用,再根据业务策略决定是否重新初始化通道。

参数上我的建议是:心跳间隔5到10秒,重试次数3次左右。间隔太长会导致断线检测时效性差,太短又会产生大量无效消息。心跳包里只放一个时间戳就够了,别塞业务数据,越轻越好。

3.4 消息路由:广播、单播、定向,别混为一谈

两个窗口一对一通信不需要路由,直接postMessage过去就行。但项目一旦变成“一个编辑器对应多个iframe运行时”或者“多个编辑器对应一个运行时容器”,消息路由问题就来了。

我的做法是,每个窗口实例在初始化时生成一个全局唯一的sourceId,并在握手消息里带给对端。后续发送消息时,根据业务需要决定走单播、定向还是广播:

  • 单播:一对一,直接调用targetWindow.postMessage。
  • 定向:消息头带targetId,接收方比对targetId和自身的sourceId,不一致就忽略。
  • 广播:有多个同源接收方时,用BroadcastChannel比用postMessage逐个发要省事得多。

另外建议加一个幂等去重逻辑。前端通信不像HTTP请求有完备的重试语义,消息重复发送在弱网或者重连时很容易出现。接收方拿到消息后,如果发现相同id的消息已经处理过,直接跳过。对于“设置全量配置”这类操作,幂等性天然成立;但对于“累加计数”这类操作,就要靠业务层自己做去重。

4. 代码落地:从一页postMessage到一套可复用的Messenger

4.1 最简实现:父窗口与iframe之间如何互发消息

先看一个最基础的例子。编辑器页面嵌入一个预览iframe,编辑器向iframe发消息,iframe收到后回执。

// 编辑器窗口 const runtimeWindow = document.getElementById('runtime-frame').contentWindow; window.addEventListener('message', function (event) { // 校验消息来源,这是安全底线 if (event.origin !== 'https://runtime.example.com') return; const data = event.data; if (data.type === 'runtime.ready') { console.log('运行时已就绪'); } }); // 向运行时发送消息 setTimeout(() => { runtimeWindow.postMessage( { type: 'editor.update', payload: { html: '<h1>hello</h1>' } }, 'https://runtime.example.com' ); }, 1000);
// 运行时窗口 window.addEventListener('message', function (event) { if (event.origin !== 'https://editor.example.com') return; const data = event.data; if (data.type === 'editor.update') { document.getElementById('container').innerHTML = data.payload.html; event.source.postMessage( { type: 'runtime.ready' }, event.origin ); } });

这个demo能跑通,但只适合教学。真实项目里,你很快就会发现需要处理握手、缓存、销毁、错误回调,于是就有了封装的动力。

4.2 封装一个轻量Messenger:带握手、缓存和消息分发

下面这段是我在项目里使用的一个简化版封装,核心思路是:统一消息出口、自动握手、消息缓存、订阅分发。

// window-messenger.js const TYPE = { HANDSHAKE: 'handshake', HANDSHAKE_ACK: 'handshake_ack', HEARTBEAT: 'heartbeat', HEARTBEAT_ACK: 'heartbeat_ack' }; function createId(type) { return `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } function createMessenger({ targetWindow, targetOrigin, sourceId }) { const listeners = new Map(); const pendingQueue = []; let ready = false; let heartbeatTimer = null; function post(type, payload, targetId) { const message = { protocol: 'wm', version: 1, id: createId(type), sourceId, targetId: targetId || '', type, timestamp: Date.now(), payload, refId: null, error: null }; targetWindow.postMessage(message, targetOrigin); } function flush() { while (pendingQueue.length) { const item = pendingQueue.shift(); post(item.type, item.payload, item.targetId); } } function handleMessage(event) { // 生产环境这里必须校验 event.origin const msg = event.data; if (!msg || msg.protocol !== 'wm') return; if (msg.type === TYPE.HANDSHAKE) { ready = true; post(TYPE.HANDSHAKE_ACK, { result: 'ok' }); flush(); return; } if (msg.type === TYPE.HANDSHAKE_ACK) { ready = true; flush(); return; } if (msg.type === TYPE.HEARTBEAT) { post(TYPE.HEARTBEAT_ACK, { ts: Date.now() }); return; } if (listeners.has(msg.type)) { listeners.get(msg.type).forEach((fn) => fn(msg.payload, msg)); } } window.addEventListener('message', handleMessage); return { send(type, payload, targetId) { if (!ready) { pendingQueue.push({ type, payload, targetId }); return; } post(type, payload, targetId); }, on(type, fn) { if (!listeners.has(type)) listeners.set(type, []); listeners.get(type).push(fn); }, startHeartbeat(interval) { heartbeatTimer = setInterval(() => { post(TYPE.HEARTBEAT, { ts: Date.now() }); }, interval); }, destroy() { clearInterval(heartbeatTimer); window.removeEventListener('message', handleMessage); listeners.clear(); } }; } export { createMessenger };

这段代码的逻辑是:send方法在通道未就绪时先把消息放进pendingQueue,握手完成后统一flush,这样就覆盖了“首包消息丢失”问题;on方法负责订阅业务消息;startHeartbeat开启心跳检测;destroy在组件卸载时清除监听器,避免内存泄漏。

你要根据自己的项目补全的,主要是三块:来源白名单校验、请求响应配对、断线自动重连。来源校验上文提过,白名单数组必须在构造Messenger时传入。请求响应配对,是在消息结构里用refId把“请求”和“响应”关联起来,配合Promise实现异步调用。断线自动重连,则是监听心跳失败后重新发握手,并把ready置为false,让后续消息重新走pendingQueue。

4.3 在编辑器项目里接入:实时把配置同步到运行时

假设你有一个低代码编辑页,用户每拖拽一个组件,需要把最新的组件树配置同步到预览iframe。接入方式如下:

// 编辑器入口文件 import { createMessenger } from './window-messenger'; const messenger = createMessenger({ targetWindow: document.getElementById('preview-frame').contentWindow, targetOrigin: 'https://preview.example.com', sourceId: 'editor-main' }); // 预览 iframe 加载完成后,运行时侧会发送 handshake messenger.on('config.update', (payload, msg) => { console.log('运行时确认收到配置', msg.id, payload.version); }); // 组件树变化时,向运行时发送最新配置 function handleComponentChange(componentTree) { messenger.send('config.update', { version: Date.now(), tree: componentTree }); } // 开启心跳,5秒一次 messenger.startHeartbeat(5000); // 页面卸载时清理 window.addEventListener('beforeunload', () => { messenger.destroy(); });

运行时侧对应的代码:

// 运行时入口 import { createMessenger } from './window-messenger'; const messenger = createMessenger({ targetWindow: window.parent, targetOrigin: 'https://editor.example.com', sourceId: 'runtime-frame' }); // 初始化完成后主动握手 messenger.send('handshake'); messenger.on('config.update', (payload) => { renderComponentTree(payload.tree); // 回执不是必须的,但可以用来做日志追踪 });

到这里,一套“从编辑器到运行时”的通信链路已经跑通了。剩下的问题,基本都是开发过程中踩坑踩出来的。

4.4 使用时的注意事项

几个容易出事的点,提前说:

一是targetOrigin不要用变量并且允许为*。在实际项目中,我见过把targetOrigin配置成空字符串或*来“省事”的写法,一旦线上预览域名被劫持或者有其他脚本,消息就裸奔了。二是消息监听器一定要在组件卸载时销毁。React或者Vue的单页应用里,组件频繁挂载卸载,如果destroy没有调用,message监听器会越来越多,内存直接往上走。三是大体积数据别直接postMessage。比如截图base64、几MB的JSON,直接传会导致页面明显卡顿,正确做法是先用IndexedDB存储,再发送一个“数据已就绪,请去取”的通知消息,接收方自行读取。

5. 真实项目排障实录:跨窗口通信常见问题与定位技巧

5.1 问题速查表

现象可能原因排查思路解决方案
首条消息收不到对方窗口未加载完成,消息发出去没接收环境在双方入口各打一条日志,确认是否完成握手加握手机制和pendingQueue缓存
明明发了消息,对方没反应类型字符串不一致,或者消息没通过校验检查消息type是否为同一枚举值,检查origin校验是否拦截消息类型统一用常量枚举
iframe重载后通道失效编辑器还在往旧window引用发消息观察控制台有没有跨域报错,检查window引用是否变化监听运行时侧发送的handshake,重新建立连接
跨域页面收不到消息targetOrigin写错,或origin校验不匹配在消息监听器里先console.log(event.origin)确认实际值统一维护origin白名单配置
同源多标签页广播无效BroadcastChannel的channel name不一致,或跨源页面使用确认两个页面的origin是否一致,channel name是否相同同源再使用,频道名抽成常量
传大对象后页面卡死postMessage携带的payload过大用Performance面板看消息发送耗时分片传输,或用IndexedDB中转
页面内存只涨不降消息监听器没销毁,或重复注册同一回调DevTools的Event Listeners面板查看监听器数量destroy时removeEventListener并清空Map

5.2 我踩过的三个坑

第一个坑,预览iframe的targetOrigin写了*。当时是为了本地调试方便,结果某个浏览器扩展一直向页面注入消息,消息落到配置面板后整个画布状态被反复重置。排查了半天才定位到是消息来源没校验。从那以后,我所有跨窗口通信代码里都强制要求白名单,本地调试阶段的白名单单独配置,上线前必须替换成正式域名。

第二个坑,没有握手机制。低代码项目里用户操作很快,从配置面板拖拽一个组件瞬间切到预览tab,结果预览收到的还是旧配置,等切回来刷新预览才正常。后来我分析了一下,原因是预览iframe第一次加载还没完成时,用户已经发了多条配置更新消息,这些消息全部丢失。加了握手和pendingQueue之后,这个问题彻底消失。

第三个坑,用BroadcastChannel传了几MB的base64图片。当时想着两个标签页要共享同一份设计稿,直接把截图base64广播出去,结果页面明显卡顿,另一个标签页收到后还出现了内存暴涨。后来改成广播一条“设计稿已导出”的消息,接收方再从IndexedDB读取数据,问题解决。跨窗口通信里传输大数据,正确姿势永远是“通知+拉取”,而不是硬塞。

5.3 调试技巧:让通信过程“肉眼可见”

跨窗口通信的调试比普通接口调试更麻烦,因为你看不到中间传输过程。我的做法是在封装的send方向统一打日志,标记为[out],在handleMessage入口打接收日志,标记为[in]。日志里带上type、id、payload的大小和关键内容。

这样在控制台里,你就能看到一条清晰的消息轨迹:“编辑器发出editor.update → 运行时收到editor.update → 运行时发出runtime.ready → 编辑器收到runtime.ready”。哪一步断了,立刻就能定位。

另外,Chrome DevTools的Event Listeners面板里有全部message监听器列表,判断是否有监听器泄漏,直接看这里就行。如果你怀疑消息频率太高导致页面卡顿,可以在收发两端各加一个计数器,每秒钟统计消息条数,超过阈值就报警。

最后再分享一个值得养成的习惯:跨窗口通信层的消息类型定义,尽量单独抽成一个常量文件,不要散落在业务代码里。我在实际项目中就是把type枚举、protocol常量、origin白名单放在同一个模块里管理。新增一个跨窗口功能时,业务侧只需要加一个type、注册一个监听器,底层传输代码完全不用动。这个习惯帮我省下的排查时间,真的不是一星半点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 3:54:42

如何让AI生成测试用例不重复:从等价类到Embedding相似度的去重实践

不得不说&#xff0c;第一次把 AI 接进测试用例生成的时候&#xff0c;我是有点兴奋的。输入一段需求描述&#xff0c;几十条用例唰一下就出来了&#xff0c;格式工整、步骤齐全&#xff0c;看起来很专业。但等我把这批用例交给测试组评审&#xff0c;人家看完第一页就皱眉了&a…

作者头像 李华
网站建设 2026/9/9 3:54:16

从马尾辫的底层逻辑到实操避坑:皮筋、高度、脸型适配全解析

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

作者头像 李华
网站建设 2026/9/9 3:53:44

2026届嵌入式校招全攻略:核心技能图谱与面试实战指南

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

作者头像 李华
网站建设 2026/9/9 3:53:37

深入理解IO多路转接:select与poll底层原理、实现细节和避坑指南

如果你用原生 socket 写过服务器&#xff0c;多半见过这个场景&#xff1a;服务器 accept 了一个客户端之后&#xff0c;如果没有额外写并发逻辑&#xff0c;它就会一直阻塞在 read/recv 那里干等数据&#xff0c;第二个客户端想连进来也只能排在后面。这是阻塞 IO 最直接的…

作者头像 李华
网站建设 2026/9/9 3:53:10

2026网站建设公司排行:排核验内容维护、询盘承接与三年服务成本

摘要&#xff1a;网站建设公司排行的决策重点不在于找到一个名称靠前的工具或公司&#xff0c;而在于确认栏目管理、产品参数、案例更新、表单通知、搜索基础设置和账号归属能否由真实人员持续完成。CNNIC第54次报告显示&#xff0c;截至2024年6月&#xff0c;中国网民规模为10…

作者头像 李华
网站建设 2026/9/9 3:52:51

TMS32F28P550嵌入式调试实战:时钟树、CLA与PWM协同排障指南

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

作者头像 李华