开发一个 chrome 浏览器插件

我们来学习开发一个 chrome 浏览器插件,更加详情的教程说明,可以查看

首先,必须先有一个入口页面,这里我们定义为 popup.html,在这个入口页中,我们可以写入打开插件时的展示的页面内容。

在这个入口 html 页面中,我们可以引入一些自定义的资源文件,如 jscss 文件,来实现完善交互和美化界面的目的。

chrome 浏览器是如何找到这个入口页面的呢?我们来认识一下 chrome 浏览器插件的必要配置文件 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
"name": "WebApe-2.6 API Test Main",
"description": "WebApe2.6的api测试工具,方便web开发与后端接口联调和测试人员进行接口测试工作,浏览器需Chrome 88+,快捷打开方式:Ctrl+Shift+Y",
"version": "1.2",
"manifest_version": 3,
"action": {
"default_popup": "popup.html",
"default_icon": "assets/img/logo.png"
},
"options_page": "popup.html",
"icons": {
"16": "assets/img/logo.png",
"32": "assets/img/logo.png",
"48": "assets/img/logo.png",
"128": "assets/img/logo.png"
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"mac": "MacCtrl+Shift+Y"
},
"description": "Opens popup.html"
}
},
"permissions": [
"storage",
"declarativeContent",
"activeTab",
"scripting",
"tabs"
],
"background": {
"service_worker": "src/background/worker.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content/script.js"],
"run_at": "document_idle"
},
{
"matches": ["<all_urls>"],
"js": ["lib/bundle.js"],
"run_at": "document_idle"
}
]
}

以上的配置,我们需要注意几个重要参数:

  • name:插件名称
  • manifest_version:适配插件的版本,目前主要是 3
  • action:定义了入口文件和图标
  • permissions:需要使用到的权限
  • background:后台可执行的 js 文件,相当于插件的服务端
  • content_scripts:默认加载插件时,会自动加载到页面中的脚本文件

上面我们常用的权限有:

  • activeTab:允许插件在当前标签页执行操作。
  • storage:允许插件访问 Chrome 浏览器的本地存储。
  • tabs:允许插件访问和修改浏览器标签页。
  • cookies:允许插件读取和修改浏览器的 cookie 数据。
  • webRequest:允许插件监视和修改网络请求。
  • webNavigation:允许插件获取浏览器导航信息。
  • notifications:允许插件创建通知。
  • identity:允许插件获取用户的身份信息。
  • contextMenus:允许插件添加右键菜单项。
  • unlimitedStorage:允许插件请求无限制的存储空间。

于是我们可以定义这样的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
www
├─assets 静态资源文件
├─dist 构建目的目录
├─lib 扩展包文件目录
├─src 代码主目录
│ ├─background 后端代码
│ │ └─worker.js
│ ├─business 业务代码
│ │ └─popup.js
│ └─content 内容加载代码
│ └─script.js
├─.gitignore git忽略配置文件
├─main.js npm执行主文件
├─manifest.json 插件配置文件
├─package-lock.json npm锁文件
├─package.json npm包文件
└─popup.html 弹出静态html

这里大家也可以看到,这里我使用了 node 去辅助生成我的 chrome 插件,也可以不用,这里我只是想可以一键压缩及去掉代码中的注释才加上去的。

其中 main.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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const fs = require("fs");
const path = require("path");
const { promisify } = require("util");

const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const mkdir = promisify(fs.mkdir);
const rmdir = promisify(fs.rm);

const { minify: minifyJS } = require("uglify-js");
const CleanCSS = require("clean-css");
const htmlMinifier = require("html-minifier");

const distDir = "dist";

// 检查创建目录
async function checkAndCreateDistDir(dir) {
try {
const stats = await stat(dir);
if (stats.isDirectory()) {
await rmdir(dir, { recursive: true });
}
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
}

await mkdir(dir);
}

// 拷贝文件
async function copyFiles(dir) {
const filesToCopy = [
"assets",
"lib",
"src",
"manifest.json",
"popup.html",
"web_cmd_x86.o",
"web_cmd_arm.o",
];

const copyFile = async (source, target) => {
const contents = await readFile(source);
await writeFile(target, contents);
};

const copyFolder = async (source, target) => {
await mkdir(target, { recursive: true });
const files = await readdir(source);

for (const file of files) {
const current = path.resolve(source, file);
const dest = path.resolve(target, file);
const stats = await stat(current);

if (stats.isDirectory()) {
await copyFolder(current, dest);
} else {
await copyFile(current, dest);
}
}
};

for (const file of filesToCopy) {
const sourcePath = path.resolve(file);
const destPath = path.resolve(dir, file);

const stats = await stat(sourcePath);
if (stats.isDirectory()) {
await copyFolder(sourcePath, destPath);
} else {
await copyFile(sourcePath, destPath);
}
}
}

// 压缩处理
function minifyFiles(dir) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
minifyFiles(filePath);
} else {
const ext = path.extname(file);

if (ext === ".js") {
let code = fs.readFileSync(filePath, "utf8");
const { error, code: minifiedCode } = minifyJS(code);

if (!error) {
fs.writeFileSync(filePath, minifiedCode);
console.log(`Minified ${filePath}`);
} else {
console.error(`Error minifying ${filePath}: ${error}`);
}
} else if (ext === ".css") {
let code = fs.readFileSync(filePath, "utf8");
const minifiedCode = new CleanCSS().minify(code).styles;

fs.writeFileSync(filePath, minifiedCode);
console.log(`Minified ${filePath}`);
} else if (ext === ".html") {
let code = fs.readFileSync(filePath, "utf8");
const minifiedCode = htmlMinifier.minify(code, {
collapseWhitespace: true,
removeComments: true,
minifyJS: true,
minifyCSS: true,
});

fs.writeFileSync(filePath, minifiedCode);
console.log(`Minified ${filePath}`);
}
}
});
}

async function main() {
try {
await checkAndCreateDistDir(distDir);
await copyFiles(distDir);
minifyFiles(path.join(__dirname, "dist"));
console.log("Task completed successfully.");
} catch (err) {
console.error("An error occurred:", err);
}
}

main();

package.json 文件中的内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "ts2.6-api-test",
"version": "1.1.2",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"main": "node main.js"
},
"author": "webape@qq.com",
"license": "ISC",
"description": "WebApe2.6的接口测试chrome插件",
"devDependencies": {
"clean-css": "^5.3.3",
"html-minifier": "^4.0.0",
"uglify-js": "^3.19.3"
}
}

如何来实现插件功能呢?这里我们来写一个基本的示例,在 popup.html 中加入一个按钮

1
<button @click="buttonClick()">点击事件</button>

在引入的 business 业务代码中的 popup.js 中有 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
function buttonClick() {
chrome.runtime.sendMessage(
{
action: "ifWebApe",
},
function (response) {
console.log("ifWebApe-response", response);
if (!response.backData.length || !response.backData[0].result) {
layer.alert(
"当前页签不是WebApe的页面,请先打开WebApe的页面并登录后再使用插件!",
{
title: "提示",
closeBtn: 0,
btn: [],
success: function (layero, index) {
// 监听警告框的 close 事件
layero.on("close", function (e) {
// 阻止默认行为,即不允许关闭
e.preventDefault();
e.stopPropagation();
});
},
}
);
}
}
);
}

在上面的代码中,chrome.runtime.sendMessage 就是向后端接口发送了一个消息,现在我们来看后端如何接收与返回

点击查看完整代码
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
function ifWebApeFunction() {
return document.getElementsByClassName("slimContent");
}
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.action === "ifWebApe") {
chrome.tabs.query(
{ active: true, currentWindow: true },
function (tabs) {
chrome.scripting
.executeScript({
target: { tabId: tabs[0].id },
func: ifWebApeFunction,
args: [],
})
.then((res) => {
sendResponse({ backData: res });
})
.catch((err) => {
sendResponse({ backData: err });
});
}
);
}

// 保持消息通道打开以便异步发送响应
return true;
});

至于在我们的内容代码中,因为直接插入到了打开的网页之中,可以立即检测,如在 content/script.js

点击查看完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 检测版本
function getChromeVersion() {
var arr = navigator.userAgent.split(" ");
var chromeVersion = "";
for (var i = 0; i < arr.length; i++) {
if (/chrome/i.test(arr[i])) chromeVersion = arr[i];
}
if (chromeVersion) {
return Number(chromeVersion.split("/")[1].split(".")[0]);
} else {
return false;
}
}
if (getChromeVersion()) {
var version = getChromeVersion();
if (version < 88) {
alert(
"你使用的谷歌浏览器版本过低,为了更好地体验请将浏览器升级到最新>=88版本!"
);
}
}