本文使用 WXT (Web Extension Tools) 框架开发,browser.XXX 会被动编译转化为 chrome.XXX,特此说明。

问题描述

在开发 Chrome 扩展时,遇到了一个常见的错误:

1
Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.

这个错误出现在扩展的 background 脚本中,具体是在 background 服务初始化完成后立即出现:

1
2
3
[wxt] Reloading content script: Object  background.js:1747
[25/02/04 14:32:35.375] [background] Background 服务初始化完成!
Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.  background.js:1

问题排查过程

1. 初步分析

最初以为这是 WXT(Web Extension Tools)框架的问题,因为在 GitHub 上发现了类似的 issue。但进一步分析发现,这其实是 Chrome 扩展架构本身的特性。

2. 代码排查

通过在代码中添加日志,定位到错误发生在 broadcastBackgroundReady 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function broadcastBackgroundReady() {
  console.log('broadcastBackgroundReady  -------- 1')
  try {
    Logger.debug('Background broadcastBackgroundReady  - 2')
    void browser.runtime.sendMessage(
      { type: 'BACKGROUND_READY' },
      (response) => {
        console.log(
          'broadcastBackgroundReady  -------- 4',
          browser.runtime.lastError,
          response
        )
        return true
      }
    )
    Logger.debug('Background broadcastBackgroundReady  - 2')
  } catch (error) {
    Logger.error('Background sendMessage', error)
  }
}

3. 错误尝试

最初尝试了几个错误的解决方案:

  1. 修改消息发送时序
  2. 添加额外的错误处理
  3. 尝试在 onInstalled 事件中重新注入 content scripts(这是一个不相关的解决方案)

解决方案

最终发现,这个错误是由 Chrome 扩展的消息传递机制导致的。正确的解决方案非常简单:

1
2
3
4
5
6
7
8
function broadcastBackgroundReady(): void {
  void browser.runtime.sendMessage(
    { type: ExtendedLogMessageType.LOG_BACKGROUND_READY },
    () => {
      void browser.runtime.lastError // 消费掉可能的异常,防止抛到外侧
    }
  )
}

关键点是在消息回调函数中访问 browser.runtime.lastError,这样就能避免错误被抛出。

技术分析

Chrome 扩展消息机制

  1. 错误产生原因

    • 当发送消息时,如果接收端不存在,Chrome 会在 browser.runtime.lastError 中设置错误信息
    • 如果在回调函数中没有检查这个错误,它就会被抛出成为未捕获的异常
  2. browser.runtime.lastError 的特殊性

    • 这是 Chrome 扩展 API 的一个特殊设计
    • 只要在回调函数中访问了 lastError,Chrome 就认为错误被处理了
    • 不需要实际对错误做任何处理,仅访问这个属性就足够了
  3. 最佳实践

    1
    2
    3
    
    browser.runtime.sendMessage(message, () => {
      void browser.runtime.lastError // 最简洁的处理方式
    })
    

为什么会出现这个错误?

在扩展初始化过程中,这个错误是正常的,因为:

  1. background 脚本初始化完成时,可能其他组件(如 popup、content scripts)还没准备好
  2. 当发送广播消息时,如果没有接收者,就会触发这个错误
  3. 这个错误本身不影响功能,只是需要正确处理以避免出现在控制台中

Chrome 扩展消息机制深入解析

browser.runtime.lastError 的特性

  1. 临时性

    • lastError 只在当前异步操作的回调函数中有效
    • 一旦离开回调函数作用域,lastError 就会被清除
    • 不能在回调函数外部访问 lastError
  2. 访问即消费

    • 只要在代码中访问了 lastError,Chrome 就认为错误被处理了
    • 不需要读取它的值,不需要进行任何处理
    • 即使是 void browser.runtime.lastError 这样的语句也足够了
  3. 作用域限制

    • lastError 只在产生它的那个回调函数中有效
    • 不同的消息回调有自己独立的 lastError
    • 不能跨回调函数共享 lastError

runtime.sendMessage 的异常处理最佳实践

  1. 基本用法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // 最简单的处理方式
    browser.runtime.sendMessage(message, () => {
      void browser.runtime.lastError
    })
    
    // 如果需要记录错误
    browser.runtime.sendMessage(message, () => {
      if (browser.runtime.lastError) {
        console.log('消息发送失败:', browser.runtime.lastError.message)
      }
    })
    
  2. Promise 封装

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    function sendMessageWithPromise(message: any): Promise<any> {
      return new Promise((resolve, reject) => {
        browser.runtime.sendMessage(message, (response) => {
          if (browser.runtime.lastError) {
            // 即使要reject,也要先访问lastError
            const error = browser.runtime.lastError
            reject(error)
          } else {
            resolve(response)
          }
        })
      })
    }
    
  3. 常见错误模式

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    // ❌ 错误:在回调外捕获异常
    try {
      browser.runtime.sendMessage(message)
    } catch (error) {
      // 这里捕获不到 lastError
    }
    
    // ❌ 错误:异步访问 lastError
    browser.runtime.sendMessage(message, (response) => {
      setTimeout(() => {
        // 这里 lastError 已经不存在了
        console.log(browser.runtime.lastError)
      }, 0)
    })
    
    // ✅ 正确:在回调函数中立即处理
    browser.runtime.sendMessage(message, (response) => {
      if (browser.runtime.lastError) {
        // 立即处理错误
      }
    })
    
  4. 错误类型: 常见的 lastError 消息包括:

    • “Could not establish connection. Receiving end does not exist.”
    • “The message port closed before a response was received.”
    • “No tab with id {tabId}.”
  5. 处理建议

    • 对于广播类消息(如本例中的 broadcastBackgroundReady),可以忽略错误
    • 对于需要响应的消息,应该适当处理错误并通知调用者
    • 在开发环境中可以记录错误日志,生产环境可以静默处理
    • 避免在错误处理中执行复杂的异步操作

性能考虑

  1. 错误处理开销

    • 访问 lastError 的开销很小
    • 相比之下,未处理的异常会在控制台产生大量日志,影响调试
    • 在高频消息传递场景下,建议使用简单的 void browser.runtime.lastError 处理方式
  2. 调试友好性

    • 开发环境可以记录详细错误信息
    • 生产环境可以使用条件编译或环境变量控制错误日志
    • 考虑使用 source map 便于定位问题

结论

这个问题展示了 Chrome 扩展开发中的一个常见陷阱。虽然错误信息看起来很严重,但实际上这是一个预期的行为,只需要正确处理 browser.runtime.lastError 就可以了。这也提醒我们在开发 Chrome 扩展时要注意消息传递机制的特殊性。