实现事件

与函数类似,事件需要在模式中进行定义,并在 ExtensionAPI 实例内的 Javascript 中进行实现。

在 API 模式中声明事件

简单事件的定义如下所示

[
  {
    "namespace": "myapi",
    "events": [
      {
        "name": "onSomething",
        "type": "function",
        "description": "Description of the event",
        "parameters": [
          {
            "name": "param1",
            "description": "Description of the first callback parameter",
            "type": "number"
          }
        ]
      }
    ]
  }
]

此片段定义了一个事件,该事件可通过扩展中的以下代码使用

browser.myapi.onSomething.addListener(param1 => {
  console.log(`Something happened: ${param1}`);
});

请注意,模式语法与函数的语法类似,但对于事件,parameters 属性指定将传递给监听器的参数。

实现事件

与函数一样,在模式中定义事件会导致自动创建包装器并将其公开到扩展的相应 Javascript 上下文中。对于扩展而言,事件显示为一个对象,该对象具有三个标准函数属性:addListener()removeListener()hasListener()。同样,如果 API 定义了一个事件但在子进程中未实现它,则子进程中的包装器会有效地将这些调用代理到主进程中的实现。

一个名为 EventManager 的辅助类使实现事件变得相对简单。简单的事件实现如下所示

this.myapi = class extends ExtensionAPI {
  getAPI(context) {
    return {
      myapi: {
        onSomething: new EventManager({
          context,
          name: "myapi.onSomething",
          register: fire => {
            const callback = value => {
              fire.async(value);
            };
            RegisterSomeInternalCallback(callback);
            return () => {
              UnregisterInternalCallback(callback);
            };
          }
        }).api(),
      }
    }
  }
}

EventManager 类通常仅如本示例中所示直接使用。构造函数的第一个参数是 ExtensionContext 实例,通常只是传递给 API 的 getAPI() 函数的对象。第二个参数是名称,仅用于调试。第三个参数是最重要的部分,它是一个函数,在第一次为该事件添加监听器时调用。此函数传递一个对象(示例中的 fire),该对象用于在事件发生时调用扩展的监听器。 fire 对象有几种不同的方法可以调用监听器,但对于在主进程中实现的事件,唯一有效的方法是 async(),它会异步执行监听器。

事件设置函数(传递给 EventManager 构造函数的函数)必须返回一个清理函数,该函数将在显式地通过扩展调用 removeListener() 或隐式地当添加监听器的扩展 Javascript 上下文被销毁时移除监听器时被调用。

在本示例中,RegisterSomeInternalCallback()UnregisterInternalCallback() 表示用于从 chrome 特权代码侦听某些内部浏览器事件的方法。这通常类似于使用 Services.obs 添加观察器或将监听器附加到 EventEmitter

在构造 EventManager 的实例后,其 api() 方法返回一个具有 addListener()removeListener()hasListener() 方法的对象。这是标准的扩展事件接口,此对象适合从扩展的 getAPI() 方法中返回,如上例所示。

处理传递给 addListener() 的额外参数

事件的标准 addListener() 方法可以接受可选的附加参数,以便在注册事件监听器时传递额外信息。此参数的一个常见应用是用于过滤,以便仅关心某些事件的一小部分实例的扩展可以避免接收它们不关心的事件带来的开销。

传递给 addListener() 的额外参数在模式中使用 extraParameters 属性定义。例如

[
  {
    "namespace": "myapi",
    "events": [
      {
        "name": "onSomething",
        "type": "function",
        "description": "Description of the event",
        "parameters": [
          {
            "name": "param1",
            "description": "Description of the first callback parameter",
            "type": "number"
          }
        ],
        "extraParameters": [
          {
            "name": "minValue",
            "description": "Only call the listener for values of param1 at least as large as this value.",
            "type": "number"
          }
        ]
      }
    ]
  }
]

以这种方式定义的额外参数将传递给事件设置函数(EventManager 构造函数的最后一个参数)。例如,扩展我们上面的示例

this.myapi = class extends ExtensionAPI {
  getAPI(context) {
    return {
      myapi: {
        onSomething: new EventManager({
          context,
          module: "myapi",
          event: "onSomething",
          register: (fire, minValue) => {
            const callback = value => {
              if (value >= minValue) {
                fire.async(value);
              }
            };
            RegisterSomeInternalCallback(callback);
            return () => {
              UnregisterInternalCallback(callback);
            };
          }
        }).api()
      }
    }
  }
}

处理监听器返回值

某些事件 API 允许扩展通过返回由 API 处理的事件监听器的值来以某种方式影响事件处理。这可以在模式中使用 returns 属性定义

[
  {
    "namespace": "myapi",
    "events": [
      {
        "name": "onSomething",
        "type": "function",
        "description": "Description of the event",
        "parameters": [
          {
            "name": "param1",
            "description": "Description of the first callback parameter",
            "type": "number"
          }
        ],
        "returns": {
          "type": "string",
          "description": "Description of how the listener return value is processed."
        }
      }
    ]
  }
]

事件的实现使用 fire.async() 的返回值,该值是一个 Promise,它解析为监听器的返回值

this.myapi = class extends ExtensionAPI {
  getAPI(context) {
    return {
      myapi: {
        onSomething: new EventManager({
          context,
          module: "myapi",
          event: "onSomething",
          register: fire => {
            const callback = async (value) => {
              let rv = await fire.async(value);
              log(`The onSomething listener returned the string ${rv}`);
            };
            RegisterSomeInternalCallback(callback);
            return () => {
              UnregisterInternalCallback(callback);
            };
          }
        }).api()
      }
    }
  }
}

请注意,模式 returns 定义是可选的,仅用于文档记录。也就是说,fire.async() 始终返回一个 Promise,该 Promise 解析为监听器的返回值,如果事件的实现不关心返回值,则可以忽略此 Promise。

在子进程中实现事件

在子进程中实现事件的原因与在子进程中实现函数的原因类似

  • 事件的监听器返回一个值,API 实现必须同步处理该值。

  • addListener() 或监听器函数具有一个或多个无法在进程之间发送的类型的参数。

  • 事件的实现与只能从子进程访问的代码交互。

  • 事件可以在子进程中实现得更高效。

在子进程中实现事件的过程与函数相同 - 只需在加载到子进程中的 ExtensionAPI 子类中实现事件即可。就像子进程中的函数可以使用 callParentAsyncFunction() 调用主进程中的函数一样,子进程中的事件可以使用类似的 getParentEvent() 订阅在主进程中实现的事件。例如,子进程中自动生成的事件代理可以显式地编写为

this.myapi = class extends ExtensionAPI {
  getAPI(context) {
    return {
      myapi: {
        onSomething: new EventManager(
          context,
          name: "myapi.onSomething",
          register: fire => {
            const listener = (value) => {
              fire.async(value);
            };

            let parentEvent = context.childManager.getParentEvent("myapi.onSomething");
            parent.addListener(listener);
            return () => {
              parent.removeListener(listener);
            };
          }
        }).api()
      }
    }
  }
}

在子进程中实现的事件可以使用一些其他方法来分派监听器

  • fire.sync() 这会同步运行监听器并返回监听器返回的值

  • fire.raw() 这会同步运行监听器,而不会将监听器参数克隆到扩展的 Javascript 隔室中。这用作性能优化,除非您详细了解 Javascript 隔室和跨隔室包装器,否则不应使用它。

事件监听器持久化

在某些情况下,事件监听器会被持久化。持久化的事件监听器可以阻止启动,或者导致事件页面或后台服务工作线程启动。

事件监听器必须在后台的顶级作用域中同步注册。稍后或异步注册的事件监听器不会被持久化。

目前,只有 WebRequestBlocking 和 Proxy 事件才能在启动时阻塞,从而导致附加组件在 Firefox 启动时更早启动。模块是否可以阻止启动由模块定义文件 (ext-toolkit.jsonext-browser.json) 中的 startupBlocking 标志定义。此外,这些是为持久后台脚本持久化的唯一事件。

仅在子进程中实现,而没有父进程对应部分的事件无法持久化。

持久化和预加载事件监听器

在术语方面

  • 持久化事件监听器 是与事件监听器相关的数据集(特别是 API 模块、API 事件名称以及在调用 addListener 时传递的参数(如果有的话)),这些数据在先前的运行中由事件页面(或后台服务工作线程)注册,并存储在 StartupCache 数据中

  • 预加载事件监听器 是一个“占位符”事件监听器,它是在事件页面(或后台服务工作线程)未运行(尚未启动或在空闲超时后挂起)时,从 StartupCache 中找到的持久化事件监听器数据创建的

ExtensionAPIPersistent 和 PERSISTENT_EVENTS

大多数 WebExtensions API 都会承诺一些 API 事件,并且这些事件中的大多数也可能预期在发出时唤醒事件页面(或后台服务工作线程),而此时后台扩展上下文尚未启动(或者在空闲超时后被挂起)。

作为实现旨在持久化其所有或部分 API 事件监听器的 WebExtensions API 的一部分

  • WebExtensions API 命名空间类应扩展 ExtensionAPIPersistent(而不是扩展 ExtensionAPI 类)

  • WebExtensions API 命名空间应具有一个 PERSISTENT_EVENTS 属性,该属性预期设置为定义方法的对象。每个方法都应以相关的 API 事件名称命名,这些名称将在内部调用

    • 在扩展事件页面(或后台服务工作线程)未运行时(既尚未启动,也未在空闲超时后挂起)。这些方法由 WebExtensions 内部调用,以便为该扩展持久化的每个 API 事件监听器在父进程中创建占位符 API 事件监听器。这些占位符监听器在内部称为 primed listeners)。

    • 在扩展事件页面(或后台服务工作线程)运行时(以及为可能为扩展创建的任何其他扩展上下文类型)。这些方法由 WebExtensions 内部调用,以创建父进程回调,该回调将负责将 API 事件转发到子进程中的扩展回调。

  • getAPI 方法中。对于此方法返回的表示 API 事件的所有 API 命名空间属性,为每个预期持久化其监听器的 API 事件创建的 EventManager 实例应包含以下选项

    • module,设置为 API 模块名称,如 "ext-toolkit.json" / "ext-browser.json" / "ext-android.json" 中所列(在大多数情况下,与 API 命名空间名称字符串相同)。

    • event,设置为 API 事件名称字符串。

    • extensionApi,设置为 ExtensionAPIPersistent 类实例。

查看一些为引入事件页面支持而应用于某些现有 API 的引入 API 事件监听器持久性的补丁也可能很有用

以下是在代码片段形式中先前描述的内容的示例

this.myApiName = class extends ExtensionAPIPersistent {
  PERSISTENT_EVENTS = {
    // @param {object}             options
    // @param {object}             options.fire
    // @param {function}           options.fire.async
    // @param {function}           options.fire.sync
    // @param {function}           options.fire.raw
    //        For primed listeners `fire.async`/`fire.sync`/`fire.raw` will
    //        collect the pending events to be send to the background context
    //        and implicitly wake up the background context (Event Page or
    //        Background Service Worker), or forward the event right away if
    //        the background context is running.
    // @param {function}           [options.fire.wakeup = undefined]
    //        For primed listeners, the `fire` object also provide a `wakeup` method
    //        which can be used by the primed listener to explicitly `wakeup` the
    //        background context (Event Page or Background Service Worker) and wait for
    //        it to be running (by awaiting on the Promise returned by wakeup to be
    //        resolved).
    // @param {ProxyContextParent} [options.context=undefined]
    //        This property is expected to be undefined for primed listeners (which
    //        are created while the background extension context does not exist) and
    //        to be set to a ProxyContextParent instance (the same got by the getAPI
    //        method) when the method is called for a listener registered by a
    //        running extension context.
    //
    // @param {object}            [apiEventsParams=undefined]
    //        The additional addListener parameter if any (some API events are allowing
    //        the extensions to pass some parameters along with the extension callback).
    onMyEventName({ context, fire }, apiEventParams = undefined) {
      const listener = (...) {
        // Wake up the EventPage (or Background ServiceWorker).
        if (fire.wakeup) {
          await fire.wakeup();
        }

        fire.async(...);
      }

      // Subscribe a listener to an internal observer or event which will be notified
      // when we need to call fire to either send the event to an extension context
      // already running or wake up a suspended event page and accumulate the events
      // to be fired once the extension context is running again and a callback registered
      // back (which will be used to convert the primed listener created while
      // the non persistent background extension context was not running yet)
      ...
      return {
        unregister() {
          // Unsubscribe a listener from an internal observer or event.
          ...
         }
        convert(fireToExtensionCallback) {
          // Convert gets called once the primed API event listener,
          // created while the extension background context has been
          // suspended, is being converted to a parent process API
          // event listener callback that is responsible for forwarding the
          // events to the child processes.
          //
          // The `fireToExtensionCallback` parameter is going to be the
          // one that will emit the event to the extension callback (while
          // the one got from the API event registrar method may be the one
          // that is collecting the events to emit up until the background
          // context got started up again).
          fire = fireToExtensionCallback;
        },
      };
    },
    ...
  };

  getAPI(context) {
    ...
    return {
      myAPIName: {
        ...
        onMyEventName: new EventManager({
          context,
          // NOTE: module is expected to be the API module name as listed in
          // ext-toolkit.json / ext-browser.json / ext-android.json.
          module: "myAPIName",
          event: "onMyEventNAme",
          extensionApi: this,
        }),
      },
    };
  }
};

测试持久化 API 事件监听器

  • extension.terminateBackground({ expectStopped: true, disableResetIdleForTest: false } = {}):

    • ExtensionTestUtils.loadExtension 返回的包装器对象提供了一个 terminateBackground 方法,该方法可用于模拟空闲超时,方法是显式触发处理空闲超时的相同逻辑。

    • 默认情况下,此帮助程序还会隐式断言,一旦 terminateBackground 异步逻辑完全执行,extension.backgroundState 将设置为 "stopped"

    • 此方法还接受一些可选参数

      • 如果 expectStopped 设置为 false,则帮助程序将断言,一旦 terminateBackground 异步逻辑完全执行,extension.backgroundState 将设置为“running”,这旨在用于涵盖重置空闲超时逻辑和条件的特定测试中。

      • 如果 disableResetIdleForTest 设置为 true,则帮助程序将忽略所有由于某些工作仍在挂起而导致重置空闲超时的条件(例如,NativeMessagingPort 仍然打开,StreamFilter 实例仍然处于活动状态或来自 API 事件监听器调用的 Promise 尚未解析)

  • ExtensionTestUtils.testAssertions.assertPersistentListeners:

    • 此测试断言帮助程序可用于更轻松地断言给定 API 事件的持久化状态(例如,断言它未持久化,或已持久化和/或准备好)。

assertPersistentListeners(extension, "browserAction", "onClicked", {
   primed: false,
 });
 await extension.terminateBackground();
 assertPersistentListeners(extension, "browserAction", "onClicked", {
   primed: true,
 });
  • extensions.background.idle.timeout 首选项决定在考虑事件页面处于空闲状态并将其挂起之前等待多长时间(在将 API 事件通知给扩展事件页面之间),在某些 xpcshell 测试中,此首选项可能设置为 0 以减少测试需要等待事件页面自动挂起的时间。

  • extension.eventPage.enabled 首选项负责为 manifest_version 2 扩展启用/禁用事件页面支持,从技术上讲,它现在在所有通道上都设置为 true,但在旨在涵盖 manifest_version 2 测试扩展的事件页面行为的测试中,仍然值得将其显式翻转为 true,直到首选项完全删除(主要是为了确保如果需要将首选项翻转为 false,测试仍然会通过)。

持久化事件监听器内部

ExtensionAPIPersistent 类提供了一种快速将 API 事件监听器持久性引入新的 WebExtensions API 并减少代码重复的方法,以下部分提供有关抽象在实践中内部执行的操作的更多详细信息。

扩展 ExtensionAPIPersistent 基类的 WebExtensions API 类仍然能够支持非持久化监听器以及持久化监听器(例如,持久化从事件页面注册的监听器的事件已不再持久化从其他扩展上下文注册的监听器),并且可以混合持久化和非持久化事件。

例如,在 toolkit/components/extensions/parent/ext-runtime.js` 中,两个事件 onSuspendonSuspendCanceled 预期永远不会持久化或准备好(即使对于事件页面),因此它们的 EventManager 实例接收以下选项

  • 一个 register 回调(而不是 PERSISTED_EVENTS 的一部分)

  • 一个 name 字符串属性(而不是用于 EventManager 实例的两个单独的 moduleevent 字符串属性)

  • 没有 extensionApi 属性(因为这仅适用于预期持久化事件页面监听器的事件)。

在实践中,ExtensionAPIPersistent 扩展 ExtensionAPI 类以提供一个通用的 primeListeners 方法,该方法负责在事件页面被挂起或尚未启动时准备持久化监听器。

primeListener 方法预期返回一个具有 unregisterconvert 方法的对象,而传递给 EventManager 构造函数的 register 回调预期返回 unregister 方法。

function somethingListener(fire, minValue) => {
  const callback = value => {
    if (value >= minValue) {
      fire.async(value);
    }
  };
  RegisterSomeInternalCallback(callback);
  return {
    unregister() {
      UnregisterInternalCallback(callback);
    },
    convert(_fire, context) {
      fire = _fire;
    }
  };
}

this.myapi = class extends ExtensionAPI {
  primeListener(extension, event, fire, params, isInStartup) {
    if (event == "onSomething") {
      // Note that we return the object with unregister and convert here.
      return somethingListener(fire, ...params);
    }
    // If an event other than onSomething was requested, we are not returning
    // anything for it, thus it would not be persistable.
  }
  getAPI(context) {
    return {
      myapi: {
        onSomething: new EventManager({
          context,
          module: "myapi",
          event: "onSomething",
          register: (fire, minValue) => {
            // Note that we return unregister here.
            return somethingListener(fire, minValue).unregister;
          }
        }).api()
      }
    }
  }
}