原文地址: https://segmentfault.com/a/11...
转载请在文首注明来源
TL;DR
本文历时较久,2020年写了一半,写到迷茫,索性搁置,2021年翻出来继续写的;经过了半年多的时间再看,会有一些不一样的理解,存在部分配图风格不一致.但是我会确保内容的正确和表达的一致.
2020年的某一天我把之前写的QQ空间批量删除说说&留言的脚本写了个chrome插件版
,名字也改成了QQ空间小助手.
因为也是第一次写插件,所以记录这个过程,分享开发过程和期间遇到的问题,并做一些关键点的梳理.
所以,这并不是一篇大而全的chrome extension开发教程,这是一篇以QQ空间助手为实战来切入插件开发但是侧重讲chrome extension开发的文章.
QQ空间小助手功能很简单,核心功能(其实也就这点功能)是批量删除qq空间的说说和留言.
实现上也很简单,只需要两个步骤.
这里面的困难一个在于找到获取&删除 留言和说说 所需要的参数(这个我们不展开介绍),
另外一个困难是这些功能怎么在chrome extension
中实现.
根据上面这些需求,我理解大概操作流程是这样的,
点击icon
,或者点击icon
弹出来的页面然后就开始获取列表,然后遍历列表开始删说说.
里面大概需要用到,组件,组件间通信这些内容.
但是因为是没有接触过这块,所以得搞清楚都需要有什么知识储备,那就先从这里开始介绍.
整体上,插件开发这个东西没那么难,需要的技术也比较简单.基本上就是前端那些知识:
外加一些软技能
chrome devtools
的使用和基本的调试能力基本上做过项目的前端都能hold住.
如果你在开发之前有些不确定的问题的话需要提前了解的话,也可以看这篇文档高频问题Q&A.
介绍完了知识储备,我们就得看文档了.
开发文档推荐看这份官方文档 加上一些 官方demo.
接触新的技术,文档肯定是不可少的,但是网上各种教程 + 文档,鱼龙混杂,我们到底应该看教程还是看文档.
我个人理解是,不管教程还是文档,都可以看,但是要优先看官方的文档和教程,看一手的资料,因为我看文档的过程中发现,非官方的东西(非一手文档)信息传达不完整,甚至有纰漏.所以尽着官方的来,除非有很好的非官方教程或者其他的原因.
也有一份非官方文档不错,但是不推荐直接看,建议结合官方文档作为对照来看,因为我在开发的时候发现这份文档有些内容和官方最新的文档不一致(比如对于browser action
和page action
的定义).
另外,在实际的阅读过程中,还会有些地方看文档看不明白的,这时候有针对的搜一些博客/教程就可以.比如这次组件通信的地方有困惑,就找了些不错的文档看.
还有一些别的文章,都整理下来可以参考.
插件通信相关辅助
其他
看完文档我们大致会得到一些信息.下面就来讲讲.
在这一部分你只需要了解一个插件会由哪些部分组成就可以了,具体功能后面介绍.
正常情况下我们看到的浏览器和插件大概长这样
当我们鼠标点击插件icon之后就变成了这样
从上面的图上来看,主要由 icon
(任务栏的图标) 和 点击之后的弹窗两个大件组成,部分插件还会有一个专门的用于配置的页面(这里没用到,就不介绍了).
另外,在UI之外,还会background
和content_script
存在.
再加上这两个重要的概念之后整个结构就是这样的
下面看看文件结构.
文件结构比较能直观的反应组件的组成和功能.从文件结构来看。一般一个插件的文件结构长这样的(之所以说一般是因为不是每个插件都能用到全部的功能).
qzone_helper_extension │ background.js │ manifest.json │ popup.html │ popup.js │ qq_icon.png └─ content_script.js
其中各个文件的功能如下:
manifest.json
是我们最先需要了解的部分,这个文件的功能类似于package.json
文件,这里定义插件的配置信息,这些信息小到插件名称,图标,版本号等描述信息,大到你需要申请的权限和各种引入的资源,入口文件等重要配置。所以这里应该是我们最先需要了解的部分。qq_icon.png
是最不值得我们理解的部分,这就是个图标文件而已。popup.html
是比较直观的一个文件,这个就是你点击插件图标之后弹出来的界面,那个界面是html写的,同样的,对应的popup.js
用来处理popup
页面的交互和popup
模块和别的模块之间的通信。background.js
和content_script.js
是核心.其中,background.js
可以理解为项目的app.js
文件,而content_script.js
是和网页是一伙的,可以简单的理解为是我们在网页那边的代理,帮我们做些网页相关的操作.加上这些结构和文件之间的对应关系是这样的
到这里,你大概了解的一个插件的基本组成结构.了解完结构,我们分来看看各部分的具体情况.
一般来说,Icon
是我们插件的功能入口,也可以显示一些徽标.
在chrome插件中,这种icon按钮是分两种的:
他们的区别与使用场景如下
类型 | 支持功能 | 应用场景 |
---|---|---|
page action | 不能使用徽章(就是提醒你有多少未读消息的红点和数字) | 非应用在所有页面的情况,比如我这次开发的QQ空间小助手就只是应用在user.qzone.qq.com的一个插件,使用的就是page action |
browser action | 拥有完整的功能(tooltip / popup / badges 对于这些功能不再赘述) | 对所有页面都可以使用的插件,比如adblock / vimium这种 |
你可能有点困惑,我了解了这个区别了,但是有什么用吗?有用,在定义manifest.json
的时候会用到.page action
对应的定义字段是page_action
,browser action
对应的定义字段是browser_action
.
pupop
这个页面主要承载简单的配置和信息展示功能.
这就是个普通的html
文件,里面写你的css
和js
逻辑.
需要注意的是,这里的js
只能操作pupop
里面的DOM
.
回到我们的需求,我们可以在popup
页面放置一些用于触发操作的按钮.比如删除说说按钮
这个文件很重要,但是一上来就放这个文件,让人有点摸不着头脑,所以,放在这里.
但是这个文件没必要每个字段都清楚啥意思,搞清楚你用到的就行了.
{ // Require "manifest_version": 2, // 不同的manifest版本会有不同的功能 "name": "My Extension", "version": "versionString", // Recommended "default_locale": "en", "description": "A plain text description", "icons": {...}, // icon文件路径 // Pick one (or none) "browser_action": {...}, "page_action": {...}, // Optional "action": ..., "author": ..., "automation": ..., "background": { // background对应的配置 // Recommended "persistent": false, // Optional "service_worker": }, "chrome_settings_overrides": {...}, "chrome_ui_overrides": { "bookmarks_ui": { "remove_bookmark_shortcut": true, "remove_button": true } }, "chrome_url_overrides": {...}, "commands": {...}, "content_capabilities": ..., "content_scripts": [{...}], // content_script对应的配置 "content_security_policy": "policyString", "converted_from_user_script": ..., "current_locale": ..., "declarative_net_request": ..., "devtools_page": "devtools.html", "event_rules": [{...}], "externally_connectable": { "matches": ["*://*.example.com/*"] }, "file_browser_handlers": [...], "file_system_provider_capabilities": { "configurable": true, "multiple_mounts": true, "source": "network" }, "homepage_url": "http://path/to/homepage", "import": [{"id": "aaaa"}], "incognito": "spanning, split, or not_allowed", "input_components": ..., "key": "publicKey", "minimum_chrome_version": "versionString", "nacl_modules": [...], "oauth2": ..., "offline_enabled": true, "omnibox": { "keyword": "aString" }, "optional_permissions": ["tabs"], "options_page": "options.html", "options_ui": { "chrome_style": true, "page": "options.html" }, "permissions": ["tabs"], // 需要申请的权限 "platforms": ..., "replacement_web_app": ..., "requirements": {...}, "sandbox": [...], "short_name": "Short Name", "signature": ..., "spellcheck": ..., "storage": { "managed_schema": "schema.json" }, "system_indicator": ..., "tts_engine": {...}, "update_url": "http://path/to/updateInfo.xml", "version_name": "aString", "web_accessible_resources": [...] }
这基本可以理解为是一个常驻后台的js
文件,在里面可以处理一些事件监听.比如监听页面初始化的事件(chrome.runtime.onInstalled
),监听通信事件(chrome.runtime.onMessage
).
另外,从background
发出去的请求可以跨域.
在V3的实现中,background引入了service worker的概念.
在chrome的设置-更多工具-任务管理器里面可以看到我们的background
任务进程.
content_script
文件是和浏览器打开的页面一块加载的,可以操作打开页面的DOM.
例如,点击插件的图标,然后页面改变颜色.这种情况是不能在background.js
中直接改变页面颜色的,而是需要通过事件发送消息到content_script.js
中,通过content_script
来操作页面DOM
.
了解完这些我们大概知道了,我们获取发请求的哪些参数包括发请求都可以在content_script
中实现.因为只有之类可以操作页面的DOM.
上面介绍完了各个组成部分,下面介绍这些部分之间的通信.chrome extension
的通信是通过事件来进行的,通信的内容是有效的JSON
对象.共有三种通信方式:
Simple one-time requests
(就像短连接)Long-lived connections
(就像长连接)Cross-extension messaging
(多个插件间通信)我们这里只展开这次用到的Simple one-time requests
.
插件内的通信分为这几种:
content script => background
content script => popup
background => content script
background => popup
popup => content script
popup => background
content script => background
/content script => popup
/background => popup
/popup => background
这么发送事件
chrome.runtime.sendMessage({greeting: "hello"}, function(response) { console.log(response.farewell); });
content script
发送的事件popup
和background
都可以收到,所以再发送的时候需要加上发给谁的标识,然后收到消息的时候处理popup => background
的通信其实也可以通过chrome.extension.getBackgroundPage()
来获取到background的所有方法,直接调用
background => content script
/popup => content script
这么发送事件
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) { console.log(response.farewell); }); });
- 因为可能打开了多个同一个url的tab,所以需要区分tab.
- 通过chrome.tabs.sendMessage发送的事件只有指定页面的content可以收到
而对于事件的监听处理是一样的
chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension"); if (request.greeting == "hello") sendResponse({farewell: "goodbye"}); } );
整个通信用一张图表示就是
加上通信的部分,我们大概清楚了,我们在popup
中触发操作,然后popup
发送事件给content_script
,content_script
开始获取请求参数,请求数据列表,然后遍历数据做删除操作.整个过程大概就这样,没用到background
.
在地址栏打开chrome://extensions/
,然后开启右上角的开发者模式
然后点击加载已解压的拓展程序
,在打开的文件选择框中选择你的目录就算安装你开发的插件了.
background / popup / content script
的控制台都是独立的,可以通过下面的方式打开
background
的控制台chrome://extensions/
页面,点击对应的插件背景页三个字即可打开background
的控制台popup
的控制台icon
在弹出的popup页面
上右键,然后点击检查,就可以打开popup
的控制台.需要注意的是popup
页面一旦关闭,控制台也会随之关闭.content script
的控制台content script
的控制台其实就是tab页的控制台,但是需要切换一下插件的调试和普通前端开发的debug方式是一样的.
更多详情可以参考文档:Debugging extensions
我们的插件开发完成之后可以选择打包成crx
文件发布到chrome 网上应用店,上传到chrome是需要注册开发者的,注册账号需要5$,注册之后可以发布多个chrome插件.
更多详情可以参考文档创建和发布自定义 Chrome 应用和扩展程序
打包功能在加载已解压的拓展程序
按钮的旁边,然后按照提示走就可以了
也可以直接打成压缩包放到网上让别人下载使用,之前可以本地安装crx
文件,现在不支持了,都是通过加载已解压的拓展程序
在本地使用.
开发的过程中发现有的时候chrome.tabs.query
得到的结果为[]
,后来发现这是chrome的一个bug,在2015年就存在了,一直没有修复.解决方法如下:
var activeTabId; chrome.tabs.onActivated.addListener(function(activeInfo) { activeTabId = activeInfo.tabId; }); // https://bugs.chromium.org/p/chromium/issues/detail?id=462939 function getActiveTab(callback) { chrome.tabs.query({currentWindow: true, active: true}, function (tabs) { let tab = tabs[0]; if (tab) { callback(tab.id); } else { chrome.tabs.get(activeTabId, function (tab) { if (tab) { callback(tab.id); } else { console.log('No active tab identified.'); } }); } }); }
更多相关讨论见: Why doesn't chrome.tabs.query() return the tab's URL when called using RequireJS in a Chrome extension?
需求是在某些页面插件的icon
才亮起来,点击icon
才会展示popup页面
.
这个功能的实现思路比较多,这里讲两种
一种是通过监听conect事件,在事件的处理方法中setIcon
和初始化对应的popup
页面.
Vimium是这么做的,可以看这里.
还有一些利用chrome
的onPageChanged
事件的规则来实现,这种处理只有在符合规则的时候才展示popup
页面.
chrome.runtime.onInstalled.addListener(function() { chrome.declarativeContent.onPageChanged.removeRules(undefined, function() { chrome.declarativeContent.onPageChanged.addRules([{ conditions: [ new chrome.declarativeContent.PageStateMatcher({ pageUrl: { hostContains: 'qzone.qq.com' } }) ], actions: [new chrome.declarativeContent.ShowPageAction()] }]); }); });
这种情况要留意插件管理页面你的插件那里有没有报错提示,像这样
这里的报错必须点进去清除了,才能继续向下进行,不然就会出现操作没有响应的情况.