Chrome 插件架构开发指南

1,233 阅读3分钟

Chrome 插件开发指南

项目架构

俗话说,工欲善其事必先利其器。想要进行chrome插件开发,好的架构必不可少。在这里推荐 rollup-plugin-chrome-extension 来作为项目的编译和打包。并且值得一提的是,项目方提供了各种模板,js、ts、Svelet等,建议大家直接使用ts版本进行开发。

使用这个的好处在哪呢?开箱即用,省去了自己写webpack配置的麻烦。并且最重要的是,它提供了类似webpack 热更新 的机制,代码编写后,会自动编译打包,并自动更新插件。(具体原理与webpack热更新相似,有兴趣的小伙伴也可自行研究)。

笔者一开始基于 creat-react-app typescript版本,并重写webpack配置从而支持多入口打包配置等功能就稍显麻烦,直到发现这个插件,如久旱逢甘霖。

文档

开发Chrome插件,查阅官方API文档必不可少。其中,可主要优先阅读 主要API分类 部分,从而了解所有可用的功能。 实际开发中,仅仅阅读文档是不够的,该文档在某些部分确实讲的不够清晰(也有可能是因为笔者英文水平不够),配套Stack Overflow搜索相关问题往往有意想不到的效果。

概念理解

想要开发Chrome插件,就必须先了解诸多概念。接下来,笔者会一一讲述。

Manifest.json

该文件可以理解为类似package.json的配置文件,主要是为了声明一些基本信息,如插件开发版本,所需要使用到的权限等。是的,Chrome插件为我们提供了大量的api,例如截图,存储、网络信息获取等。

Content-Script

该模块会被自动插入当前浏览器正在浏览的页面,相当于提供了篡改当前浏览页面的能力,你可以在这里操纵浏览页面的DOM结构,但同时它又能调用部分插件的api,同时它能通过相关api和其他模块进行通信。

background

运行在插件的后台模块,可以理解为插件自己的script,在这里去调用丰富的chrome底层能力,例如截取当前页面、发送请求(此处发送不会跨域)等

popup

点击插件时弹出的部分(如图所示),就当成一个普通的页面进行开发即可。

image-20220421164352310.png

通信

通信至关重要,插件的开发离不开通信,因为background和Content-Script有着如此大的不同,导致他们必须分工合作。而分工合作又离不开通信。而通信又分为单次通信和长时通信

单次通信
  • 由Content-Script发起的与background进行通信

    \ Content-Script 
    chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
      console.log(response.farewell);
    });
    
    \ background  接收方
    chrome.runtime.onMessage.addListener((msg) => {
      console.log(`reseive${msg}`)
    });
    
  • 由background进行发起通信

    // background 通用的一个发送信息封装函数
    /**
     * send message to content
     */
    export const sendMessage = (data: Message, options?: MessageOptions) => {
      chrome.tabs.query(options && options.queryOptions || { active: true, currentWindow: true }, (tabs) => {
        const tabId = (options && options.tabId) || tabs[0].id
        if (tabId) {
          chrome.tabs.sendMessage(tabId, data);
        }
      });
    };
    ​
    
    \ Content-Script  接收方
    chrome.runtime.onMessage.addListener((msg) => {
      console.log(`reseive${msg}`)
    });
    
长时通信 (链接
  • 发起方

    // content-script
    var port = chrome.runtime.connect({name: "knockknock"});
    port.postMessage({joke: "Knock knock"});
    port.onMessage.addListener(function(msg) {
      if (msg.question === "Who's there?")
        port.postMessage({answer: "Madame"});
      else if (msg.question === "Madame who?")
        port.postMessage({answer: "Madame... Bovary"});
    });
    
    // background 通用的一个发送信息封装函数
    /**
     * send message to content
     */
    export const sendMessage = (data: Message, options?: MessageOptions) => {
      chrome.tabs.query(options && options.queryOptions || { active: true, currentWindow: true }, (tabs) => {
        const tabId = (options && options.tabId) || tabs[0].id
        if (tabId) {
          const port = chrome.tabs.connect(tabId, {name: "knockknock"});
          port.postMessage({joke: "Knock knock"});
          port.onMessage.addListener(function(msg) {
            if (msg.question === "Who's there?")
              port.postMessage({answer: "Madame"});
            else if (msg.question === "Madame who?")
              port.postMessage({answer: "Madame... Bovary"});
          });
        }
      });
    };
    
  • 接收方

    // background 和 content-script 都可以复用
    chrome.runtime.onConnect.addListener(function(port) {
      console.assert(port.name === "knockknock");
      port.onMessage.addListener(function(msg) {
        if (msg.joke === "Knock knock")
          port.postMessage({question: "Who's there?"});
        else if (msg.answer === "Madame")
          port.postMessage({question: "Madame who?"});
        else if (msg.answer === "Madame... Bovary")
          port.postMessage({question: "I don't get it."});
      });
    });