问题背景

最近我在开发Chrome浏览器插件时,遇到了一个需要脚本初始化的问题。在插件被安装后或浏览器标签页被刷新时,我需要重新初始化插件。为了实现这一点,我研究了Chrome提供的几个API接口,它们分别是chrome.runtime.onInstalled.addListenerchrome.runtime.onStartup.addListener以及chrome.tabs.onUpdated.addListener。在本文中,我将对这三个关键事件监听器进行梳理,并详细说明它们之间的区别。

事件监听器

使用注意项

事件监听器通常是在 background.js 文件中使用,因为 background.js 是扩展程序的后台脚本,负责管理扩展的生命周期和处理事件。不过,也可以在其他 JavaScript 文件中使用事件监听器,只是使用一些特殊的方法:

1、可以在扩展程序的 manifest.json 中声明多个后台脚本。

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "background": {
    "service_worker": ["background1.js", "background2.js"]
  },
  "permissions": [
    "tabs",
    "storage",
    "activeTab",
    "scripting"
  ]
}

2、通过模块化的方式,将事件监听器相关的代码组织到不同的模块中,然后在 background.js 中导入和使用这些模块。

// eventHandlers.js
export function setupEventListeners() {
  chrome.runtime.onInstalled.addListener(() => {
    console.log('Extension installed');
  });

  chrome.runtime.onStartup.addListener(() => {
    console.log('Browser startup');
  });

  chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.status === 'complete' && tab.url.includes('https://blog.51cto.com/')) {
      console.log('Page loaded: ', tab.url);
    }
  });
}
// background.js
import { setupEventListeners } from './eventHandlers.js';

setupEventListeners();

3、虽然内容脚本或其他页面脚本,无法直接监听扩展的生命周期事件,但是如果需要与后台脚本通信,可以使用消息传递的方式。

// contentScript.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'pageLoaded') {
    console.log('Content script received page loaded message');
    // 执行相关操作
  }
});
// background.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url.includes('https://blog.51cto.com/')) {
    chrome.tabs.sendMessage(tabId, { action: 'pageLoaded' });
  }
});

chrome.runtime.onInstalled.addListener 的应用场景

触发时机:

  • 当扩展程序被首次安装时触发。
  • 当扩展程序被更新到新版本时触发。
  • 当 Chrome 浏览器被更新到新版本时触发(扩展程序的清单文件中指定了 manifest_version 为2,而不是3)。

适用场景:

  • 一次性初始化任务:适用于在扩展程序安装或更新时执行一次性的初始化任务,例如设置初始配置、显示欢迎页面、注册持久性任务等。
  • 注册内容脚本:在扩展程序安装或更新时,可以通过该事件注册内容脚本,以便在特定网站上执行 JavaScript 代码。这对于在特定页面上执行定制化操作非常有用。
  • 创建上下文菜单:可以在安装或更新扩展程序时创建右键菜单项,以提供更方便的用户体验。例如,可以添加 "在新标签页中打开链接" 的选项。
  • 初始化存储:设置或重置存储中的初始数据。这对于在扩展程序启动时需要一些默认设置或数据非常有用。

代码示例

chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    console.log('扩展程序已安装');
    // 执行安装时的初始化任务
    // 例如:设置默认配置
    chrome.storage.sync.set({color: 'blue'}, function() {
      console.log('默认颜色设置为蓝色');
    });
  } else if (details.reason === 'update') {
    console.log('扩展程序已更新');
    // 执行更新时的任务
    // 例如:迁移存储中的旧数据
  }
});

chrome.runtime.onStartup.addListener 的应用场景

触发时机:

  • 当 Chrome 浏览器启动时触发(包括重新启动)。
  • 当用户首次启动浏览器并登录他们的 Google 账号,或者当用户在已经启动的浏览器中切换到不同的 Chrome 账号时

用途:

  • 重复初始化任务:适用于在每次浏览器启动时执行初始化任务,例如恢复扩展程序的状态、重新注册临时任务等,或者在启动时清理上一次运行中留下的数据。

  • 检查并恢复状态:在浏览器启动时检查并恢复扩展程序的状态。从存储中读取上次保存的状态,并恢复这些状态。例如,恢复用户设置的选项,恢复上次浏览的页面等。

  • 重新注册动态内容脚本:重新注册在运行时添加的内容脚本。这样可以确保内容脚本在目标页面上始终有效。

  • 初始化连接:建立必要的连接或重新初始化服务。例如,与后台服务器重新建立连接,初始化 WebSocket 等。

代码示例

chrome.runtime.onStartup.addListener(() => {
  console.log('Chrome 浏览器已启动或用户已登录');
  
  // 恢复扩展程序的状态
  chrome.storage.sync.get(['color'], function(result) {
    if (result.color) {
      console.log('启动时获取的颜色是:' + result.color);
    } else {
      console.log('没有设置颜色,使用默认值');
      chrome.storage.sync.set({color: 'blue'}, function() {
        console.log('默认颜色设置为蓝色');
      });
    }
  });

  // 重新注册动态内容脚本
  chrome.scripting.registerContentScripts([{
    id: 'myContentScript',
    matches: ['https://blog.51cto.com/*'],
    js: ['contentScript.js'],
    runAt: 'document_end'
  }]);

  // 初始化连接,例如 WebSocket
  const socket = new WebSocket('wss://xiaodou.com/socket');
  socket.onopen = () => {
    console.log('WebSocket 连接已建立');
  };
  socket.onmessage = (event) => {
    console.log('收到消息:', event.data);
  };
  socket.onclose = () => {
    console.log('WebSocket 连接已关闭');
  };
});

chrome.tabs.onUpdated.addListener 的应用场景

触发时机

当标签页的状态更新时触发,这包括页面加载开始、页面加载完成、URL 更改等情况。

用途

  • 监听页面更新:通过监听标签页的更新事件,可以在页面加载开始、加载完成或 URL 发生变化时执行特定操作。例如,可以记录用户访问的页面,或者在页面加载完成时执行一些数据采集或分析任务。

  • 在特定页面上动态注入脚本或样式:可以根据 URL 动态决定是否注入特定的内容脚本或样式表。例如,当用户访问某个网站时,自动注入脚本来修改页面的外观或功能,提升用户体验。

  • 根据页面加载状态更新扩展图标、弹出页面等:根据页面的加载状态,动态改变扩展程序的图标,或者更新弹出页面的内容。例如,当用户访问某个特定网站时,扩展程序的图标变为特定颜色,或者弹出页面显示该网站的特定信息。

代码示例

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // 仅在页面加载完成时触发
  if (changeInfo.status === 'complete' && tab.url) {
    // 例如:检查是否为特定网站
    if (tab.url.includes('https://blog.51cto.com/')) {
      console.log('页面加载完成: ', tab.url);
      // 动态注入脚本
      chrome.scripting.executeScript({
        target: { tabId: tabId },
        function: () => {
          // 示例:向页面注入脚本
          console.log('在页面上执行脚本');
          // 在页面右下角添加一个导出按钮
          const button = document.createElement('button');
          button.textContent = '导出';
          button.style.position = 'fixed';
          button.style.bottom = '10px';
          button.style.right = '10px';
          button.style.zIndex = 1000;
          document.body.appendChild(button);
        }
      });
    }
  }
});

最后象征性的总结一下,就是基本的使用场景如下所示

chrome.runtime.onInstalled.addListener(() => {
    // 执行安装或更新时的一次性初始化任务
});

chrome.runtime.onStartup.addListener(() => {
    // 执行每次启动时需要重复进行的任务
});

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.status === 'complete') {
        // 在特定页面更新完成后执行任务
    }
});