iOS中使用WKScriptMessageHandler监听WhatsApp Web消息删除事件时messageDeleted未触发的问题
看起来你已经做了不少基础排查,不过从你的描述和代码来看,问题大概率出在JavaScript的监听逻辑或者消息传递的环节上,我来一步步帮你梳理可能的原因和解决办法:
首先,先确认JavaScript里的postMessage真的执行了吗?
你说Observer在元素移除时触发,但有没有可能是extractMessageContent函数报错,导致postMessage根本没发出去?或者messageElement没找到?
先给你的JS代码加几个调试日志,然后在iOS端监听WebView的控制台输出,看看JS里到底发生了什么:
- 在JS的
mutation.removedNodes回调里,找到messageElement后立刻加console.log:
if (messageElement) { console.log("Detected deleted message, ID:", messageElement.getAttribute('data-id')); // 然后再执行postMessage和extractMessageContent }
- 在iOS端添加控制台日志监听,这样就能看到WebView里的JS日志:
// 在WebView配置里加这个 config.preferences.javaScriptEnabled = true; let logger = ConsoleMessageLogger() userContentController.add(logger, name: "console") // 自定义一个控制台消息处理器 class ConsoleMessageLogger: NSObject, WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("JS Console:", message.body) } } // 同时注入一段JS把console.log转发给iOS let consoleScript = """ (function(){ const originalLog = console.log; console.log = function(...args) { originalLog.apply(console, args); window.webkit.messageHandlers.console.postMessage(args.join(' ')); }; })(); """ userContentController.addUserScript(WKUserScript(source: consoleScript, injectionTime: .atDocumentStart, forMainFrameOnly: true))
这样你就能确认JS里到底有没有走到postMessage那一步。
检查MutationObserver的消息元素检测逻辑
你现在用node.querySelector('[data-id]')来查找被删除的消息元素,但mutation.removedNodes里的node本身可能就是带data-id的消息元素,不需要再querySelector。比如当一条消息被删除时,被移除的节点就是消息本身,而不是它的父容器。修改这段逻辑:
mutation.removedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // 先检查当前节点是否是消息元素,再找子节点 let messageElement = node.hasAttribute('data-id') ? node : node.querySelector('[data-id]'); if (messageElement) { console.log("Found deleted message element:", messageElement); // 继续执行postMessage } } });
给extractMessageContent加错误捕获
如果你的extractMessageContent函数里有报错(比如正则匹配失败、某个DOM元素找不到),会直接导致整个回调终止,postMessage根本发不出去。给它加个try-catch:
function extractMessageContent(element) { try { let messageContainer = element.querySelector('[data-pre-plain-text]'); let messageText = element.querySelector('.selectable-text'); let mediaContainer = element.querySelector('.media-container'); let timestampElement = element.querySelector('div[data-pre-plain-text]'); let timestamp = ''; let sender = ''; let mediaUrl = ''; let mediaName = ''; if (timestampElement) { let preText = timestampElement.getAttribute('data-pre-plain-text'); if (preText) { // 修正正则的转义问题 let matches = preText.match(/\[(.*?)\].*?([^:]+):/); if (matches) { timestamp = matches[1]; sender = matches[2].trim(); } } } if (mediaContainer) { let mediaElement = mediaContainer.querySelector('img, video'); if (mediaElement) { mediaUrl = mediaElement.src; mediaName = mediaElement.alt || mediaElement.src.split('/').pop(); } } return { id: element.getAttribute('data-id'), text: messageText ? messageText.innerText : '', sender: sender, timestamp: timestamp, hasMedia: !!mediaContainer, mediaUrl: mediaUrl, mediaName: mediaName }; } catch (err) { console.error("Error extracting message content:", err); // 即使出错,也要返回基础数据,保证postMessage能发出去 return { id: element.getAttribute('data-id'), error: err.message }; } }
检查iOS端的消息解析是否失败
你现在的代码里,如果message.body无法转成[String: Any],会直接return,不会有任何日志。先把这段的日志加上,看看是不是解析失败了:
@objc func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("Received raw message:", message.body, "Type:", type(of: message.body)) guard let body = message.body as? [String: Any], let type = body["type"] as? String, let data = body["data"] as? [String: Any] else { print("Failed to parse message body, raw content:", message.body) return } // 原来的switch逻辑 }
有时候WKWebView会把复杂的JS对象转成NSString(比如存在循环引用或者无法序列化的类型),这时候你需要在JS里把对象转成JSON字符串,再在iOS端解析:
// JS里修改postMessage: window.webkit.messageHandlers.messageObserver.postMessage(JSON.stringify({ type: 'messageDeleted', data: extractMessageContent(messageElement) }));
// iOS端修改解析: guard let rawString = message.body as? String, let data = rawString.data(using: .utf8), let body = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { print("Failed to parse JSON message:", message.body) return } // 后续解析body的逻辑不变
考虑WhatsApp Web的DOM结构变化或删除逻辑
WhatsApp Web有时候不会直接把删除的消息从DOM里移除,而是修改消息内容为“消息已删除”,或者给元素加特定的class。这时候你的MutationObserver只监听childList变化就不够了,需要同时监听属性和内容变化:
messageObserver.observe(chatContainer, { childList: true, subtree: true, attributes: true, characterData: true });
然后在mutation回调里处理属性/内容变化的情况:
} else if (mutation.type === 'attributes' || mutation.type === 'characterData') { let target = mutation.target; let messageElement = target.closest('[data-id]'); if (messageElement) { // 检查是否是已删除的消息,比如文本包含“已删除”,或者有特定class let isDeleted = messageElement.textContent.includes("已删除") || messageElement.classList.contains("message-deleted"); if (isDeleted) { console.log("Message marked as deleted:", messageElement.getAttribute('data-id')); window.webkit.messageHandlers.messageObserver.postMessage({ type: 'messageDeleted', data: extractMessageContent(messageElement) }); } } }
最后,确认聊天容器的选择器是否正确
WhatsApp Web的DOM结构可能更新了,原来的#main div.copyable-area可能不是现在的聊天消息容器。你可以在Chrome开发者工具里打开WhatsApp Web,找到聊天消息的父容器,复制它的选择器。比如现在可能是div[data-testid="conversation-panel-messages"]或者其他,替换掉observeChat里的选择器。
按照这些步骤一步步排查,应该能找到问题所在。大概率是JS里的某个环节没走到postMessage,或者iOS端解析消息失败导致的。
备注:内容来源于stack exchange,提问作者Vivek Warde

