# 第一个 Chrome 扩展

# 背景

一直以来我就想学习怎样写 Chrome 扩展,然后做一个出来,但是一直没有好的想法,不知道做什么。最近入职了一家新公司,每个公司都有每个公司的开发流程,这家公司前端开发需要读取路由、按钮、后台接口的权限配置,生成一个 JSON 文件,然后把这个 JSON 文件上传到另一个系统里(简称权限系统),所以开发的时候我们经常这么做:

  1. 打开调试
  2. 打开 "console" 页签
  3. 输入"window.getPermissions()",这个函数将生成权限 JSON 文件,并打印在控制台
  4. 在控制台 copy 上一步打印的 log
  5. 打开权限系统,把上一步 copy 的权限 JSON 上传

如果权限经常修改,我们就要不停地重复上面 5 个步骤,然后我就在想能不能写一个 Chrome 扩展,通过右键菜单直接 copy 权限 JSON 文件,并打开权限系统,这样就只需一个简单的动作就完成了上面 5 个步骤,大大提高了开发效率。于是乎我的第一个 Chrome 扩展就这样产生了。

# 基本结构

下面是这个扩展程序的目录结构

├── background.js
├── images
|  ├── icon128.png
|  ├── icon16.png
|  ├── icon32.png
|  └── icon48.png
├── itrus-permission.js
└── manifest.json
1
2
3
4
5
6
7
8

Chrome 扩展程序最重要的是 Manifest 文件,现在是 v3 版本。Manifest 是一个 JSON 文件,描述了 Chrome 扩展的功能和配置,里面有很多的配置项,下面是我这个扩展程序用到的一些配置,更多详情请参考 Manifest file format (opens new window)

现在网络上关于 Chrome 扩展的文章大部分还是 v2 版本,Chrome 计划将在 2024 年 6 月开始停用 Chrome 的不稳定版本中的 Manifest v2 扩展程序,所以这里我们使用 v3 版本的 Manifest。

{
  "name": "iTrus Extensions",
  "description" : "浏览器插件",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "permissions": ["contextMenus", "scripting"],
  "host_permissions": ["<all_urls>"],
  "icons": {
    "16": "images/icon16.png",
    "32": "images/icon32.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# name

扩展程序的名称,最多 45 个字符。

# description

扩展程序的描述,最多 132 个字符。

# manifest_version

manifest 版本号,当前是 3。

# background

格式:{ "service_worker": "xxx.js", type: "module" }

background.service_worker 指定扩展程序的 Service Worker 的 JavaScript 文件,可以把它当做扩展程序的主应用,它在被需要时加载,在空闲时被关闭,它负责与其它页面进行通信,接收消息,处理消息等。更多详情请参考 Extension Service Workers (opens new window)

background.type: "module" 表示我们可以将其它模块导入到 Service Worker 中,这里有一个这样的例子 (opens new window)

# permissions

允许使用哪些扩展 API,各个 API 的参考页面列出了它们所需的权限,这里我们要使用 contextMenusscripting,更多详情请参考 Permissions (opens new window)

# host_permissions

允许扩展可以访问哪些网页内容,可以使用网址匹配模式 (opens new window)

匹配模式使用以下结构指定一组网址:

<scheme>://<host>/<path>
1

scheme:必须是以下内容之一,并使用双斜线 (//) 与格式的其余部分分隔开:

  • http
  • https
  • file
  • 通配符 *,仅与 httphttps 匹配

host:主机名 (www.example.com)。支持通配符 *,比如 *.example.com 或者仅使用通配符 *。 如果您使用通配符 *,它必须是第一个或唯一的字符,并且后面必须跟一个句点 (.) 或正斜杠 (/)。

path:必须至少包含正斜杠。斜杠本身会匹配任何路径,就如同它后跟一个通配符 (/*) 一样。

使用 "<all_urls>" 表示匹配 scheme 允许的所有网址,[https://*/* , http://*/* ] 匹配 http/https 所有网址,更多详情请参考匹配模式 (opens new window)

关于 host_permissions 的更多详情,请参考 Host Permissions (opens new window)

Chrome 推荐使用 "activeTab" 代替 host_permissions ,更多详情请参考 The activeTab permission (opens new window)

# icons

一个或多个图标,不同大小的图标用在不同的地方,推荐使用 .png 格式的图标,虽然官方文档说支持别的格式,但是我开发的时候,发现我的 .jpeg 不起作用, 更多详情请参考 Icons (opens new window)

# 加载和调试扩展程序

在开发的过程中我们想看看实现效果,我们需要先加载我们的扩展程序。打开 Chrome 扩展管理,然后打开 "开发者模式",点击 "加载已解压的扩展程序",选择我们的扩展程序

然后可以看到我们的扩展程序了,如果我们改了扩展程序的代码,需要手动刷新

如果要调试我们创建的 background service worker,点击上面的 "Service Worker",将会打开浏览器调试工具

如果扩展程序运行有错误,在浏览器扩展上会出现 "错误" 按钮,点击这个按钮可以看到错误的详细信息

# 创建右键菜单

首先我们需要在插件安装的时候创建右键菜单,可以使用 chrome.runtime.onInstalled (opens new window) 生命周期钩子,通过 chrome.contextMenus.create (opens new window) 在 Service Worker 里创建右键菜单

chrome.contextMenus 需要 contextMenus 权限

// background.js
// chrome.runtime.onInstalled 插件安装是生命周期钩子
chrome.runtime.onInstalled.addListener(function () {
  // chrome.contextMenus.create 创建右键菜单
  chrome.contextMenus.create({
    id: "top",
    title: "iTrus",
    contexts: ["all"]
  });
  chrome.contextMenus.create({
    id: "permission",
    parentId: "top",
    title: "➹ Get Permission",
    contexts: ["all"]
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建右键菜单时,需要指定菜单的 idtitle ,右键菜单支持嵌套结构,通过 parentId 指向父菜单的 idcontexts 表示在什么样的上下文(context)中才显示菜单,默认是 "page" 表示页面,all 表示所有场景都显示菜单,更多详情请参考 ContextType (opens new window)

然后通过 chrome.contextMenus.onClicked (opens new window) 添加右键菜单响应函数,响应函数有两个参数:infotabinfo 包含菜单和上下文(context)的信息, tab 包含浏览器 tab 页签信息。

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "permission") {
    chrome.scripting.executeScript({
      target: {
        tabId: tab.id, 
        allFrames: true,
      },
      world: "MAIN",
      files: ["itrus-permission.js"]
    }, result => {
      console.log("result", result);
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在右键菜单响应函数里,通过 chrome.scripting.executeScript 注入执行脚本

# 注入脚本

因为我们要调用宿主的 window.getPermission() 函数,所以需要注入脚本。Chrome 扩展提供了三种注入脚本的方法

因为我们需要响应右键菜单,所以采用编码注入

chrome.scripting.executeScript({
  target: {
    tabId: tab.id, 
    allFrames: true,
  },
  files: ["itrus-permission.js"],
  world: "MAIN",
}, result => {
  console.log("result", result);
});
1
2
3
4
5
6
7
8
9
10

target (opens new window):指定脚本注入的目标,target.tabId 指定具体的 tab 页签,target.allFrames: true 表示脚本被注入的页面的所有 iframe。

files:注入的 JS 文件的路径,相对于扩展的根目录。

world (opens new window):这个配置很关键,它有两个值 "ISOLATED""MAIN",默认值是 "ISOLATED",表示隔离环境,这是扩展特有的执行环境,可以使用 Chrome 扩展 API,但是不能访问宿主环境特有数据,比如不能访问我们在宿主环境里给 window 添加的 getPermission 函数;"MAIN" 表示与宿主页面的 JavaScript 共享的执行环境,因此它可以宿主环境特有数据,但是不能使用 Chrome 扩展 API。更多详情请参考 Work in isolated worlds (opens new window)

# 获取和复制数据

接下来我们就要在 itrus-permission.js 里获取和复制数据

# 获取数据

// itrus-permission.js
if (window.getPermissions) {
  const data = window.getPermissions();
  if (data) {
    copyTextToClipboard(data);
  } else {
    console.log("error: no permission data");
  }
} else {
  console.log("error: no getPermissions function");
}
1
2
3
4
5
6
7
8
9
10
11

# 复制数据到 Clipboard

复制文本到 clipboard,有两种方式:

因为本地开发有时候使用 http://ip:port 的方式,所以我们使用 Clipboard API + document.execCommand() 回退的方式,经测试 document.execCommand() 在 Chrome 118 还可以使用

如果使用 http 协议,同时浏览器不支持 document.execCommand(),该怎么实现复制数据到 clipboard 呢?目前我没找到解决方案

// 使用 Clipboard API
function copyTextToClipboard(text) {
  if (!navigator.clipboard) {
    fallbackCopyTextToClipboard(text);
    return;
  }
  navigator.clipboard
    .writeText(text)
    .then(() => {
      window.open("http://192.168.100.230:1889", "_blank");
    })
    .catch(err => {
      console.log("navigator.clipboard failed", err);
      fallbackCopyTextToClipboard(text);
    });
}

// 使用 document.execCommand("copy")
function fallbackCopyTextToClipboard(text) {
  const input = document.createElement("input");
  input.value = text;
  document.body.appendChild(input);
  input.select();
  try {
    const successful = document.execCommand("copy");
    if (successful) {
      window.open("http://192.168.100.230:1889", "_blank");
    } else {
      console.log("execCommand call failed");
    }
  } catch (err) {
    console.error("execCommand failed", err);
  }
  document.body.removeChild(input);
}
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

至此,我的第一个 Chrome 扩展完成了,实现效果如下:

# References