ReZero's Utopia.

如何写一个简易的chrome插件

Word count: 2.2kReading time: 9 min
2020/03/07 Share

如何写一个简易的chrome插件

推荐阅读

https://developer.chrome.com/extensions/getstarted

推荐阅读官方文档,本文只做简单的演示。

本文图片之类链接打不开,可直接科学上网走外链,参考 原博文

概念介绍

基本配置

  1. manifest.json

可以类比 AndroidManifest,几乎所有的项目都会有一个主配置文件,用来配置全局的基本信息和属性来保证系统的基本启动运行,该文件为 chrome plugin 主配置文件。
基于浏览器插件考虑所占体积这个特性,几乎所有关键的配置属性信息都可以在此找到,除了名称,版本等必要的信息,
尤为值得关注的几个属性就是

  • background 改配置属性决定项目启动时会去扫描哪些 js 文件, 然后以此决定需要监听的事件
1
2
3
4
"background": {
"scripts": ["background.js"],
"persistent": false
}
  • permission 该属性为权限属性,这个的重要性不必多说,上面的属性注册事件监听时往往需要依赖大量的api,而这些api的权限就依赖于这个 permission 属性
1
"permissions": ["storage"]
  • browser_action 权限和动作都有了,剩下的显然就是渲染的模板文件了,该属性决定了回显需要的大多数配置,包括插件的图标,点击插件时显示的页面等
1
2
3
4
"browser_action": {
"default_icon": "icon_16.png",
"default_popup": "popup.html"
}

概念图

重点注意

  • 上面的图中可以看到,插件是基于 message 来互相通信的, 这个 message 后面再说

  • Chrome 不允许将 JavaScript 代码段直接内嵌入HTML文档 (inline-script也是被禁止)

所以我们需要通过外部引入的方式引用JS文件
所以所有元素的事件都需要使用JavaScript代码进行绑定
如果你没有使用一个拥有强大选择器的库(jQuery), 最好给需要绑定事件的元素分配一个id以便进行操作

  • "manifest_version": 2, 这个写 2 吧, 别问为啥

  • contentscript.js 这个决定了用户所在的界面操作,emmmm, 就简单理解为通过插件隐性的操作用户的浏览界面,这个这么说明显风险很大,摆明了就是注入 js 搞事的,可以参考个例子: 永远点不到的百度按钮 注:文章是很久以前瞎抄的图灵社区来着,可能乱七八糟的,就看个效果就行了,本文只是简单的开发,不会使用各类复杂的属性

  • 前面提到的background 属性,有两个可能不是很必要使用的值,一个是 page, 这个是后台配置属性文件,一般需要构建后台页面时才需要。 而 persistent 属性定义了常驻后台的方式(默认为true)——当其值为true时,表示扩展将一直在后台运行,无论其是否正在工作;当其值为false时,表示扩展在后台按需运行,这就是Chrome后来提出的 Event Page, 这个说白了就是按需运行,少耗点内存,不过少耗有什么用呢,chrome嘛,大家都懂的。

实战

开发目标

实现一个访问日志记录,用来记录当前浏览器发出的请求

开发日志 项目地址

  1. 先从基本配置开始, 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "Request Record",
"description": "Generate the request log and group them by address url",
"version": "0.1",
"permissions": [
"debugger",
"tabs"
],
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icon.png",
"default_title": "Request Record"
},
"manifest_version": 2
}

我们定义了这个插件的一些基本属性,包括它的名字,描述,版本号,申请权限(这里申请了debugger权限,因为这个权限级别比较高,可以方便开发使用,实际开发是不推荐使用的,另一个tabs权限自然就是tab页相关了), 以及为其注册了一个后台 js 用来主监听,和默认使用的插件 icon

The chrome.debugger API serves as an alternate transport for Chrome’s remote debugging protocol. Use chrome.debugger to attach to one or more tabs to instrument network interaction, debug JavaScript, mutate the DOM and CSS, etc. Use the Debuggee tabId to target tabs with sendCommand and route events by tabId from onEvent callbacks.

  1. 接下来我们在 background.js 中注册基本的监听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.debugger.attach({tabId:tab.id}, version,
onAttach.bind(null, tab.id));
});

var version = "1.0";

function onAttach(tabId) {
if (chrome.runtime.lastError) {
alert(chrome.runtime.lastError.message);
return;
}

chrome.windows.create(
{url: "headers.html?" + tabId, type: "popup", width: 800, height: 600});
}
  • 上述使用的api都可以在文末的手册链接中找到,这里只做简单的描述,第一个 browserAction.onClicked 会根据你是否已经 popup(就是你点图标后弹出的小窗) 来决定是否触发, 这个监听器会监听到一个 tabId 从而定位目标的 tab 页。

  • 接下来使用 chrome.debugger.attach 使用这个将 debug 目标挂载上,这个 attach 接受三个参数, debuggee 对象, 版本号,和一个回调函数,挂载成功时会触发该回调,失败时会采用类似 C 的异常处理方式,也就是用全局变量去set异常记录。

  • 我们在 debugger 成功挂载上目标对象后,创建我们的主页面,开始我们真正的业务代码开发

  1. 业务相关
  • 前面提到过 chrome, chrome 不允许直接将 js 嵌入文档,所以都是外部引入 js 的,我们这里不引入其他工具型 js 了,直接使用基本的 id 去锁定目标 element 来操作,可以看到总共使用了三部分,一个按钮,用作请求日志的导出,一个 container, 用做基本的渲染展示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<html>
<head>
<style>
body {
font-family: monospace;
word-wrap: break-word;
}

#container {
white-space: pre;
}

.request {
border-top: 1px solid black;
margin-bottom: 10px;
}
</style>
</head>

<body>

<button id="ecl">export</button>
<div id="container"></div>

<script src="headers.js"></script>

</body>
</html>
  • 然后我们在 header.js 中,通过 load event, 为页面加载完成后绑定回调方法,在方法中首先使用 sendCommand 向目标 tab 页发送指令 Network.enable 来启用 web 请求 tracking。在此之后我们开启 debugger 事件监听,通过监听的第二,三个参数可以获取到大部分了想要的数据了(二三个参数分别是 remote debugging protocol 的方法名和方法参数), 至此所有的资源都准备好了,接下来就是依赖这些资源自行进行开发了
1
2
3
4
5
6
7
8
9
10
var tabId = parseInt(window.location.search.substring(1));

window.addEventListener("load", function() {
chrome.debugger.sendCommand({tabId:tabId}, "Network.enable");
chrome.debugger.onEvent.addListener(onEvent);
});

window.addEventListener("unload", function() {
chrome.debugger.detach({tabId:tabId});
});
  • 最后,以我写的简单小插件作为最后 onEvent 监听器样例吧,代码风格很糟,草草起手,加上我专项是后端,前端的原生 js 并不熟悉,所以仅仅加了注释方便参考,前面核心的地方都说了,后面这一点其实可有可无(我就是单纯的根据url进行了个前缀分类然后导出了 csv,而也可以按照http header的状态码(比如403, 懂的都懂)或者任意信息做个类似的),大家按自己的需求开发即可【Dev tools network 的 abilities 都有了还有啥做不出来 -_-】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function onEvent(debuggeeId, message, params) {
if (tabId != debuggeeId.tabId)
return;

if (message == "Network.requestWillBeSent") {
var requestDiv = requests[params.requestId];
if (!requestDiv) {
var requestDiv = document.createElement("div");
requestDiv.className = "request";
requests[params.requestId] = requestDiv;
}

chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, function (tabs) {
// get the function code
var tabUrl = tabs[0].url;
const urlPaths = parseURL(tabUrl).fragment.split("/");

const functionCode = urlPaths[3].toUpperCase() || urlPaths[2].toUpperCase();

// existRequest[tabUrl] = parseURL(params.request.url).path;
// trim the requests and format them to request object which got two attributes: method & url
const requestUrlWithArgs = parseURL(params.request.url).path;
let requestUrl = requestUrlWithArgs;
if(requestUrlWithArgs.indexOf("?") != -1){
requestUrl = requestUrlWithArgs.split("?")[0];
}
//valid url not request for css, js, etc ...
if(indexOfArray(fileUrls, requestUrl)) {
return ;
}

// format number to {id}, a2-b0-cq to {oid}, and a_z to {code}
let requestObject = {};
requestObject.method = params.request.method;
requestObject.url = formatUrl(requestUrl);

// create such a struct: include key & value which key is function code and value is a list contanis unique requests
pushIfNotPresent(existRequest, functionCode, requestObject,
(src, dest) => { return src.method === dest.method && src.url === dest.url; });

var requestLine = document.createElement("pre");
requestLine.textContent = JSON.stringify(existRequest, null, 4);
requestDiv.appendChild(requestLine);

const containerNode = document.getElementById("container");
while (containerNode.firstChild) {
containerNode.removeChild(containerNode.firstChild);
}
containerNode.appendChild(requestDiv);
});

}
}
  • 附一张截图

插件样例图

推荐查询手册

Chrome 插件API文档

Chrome 开发协议相关

懒推广

插件怎么写都说了,以后应该也不会写类似的文章,这里就顺便说下我自己用的一些插件,老实说并不常用,但是偶尔用上就莫名被爽到。

chrome 插件

CATALOG
  1. 1. 如何写一个简易的chrome插件
    1. 1.1. 推荐阅读
    2. 1.2. 概念介绍
      1. 1.2.1. 基本配置
      2. 1.2.2. 重点注意
    3. 1.3. 实战
      1. 1.3.1. 开发目标
      2. 1.3.2. 开发日志 项目地址
    4. 1.4. 推荐查询手册
    5. 1.5. 懒推广