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
点击插件时弹出的部分(如图所示),就当成一个普通的页面进行开发即可。
通信
通信至关重要,插件的开发离不开通信,因为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."}); }); });