JTP UnLoop 插件分析

前言

这篇文章相当于 Chrome 插件入门, 因为这也是我第一次写 Chrome 插件.

插件项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
|-- LICENSE
|-- README.md
|-- src
| |-- content_scripts # 自定义的脚本目录
| | |-- content_script.js # 主自定义脚本
| | `-- hookLoader.js # xhr拦截逻辑的实现
| |-- images
| | `-- lemon-violet.png # 插件的logo
| |-- libs # 第三方js库
| | |-- ajaxhook.min.js # xhr拦截库
| | |-- bluebird.min.js # Promise库
| | `-- lodash.min.js # JavaScript的数据处理工具库
| `-- manifest.json # 插件配置文件
`-- ...

插件配置文件-manifest.json

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
{
"manifest_version": 2,
"name": "Jira Team Plan UnLoop",
"version": "0.0.1",
"description": "用于修复在jira中使用Jira Team Planning时查询项目列表时进入无线循环导致CPU使用率升高卡顿的问题.",
"permissions": ["http://example.com:8080/secure/TeamPlanning.jspa", "storage", "webRequest", "webRequestBlocking"],
"icons": {
"16": "images/lemon-violet.png",
"48": "images/lemon-violet.png",
"128": "images/lemon-violet.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": [
"/libs/bluebird.min.js",
"/libs/lodash.min.js",
"/libs/ajaxhook.min.js",
"/content_scripts/content_script.js",
"/content_scripts/hookLoader.js"
],
"all_frames": true,
"run_at": "document_end"
}
]
}

对于关键的字段做一下介绍:
permissions: 插件所需申请的权限列表, 这里可以看到其中有个 url, 就是代表党匹配了这个 url 之后, 插件才会正常工作
content_scripts: 这是一个数组, 每一个元素代表在规定的条件时做规定的 js 注入
content_scripts[0].matches: 当 url 与规则匹配时, 触发这个 content_scripts 的操作内容
content_scripts[0].js: 需要注入的 js
content_scripts[0].all_frames: false 为在仅在页面顶部框架中操作, true 为在页面所有框架中操作
content_scripts[0].run_at: 指定在 document 的某个状态时注入脚本, 具体看官方说明

主自定义脚本-content_script.js

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 注入来至本扩展的js
const scriptLoaderByExt = extJsPath => {
if (extJsPath) {
return new Promise((resolve, reject) => {
if (chrome.runtime.getURL) {
const theScript = document.createElement('script');
const src = chrome.runtime.getURL(extJsPath);
console.log('src: ', src);
theScript.src = src;
theScript.onload = () => {
resolve(theScript);
};
theScript.onerror = () => {
reject(`load ${src} failed`);
};
document.querySelector('*').appendChild(theScript);
} else {
reject('没有可插入的脚本.');
}
});
}
};

// 注入来至互联网上的js或来至HTML脚本
const scriptLoaderByWeb = ({ src, innerHTML }) => {
if (src) {
return new Promise((resolve, reject) => {
const theScript = document.createElement('script');
theScript.src = src;
theScript.onload = () => {
resolve(theScript);
};
theScript.onerror = () => {
reject(`load ${src} failed`);
};
document.querySelector('*').appendChild(theScript);
});
}
const theScript = document.createElement('script');
theScript.innerHTML = innerHTML;
document.body.appendChild(theScript);
return theScript;
};

// 定义主函数
const main = async () => {
try {
// 注入第三方库脚本和自定义脚本到当前页面
console.log('插入脚本...');
// 注入Promise库
await scriptLoaderByExt('libs/bluebird.min.js');
// 注入工具库
await scriptLoaderByExt('libs/lodash.min.js');
// 注入xhr拦截库
await scriptLoaderByExt('libs/ajaxhook.min.js');
// 注入自定义的主脚本, 此步理论上可以不注入
await scriptLoaderByExt('content_scripts/content_script.js');
// 注入xhr拦截器逻辑函数
await scriptLoaderByExt('content_scripts/hookLoader.js');
console.log('脚本插入完毕.');
// 注入一段HTML内的脚本
// 1. 用于初始化全局变量
// 2. 用于运行刚刚注入的hookLoader.js中提供拦截器逻辑函数
scriptLoaderByWeb({
innerHTML: `
let blacklistUrl = [];
let projectCount = 0;
hookLoader();
`
});
} catch (err) {
console.log('err: ', err);
}
};

// 运行主函数
main();

以上注释已经分析了 content_script.js 脚本的主要功能

xhr 拦截器逻辑函数-hookLoader.js

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
const hookLoader = () => {
hookAjax({
onreadystatechange: function(xhr) {
/** 当查询project列表的数据有返回时, 计算出当前的project总数
** project总数将用于限制黑名单对比的次数, 这样可以解释对黑名单进行对比的次数, 已减少对比黑名单产生的不必要消耗
**/
if (xhr.responseURL.match('rest/api/2/project\\?&_') && xhr.response.length > 0) {
try {
const projectArray = JSON.parse(xhr.response);
if (projectArray.constructor === Array) {
projectCount = projectArray.length;
}
} catch (err) {
console.log('projectArray err: ', err);
}
}
},
onload: function(xhr) {},
open: function(arg, xhr) {
// 30秒清除一次黑名单, 防止因为黑名单原因导致后期真实想请求数据时被无辜拦截
if (projectCount !== 0 && blacklistUrl.length >= projectCount) {
setTimeout(() => {
if (projectCount !== 0) {
blacklistUrl = [];
projectCount = 0;
}
}, 30000);
return true;
}
const url = arg[1];
// 匹配是否为project权限查询, 如果是且不在黑名单中则加入黑名单, 如果在黑名单中则拦截请求
const blackUrl = _.indexOf(blacklistUrl, url.replace(/&_=[0-9]{13}/, ''));
if (blackUrl > -1) {
return true;
} else {
if (url.match('rest/api/2/mypermissions.*projectKey=')) {
blacklistUrl.push(url.replace(/&_=[0-9]{13}/, ''));
}
}
}
});
};

以上注释已经分析了 hookLoader.js 脚本的主要功能

插件调试

这里没有上网查插件调试的方法, 讲的是我最笨的调试方法, 相信网上应该有更好的调试方法.

  1. 当 content_scripts 中的规则运行成功后, 不严格的来说, 会走 content_script.js 脚本(因为只有这个脚本里面有执行动作, 其它脚本仅是方法定义等)
  2. 所以只要在 content_script.js 脚本中用 console.log 来打印数据, 就可以在 Console 中开始调试插件了
  3. 如果想调试的时候查看某个对象有哪些属性和方法, 可以将这个对象赋值给 window.someObj(其中这个 someObj 仅是一个例子, 可以取任意名字)
  4. 然后在 Console 中输入 window.someObj 就可以查看到这个对象的具体内容, 非常方便.

参考

Chrome extensions development
Chrome 插件(扩展)开发全攻略
demo for inject_script_extension
lodash.js 官网
lodash.js 中文网
bluebird.js
Ajax-hook
Tempo Team Planning 死循环分析与解决思路
Tempo Team Planning 死循环 bug 解决方案

显示 Gitment 评论