[{"data":1,"prerenderedAt":83},["ShallowReactive",2],{"case-dev-build-log-ws-mock-dsl-zh":3,"blog-list-zh":13},{"slug":4,"title":5,"summary":6,"date":7,"featured":8,"seoDescription":9,"series":10,"seriesOrder":11,"html":12},"dev-build-log-ws-mock-dsl","为什么我们重新设计了 WebSocket Mock DSL，而不是用 YAML、JSON 或 JS","轻量 WebSocket Mock 不该比 HTTP Mock 更重。YAML、JSON、JS 都能做，但写「连上、登录、心跳」太啰嗦。我们自研短 DSL，就是让轻量 Mock 更轻。","2026-08-13",false,"DevPeek 开发实录：为什么 WebSocket Mock 自研 Flow DSL。现成 YAML、JSON、JS 能做重活；轻量顺序剧本用更短的文本来写。","dev-build-log",3,"\u003Ch2>轻量 Mock，不该先变重\u003C/h2>\n\u003Cp>HTTP 联调里，\u003Ca href=\"/docs/mock/\">Mock\u003C/a> 很轻：抓一帧，勾特征，改响应。一来一回就结束。\u003C/p>\n\u003Cp>WebSocket 的\u003Cstrong>轻量\u003C/strong>场景其实也不长：连上、登录、回一句、隔几秒推个心跳。要 Mock 的是一小段对话，不是一篇程序。\u003C/p>\n\u003Cp>现成方案却常把这段对话塞进 JSON 规则表、YAML 配置，或一段 JS。\u003Cstrong>功能很强，也能跑。\u003C/strong> 问题是：本来十来行能说完的事，要先填字段、堆括号、写 \u003Ccode>setInterval\u003C/code>。轻量 Mock 先被容器写重了。\u003C/p>\n\u003Cp>我们重新设计了一套 DSL，图的就是这件事——\u003Cstrong>让轻量 Mock 更轻\u003C/strong>。界面叫 \u003Cstrong>WS Flow\u003C/strong>，文件扩展名是 \u003Ccode>.dpws\u003C/code>。人写对话，工具链再把它编译成运行时需要的 JSON 和状态结构。\u003C/p>\n\u003Ch2>HTTP 规则表为什么不能直接套过来\u003C/h2>\n\u003Cp>最初我们也想复用 HTTP Mock：给每条 WebSocket 消息做一条「消息条件 + 返回内容」规则。很快就发现，两者看起来都在收发数据，交互模型却不同。\u003C/p>\n\u003Cp>HTTP 是一次请求对应一次响应。规则命中后返回内容，这次交互就结束了。WebSocket 则是一条连接里连续发生很多轮：\u003C/p>\n\u003Cul>\n\u003Cli>握手完成后，服务端可能不等客户端发消息就先推 \u003Ccode>challenge\u003C/code>\u003C/li>\n\u003Cli>客户端登录成功后，后续消息才进入「已登录」阶段\u003C/li>\n\u003Cli>一个请求可能先回 ACK，再连续推送多帧结果\u003C/li>\n\u003Cli>heartbeat、行情等消息由定时器主动产生\u003C/li>\n\u003Cli>logout 或断线后，之前启动的周期推送必须停止\u003C/li>\n\u003C/ul>\n\u003Cp>如果仍用平铺规则表，就得额外增加 \u003Ccode>onOpen\u003C/code>、\u003Ccode>after\u003C/code>、\u003Ccode>nextState\u003C/code>、\u003Ccode>timerId\u003C/code>、\u003Ccode>cancelWhen\u003C/code> 等字段。字段补得越多，越接近把一个状态机拆散后塞进表格。\u003C/p>\n\u003Cp>所以 WS Flow 没有沿用 HTTP Mock 的形状。HTTP Mock 继续从抓包和 GUI 出发；WebSocket Mock 直接描述「连接中的多轮消息树」。两者的分工也写进了 \u003Ca href=\"/docs/wsmock-dsl/\">WebSocket Mock 文档\u003C/a>。\u003C/p>\n\u003Ch2>先定三个约束\u003C/h2>\n\u003Cp>写语法之前，我们先给它划了边界。\u003C/p>\n\u003Cp>第一，\u003Cstrong>读起来要像对话\u003C/strong>。条件写在上一层，返回缩进在下一层；下一轮消息继续往下缩进。即使没读过语言参考，也应该大致看得出先后关系。\u003C/p>\n\u003Cp>第二，\u003Cstrong>服务端主动行为必须是一等公民\u003C/strong>。连接建立、定时推送、延迟发送和主动断开不能藏在插件或回调里，否则最常见的长连接场景反而最难写。\u003C/p>\n\u003Cp>第三，\u003Cstrong>它不能长成另一门 JavaScript\u003C/strong>。不提供 \u003Ccode>if\u003C/code>、\u003Ccode>for\u003C/code>、函数和任意表达式。复杂逻辑一旦需要真正的编程能力，就交给 JS；DSL 只把短会话写短。\u003C/p>\n\u003Cp>这三个约束也决定了语法的主体：缩进负责结构，少量符号负责匹配、生命周期和发送。\u003C/p>\n\u003Ch2>同一段对话，JSON 与 Flow 差在哪\u003C/h2>\n\u003Cp>以「连上发 challenge → 登录成功 → 每 15 秒 tick」为例，如果把它抽象成规则 JSON，大致会写成这样：\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;url&quot;: &quot;wss://api.example.com/ws&quot;,\n  &quot;onOpen&quot;: [{ &quot;send&quot;: { &quot;type&quot;: &quot;challenge&quot; } }],\n  &quot;handlers&quot;: [\n    {\n      &quot;match&quot;: { &quot;type&quot;: &quot;login&quot; },\n      &quot;reply&quot;: { &quot;type&quot;: &quot;loginSuccess&quot; },\n      &quot;then&quot;: {\n        &quot;every&quot;: &quot;15s&quot;,\n        &quot;send&quot;: { &quot;type&quot;: &quot;tick&quot; }\n      }\n    }\n  ]\n}\n\u003C/code>\u003C/pre>\n\u003Cp>与这段对话直接相关的业务词只有 \u003Ccode>challenge\u003C/code>、\u003Ccode>login\u003C/code>、\u003Ccode>loginSuccess\u003C/code> 和 \u003Ccode>tick\u003C/code>。其余字符大多在解释配置结构：\u003Ccode>onOpen\u003C/code>、\u003Ccode>handlers\u003C/code>、\u003Ccode>match\u003C/code>、\u003Ccode>reply\u003C/code>、\u003Ccode>then\u003C/code>。\u003C/p>\n\u003Cp>YAML 能去掉括号，但去不掉这层壳：仍然要写键、列表和嵌套对象，而且缩进同时承担「配置归属」和「对话顺序」两种含义。JS 最灵活，可一旦加上事件监听、定时器和清理逻辑，一段 Mock 很容易变成需要维护的小程序。\u003C/p>\n\u003Cp>同一段，Flow 是这样：\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-text\"># ws wss://api.example.com/ws\n# profile type\n# ping auto\n\n--@open\n    challenge\n        --type=login\n            loginSuccess\n                --@loop 15s\n                    tick\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ccode>--@open\u003C/code> 表示连接建立，先向客户端发送 \u003Ccode>challenge\u003C/code>；它下面的 \u003Ccode>--type=login\u003C/code> 等待下一帧登录消息，命中后再返回 \u003Ccode>loginSuccess\u003C/code>。单词返回会按文件头编译成 \u003Ccode>{&quot;type&quot;:&quot;loginSuccess&quot;}\u003C/code>，业务帧不必每次手写 JSON。延迟和周期推送直接写在行上（\u003Ccode>+300ms\u003C/code>、\u003Ccode>--@loop 15s\u003C/code>），不必另起控制流。\u003C/p>\n\u003Cp>这里省掉的不只是字符数，更是概念切换。写的人不必先想「我要创建一个 handler，再给它挂一个 timer」，只需按实际对话顺序往下写。\u003C/p>\n\u003Ch2>缩进不只是排版，它就是会话状态\u003C/h2>\n\u003Cp>只写固定回复还不够。真实联调里，经常要表达「登录以后才允许订阅」「退出房间后停止房间消息」。\u003C/p>\n\u003Cp>为此，Flow 用 scope 表示连接当前走到哪一段。\u003Ccode>~auth\u003C/code> 进入已登录 scope，\u003Ccode>!~auth\u003C/code> 退出它：\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-text\"># profile type\n# capture *\n\n--type=login ~auth\n    {&quot;type&quot;:&quot;loginSuccess&quot;,&quot;token&quot;:&quot;$token&quot;}\n        --@loop 5s\n            {&quot;type&quot;:&quot;heartbeat&quot;,&quot;ts&quot;:&quot;$now&quot;}\n        --type=refresh\n            {&quot;type&quot;:&quot;profile&quot;,&quot;token&quot;:&quot;$token&quot;}\n\n--type=logout !~auth\n    logoutSuccess\n\n--type=logout\n    {&quot;type&quot;:&quot;error&quot;,&quot;reason&quot;:&quot;not_logged_in&quot;}\n\u003C/code>\u003C/pre>\n\u003Cp>客户端发来登录帧后，字段会按 \u003Ccode># capture *\u003C/code> 保存为连接变量，返回里可用 \u003Ccode>$token\u003C/code> 引用。进入 \u003Ccode>auth\u003C/code> 后，5 秒一次的 heartbeat 开始运行；收到 logout 时退出 \u003Ccode>auth\u003C/code>，绑定在这个 scope 上的定时器也随之取消。\u003C/p>\n\u003Cp>最后一个普通 \u003Ccode>--type=logout\u003C/code> 是未登录时的兜底。它和 \u003Ccode>--type=logout !~auth\u003C/code> 写的是同一个业务动作，但是否处于 \u003Ccode>auth\u003C/code> scope 决定了走成功还是报错。\u003C/p>\n\u003Cp>这也是我们没有把 \u003Ccode>--@loop\u003C/code> 简单翻译成全局 \u003Ccode>setInterval\u003C/code> 的原因。定时器必须属于某段会话：进入时启动，退出时回收，否则 Mock 跑久了会不断留下幽灵推送。完整的 scope 写法见 \u003Ca href=\"/docs/dpws/scope/\">\u003Ccode>.dpws\u003C/code> Scope\u003C/a>。\u003C/p>\n\u003Ch2>用 Profile 适配协议，而不是复制一套语法\u003C/h2>\n\u003Cp>并不是所有 WebSocket 协议都用 \u003Ccode>type\u003C/code> 分发消息。有的用 \u003Ccode>event\u003C/code>、\u003Ccode>action\u003C/code>、\u003Ccode>method\u003C/code>，还有的是纯文本。\u003C/p>\n\u003Cp>Flow 把这些差异放进文件头：\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-text\"># profile event\n# dispatch event\n# format json\n# capture *\n\n--event=message\n    {&quot;event&quot;:&quot;message&quot;,&quot;text&quot;:&quot;收到：$text&quot;}\n\u003C/code>\u003C/pre>\n\u003Cp>在 \u003Ccode>event\u003C/code> Profile 下，单词返回 \u003Ccode>welcome\u003C/code> 会编译为 \u003Ccode>{&quot;event&quot;:&quot;welcome&quot;}\u003C/code>；换成默认的 \u003Ccode>type\u003C/code> Profile，则是 \u003Ccode>{&quot;type&quot;:&quot;welcome&quot;}\u003C/code>。JSON-RPC 可以按 \u003Ccode>method\u003C/code> 匹配，纯文本协议可以用 \u003Ccode>--=...\u003C/code> 匹配整帧。\u003C/p>\n\u003Cp>Profile 只提供默认约定，不改变 Flow 的结构。这样不需要为每种业务协议发明新的关键字，也不会把协议字段硬编码进解析器。文件头的完整选项见 \u003Ca href=\"/docs/dpws/headers/\">\u003Ccode>.dpws\u003C/code> 文件头\u003C/a>。\u003C/p>\n\u003Ch2>少量符号，各自只做一件事\u003C/h2>\n\u003Cp>我们刻意控制了符号预算：\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>--field=value\u003C/code>：匹配客户端消息；嵌套条件表示 AND\u003C/li>\n\u003Cli>\u003Ccode>--@open\u003C/code>、\u003Ccode>--@loop\u003C/code>：连接和定时等生命周期事件\u003C/li>\n\u003Cli>\u003Ccode>~name\u003C/code>、\u003Ccode>!~name\u003C/code>：进入或退出命名 scope\u003C/li>\n\u003Cli>\u003Ccode>$name\u003C/code>：引用当前连接捕获到的变量\u003C/li>\n\u003Cli>\u003Ccode>@file\u003C/code>：从文件读取较大的返回内容\u003C/li>\n\u003Cli>\u003Ccode>|\u003C/code>：对返回内容做模板、取值或字段修改\u003C/li>\n\u003Cli>\u003Ccode>+500ms\u003C/code>：延迟发送，\u003Ccode>!close\u003C/code>：发送后关闭连接\u003C/li>\n\u003C/ul>\n\u003Cp>同一条件下写多行返回，会按顺序发送，每行都能有自己的延迟。于是「立即 ACK，300ms 后推一帧进度，再过 400ms 推最终结果」不需要数组和调度代码：\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-text\"># capture id\n\n--type=agent\n    {&quot;type&quot;:&quot;ack&quot;,&quot;id&quot;:&quot;$id&quot;,&quot;ok&quot;:true}\n    @fixtures/agent-progress.json | template +300ms\n    @fixtures/agent-final.json | template +400ms\n\u003C/code>\u003C/pre>\n\u003Cp>语法看起来短，但运行语义不能含糊。例如 \u003Ccode>--type=ping\u003C/code> 匹配的是 JSON 业务消息，\u003Ccode>--@ping\u003C/code> 匹配的是 WebSocket 协议级 Ping 帧；两者不能混为一谈。详细匹配与返回规则放在 \u003Ca href=\"/docs/dpws/\">\u003Ccode>.dpws\u003C/code> 语言参考\u003C/a> 中，博客只保留能建立心智模型的部分。\u003C/p>\n\u003Ch2>自研 DSL 最贵的不是 Parser\u003C/h2>\n\u003Cp>把缩进文本解析成 AST 并不算最难。真正贵的是让失败可理解、行为可验证。\u003C/p>\n\u003Cp>如果只做一个能跑的 Parser，用户写错缩进、引用了不存在的变量，或在 Mock 接管连接后忘记处理协议 Ping，最后看到的只会是「怎么没消息」或「为什么一会儿就断线」。这类静默失败比多写几行 JSON 更糟。\u003C/p>\n\u003Cp>因此 \u003Ccode>.dpws\u003C/code> 不是直接边读边执行，而是经过 parse、validate、compile，再交给 runtime。校验会给出稳定错误码和行号，例如：\u003C/p>\n\u003Cul>\n\u003Cli>缩进结构非法\u003C/li>\n\u003Cli>未知的生命周期事件或管道\u003C/li>\n\u003Cli>\u003Ccode>$变量\u003C/code> 未定义\u003C/li>\n\u003Cli>scope 重复定义\u003C/li>\n\u003Cli>Mock 截断 upstream，却没有启用 \u003Ccode># ping auto\u003C/code> 或处理 \u003Ccode>--@ping\u003C/code>\u003C/li>\n\u003C/ul>\n\u003Cp>校验已经接进编辑器；高亮和补全还在路上。错误码的含义可在 \u003Ca href=\"/docs/dpws/errors/\">\u003Ccode>.dpws\u003C/code> 错误码\u003C/a> 查询。我们还保留了 simulate 这一层，让同一份 Flow 可以输入 open、message、tick、close 事件，再检查输出帧、scope 变化和定时器是否按预期停止。\u003C/p>\n\u003Ch2>轻的用 DSL，重的仍用 JS\u003C/h2>\n\u003Cp>Flow \u003Cstrong>只对准轻量 Mock\u003C/strong>：顺序的一小段会话。连接欢迎、登录、订阅、几次推送、心跳和退出，都在它的舒适区。\u003C/p>\n\u003Cp>下面这些情况则应该停下来考虑 JS 或专门的 Mock 服务：\u003C/p>\n\u003Cul>\n\u003Cli>多连接之间需要共享并实时修改状态\u003C/li>\n\u003Cli>消息会并发、乱序，分支依赖复杂时序\u003C/li>\n\u003Cli>payload 需要大量随机生成或动态计算\u003C/li>\n\u003Cli>要访问数据库、外部 API 或执行任意用户代码\u003C/li>\n\u003Cli>团队更看重 JSON Schema、既有 CI 和通用编辑器生态\u003C/li>\n\u003C/ul>\n\u003Cp>这不是 DSL 没写完，而是边界有意为之。每增加一种表达能力，都要同时增加解析、报错、调试和文档成本。若最终补齐变量赋值、条件表达式、函数、模块和异步 API，我们只是重新造了一门更陌生的脚本语言。\u003C/p>\n\u003Cp>自研当然也有账：Parser、错误提示、高亮、补全和版本兼容都要自己维护，同事也要学习一组新符号。换来的价值必须足够具体——打开文件能直接读出对话顺序，修改一条返回不用先理解一套配置 Schema，周期推送能随会话 scope 自动启停。\u003C/p>\n\u003Ch2>回到最初的问题\u003C/h2>\n\u003Cp>我们不是因为 YAML、JSON 或 JS 做不到，才设计 WS Flow。恰恰相反，它们什么都能做；问题在于轻量场景也要为这种通用性付费。\u003C/p>\n\u003Cp>\u003Ccode>.dpws\u003C/code> 选择的是另一种交换：牺牲任意编程能力，换取更短的业务表达；增加一套受控语法，换取可编译、可校验、可模拟的会话模型。\u003C/p>\n\u003Cp>判断它是否值得，不看少写了多少括号，而看联调的人能不能在几十秒内回答三个问题：\u003Cstrong>客户端发什么会命中？服务端接着回什么？这个阶段什么时候结束？\u003C/strong>\u003C/p>\n\u003Cp>如果一份 Flow 打开后就能直接回答，这套 DSL 才算达到了目标。\u003C/p>\n\u003Ch2>和前两篇是同一条线\u003C/h2>\n\u003Cp>\u003Ca href=\"/blog/dev-build-log-sqljs-to-better-sqlite3/\">抓包历史从 sql.js 迁到原生 SQLite\u003C/a>，解决的是挂久了的存储。\u003Cbr>\u003Ca href=\"/blog/dev-build-log-electron-to-tauri/\">桌面壳从 Electron 换成 Tauri\u003C/a>，解决的是壳太重。\u003Cbr>这篇是：轻量长连接 Mock 不该先被 JSON/YAML/JS 写重。我们用更短的 DSL，让它保持轻。\u003C/p>\n\u003Ch2>相关文档\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"/docs/wsmock-dsl/\">WebSocket Mock\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"/docs/dpws/\">\u003Ccode>.dpws\u003C/code> 语言参考\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"/docs/mock/\">Mock 规则\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"/docs/capture/\">抓包与过滤\u003C/a>\u003C/li>\n\u003C/ul>\n\u003Ch2>下一篇\u003C/h2>\n\u003Cp>\u003Cstrong>\u003Ca href=\"/blog/dev-build-log-silent-auto-update/\">离开 Electron 之后，静默更新为什么必须自己做\u003C/a>\u003C/strong>——换壳之后现成更新器用不上了。检查和下包放到托盘里后台做，点一下再静默覆盖。\u003C/p>\n\u003Chr>\n\u003Cp>欢迎 \u003Ca href=\"/\">下载 DevPeek\u003C/a> 看 HTTP Mock 与抓包怎么串；WS Flow 以 \u003Ca href=\"/docs/wsmock-dsl/\">WebSocket Mock 文档\u003C/a> 为准。不同看法可以到 \u003Ca href=\"https://github.com/GYPengDev/devpeek/discussions\">GitHub Discussions\u003C/a> 聊。\u003C/p>\n",{"items":14},[15,22,29,30,37,44,50,56,64,70,77],{"slug":16,"title":17,"summary":18,"date":19,"featured":8,"seoDescription":20,"series":21,"seriesOrder":11},"https-ssl-skip-decrypt","开了 SSL 抓包，有些网站反而打不开？把域名加进「跳过 SSL 代理」","系统代理和证书都没问题，一开 SSL 代理个别站却打不开。多半是源站证书和 Host 对不上。把域名加进「跳过 SSL 代理（隧道）」即可，不必关掉整站 SSL 抓包。","2026-09-01","DevPeek 开了 SSL 代理后部分网站打不开？ERR_TLS_CERT_ALTNAME_INVALID 是源站证书与 Host 不匹配，不是 CA 未信任。用 Include / Exclude：把域名加入「跳过 SSL 代理（隧道）」。","api-debug-new-tricks",{"slug":23,"title":24,"summary":25,"date":26,"featured":8,"seoDescription":27,"series":10,"seriesOrder":28},"dev-build-log-silent-auto-update","换掉 Electron 后，自动更新也得重做","DevPeek 换成 Tauri 后，更新的不只是一个窗口，而是 Launcher、Core 和 Shell 三个进程。我们把检查和下载挪到后台，等安装包准备好后，再由托盘完成最后一次重启。","2026-08-28","DevPeek 开发实录：桌面端从 Electron 换到 Tauri 后，如何为 Launcher、Core 和 Shell 重做自动更新，并避免更新过程影响系统代理。",4,{"slug":4,"title":5,"summary":6,"date":7,"featured":8,"seoDescription":9,"series":10,"seriesOrder":11},{"slug":31,"title":32,"summary":33,"date":34,"featured":8,"seoDescription":35,"series":10,"seriesOrder":36},"dev-build-log-electron-to-tauri","DevPeek 架构改造：抛弃 Electron，拥抱 Tauri","联调工具不该为「开一个窗口」再拖一整套 Chromium。我们把业务收到 Core，桌面壳换成 Tauri，安装更轻、后台更省，关窗后托盘里也能随时唤回来。","2026-08-08","DevPeek 开发实录：桌面壳从 Electron 迁到 Tauri，业务留在 Core，壳只负责窗口与系统能力，体积与资源占用更友好。",2,{"slug":38,"title":39,"summary":40,"date":41,"featured":8,"seoDescription":42,"series":10,"seriesOrder":43},"dev-build-log-sqljs-to-better-sqlite3","抓包历史为什么从 sql.js 迁到原生 SQLite","早期 DevPeek 用纯 JavaScript 版 SQLite 存抓包记录，联调一忙就占内存、列表发紧。我们换成系统级原生 SQLite 后，代理可以连续挂一整天，内存也稳。","2026-08-05","DevPeek 开发实录：抓包历史存储从 sql.js 换成原生 SQLite，解决长时间联调内存上涨、列表卡顿，让代理可以挂一整天。",1,{"slug":45,"title":46,"summary":47,"date":48,"featured":8,"seoDescription":49,"series":21,"seriesOrder":36},"mock-map-route-split-api","API 已迁新服务，前端还没发版？用「转发规则」联调","服务拆分后，前端仍请求旧 API，订单接口却已经迁到新服务。使用 DevPeek「转发规则」无需修改前端代码，即可把指定接口转发到新域名；若新服务未完成，还可以用 Mock 先跑通页面。","2026-07-26","微服务联调与 API 域名迁移：用 DevPeek「转发规则」做 API 转发，不改前端即可把旧域名请求指到新服务；服务迁移未完成时可用 Mock 先验页面。附接口联调 Demo。",{"slug":51,"title":52,"summary":53,"date":54,"featured":8,"seoDescription":55,"series":21,"seriesOrder":43},"api-param-encryption-debug","H5 接口参数加密？DevPeek 一键解密调试","联调时接口参数 AES 加密看不到明文？用 DevPeek 参数转换功能，填好密钥和 IV 就能自动解密，还能改参重放。","2026-07-18","用 DevPeek 参数转换功能解密 AES-GCM 加密的 H5 API 请求，支持双向转换、明文编辑、调试重放。",{"slug":57,"title":58,"summary":59,"date":60,"featured":61,"seoDescription":62,"series":63,"seriesOrder":36},"h5-debug-console-mock","H5 调试实战（二）：Android WebView 白屏排查，从 Console 远程定位到 Mock 验证","活动 H5 在部分 Android 设备中点击按钮后白屏，请求全部 200 却没有页面反馈。通过 DevPeek Console 获取线上 WebView 运行日志，远程 eval 确认 polyfill 覆盖问题，再使用 Mock 验证异常状态下的降级页面。","2026-07-13",true,"H5 调试实战第二篇：排查 Android WebView 白屏问题。通过 Console 远程调试定位 JavaScript polyfill 兼容问题，并结合 Mock 验证接口异常场景下的页面降级逻辑。","h5-debug",{"slug":65,"title":66,"summary":67,"date":68,"featured":61,"seoDescription":69,"series":63,"seriesOrder":43},"wechat-h5-storage-debug","H5 调试实战（一）：微信 H5 localStorage 调试，在电脑上修改真机缓存","微信 H5 换测试号后头像仍显示旧用户，抓包确认接口正常，却定位到 localStorage 遗留旧数据。本文记录一次真实联调过程，以及如何在电脑上查看、修改微信 WebView 的 localStorage、sessionStorage 和 IndexedDB。","2026-07-11","微信 H5 调试实战：通过真实案例定位 localStorage、sessionStorage、IndexedDB 导致的数据缓存问题，对比 vConsole、Remote Debug 等方案，介绍真机 WebView 调试思路。",{"slug":71,"title":72,"summary":73,"date":74,"featured":61,"seoDescription":75,"series":76,"seriesOrder":36},"why-we-built-devpeek-h5-debug","我们为什么做 DevPeek（二）：App 里那页 H5，在电脑上也能对着查","登录接口用参数转换啃下来了，活动页 H5 却在 App WebView 里才崩。真机 remote debug 折腾一圈，DOM 和抓包还是两拨窗口——于是把镜像和自研调试面板收进 DevPeek。","2026-07-10","DevPeek 起源系列第二篇：App 内嵌 H5 只在真机出问题、remote debug 与抓包割裂的联调痛点，以及调试 Tab 如何把页面镜像到电脑并用自研面板查 DOM、Console 与 Network。","origin",{"slug":78,"title":79,"summary":80,"date":81,"featured":61,"seoDescription":82,"series":76,"seriesOrder":43},"why-we-built-devpeek","我们为什么做 DevPeek（一）：HTTPS 解密了，Body 还是天书","版本日前那晚，TLS 早就解开了，改请求里一个字段却还要翻脚本加解密。于是萌生做一个把业务加解密收进代理的工具——DevPeek 从这儿开始。","2026-07-09","DevPeek 起源系列第一篇：联调时请求体加解密的手工折腾，以及为什么先做一款把业务加解密收进代理的工具。",1788746290313]