chrome 插件开发指南(Manifest V3)

10,943 阅读12分钟

一、什么是 Chrome插件

1.1 概述

严格来讲,我们正在说的东西应该叫 Chrome 扩展(Chrome Extension),真正意义上的 Chrome 插件是更底层的浏览器功能扩展,需要对浏览器源码有一定掌握才有能力去开发。鉴于 Chrome 插件的叫法已经习惯,本文中也全部采用这种叫法。在百度指数里也没有收录“chrome 扩展”这个词,只有“chrome 插件”。

Chrome 插件是一个用 Web 技术开发、用来增强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。

另外,其实不只是前端技术,Chrome 还可以配合 C++ 编写的 dll 动态链接库实现一些更底层的功能(NPAPI),比如全屏幕截图等。

由于安全原因,Chrome 浏览器42以上版本已经陆续不在支持 NPAPI 插件,取而代之的是更安全的 PPAPI。

1.2 Chrome 插件是如何工作的

扩展是基于诸如 HTML、 JavaScript 和 CSS 之类的 Web 技术构建的。它们运行在一个独立的沙箱执行环境中,并与 Chrome 浏览器进行交互。

image.png
从图中可以看出,存在三个进程:扩展进程(Extension Process)、页面渲染进程(Render Process)、浏览器进程(Browser Process)。
1)扩展进程中运行Extension Page,Extension Page主要包括backgrount.html和popup.html:

  • backgrount.html中没有任何内容,是通过background.js创建生成,当浏览器打开时,会自动加载插件的background.js文件,它独立于网页并且一直运行在后台,它主要通过调用浏览器提供的API和浏览器进行交互;
  • popup.html则不同,它有内容,是一个实实在在的页面,和我们普通的web页面一样,由html、css、Javascript组成,它是按需加载的,需要用户去点击地址栏的按钮去触发,才能弹出页面。

2)渲染进程主要运行Web Page,当打开页面时,会将content_script.js加载并注入到该网页的环境中,它和网页中引入的Javascript一样,可以操作该网页的DOM Tree,改变页面的展示效果;
3)浏览器进程在这里更多起到桥梁作用,作为中转可以实现Extension Page和content_script.js之间的消息通信。

1.3 延伸阅读:插件和扩展的区别

扩展(Extension)指的是通过调用 Chrome 提供的 Chrome API 来扩展浏览器功能的一种组件,工作在浏览器层面,使用 HTML + Javascript 语言开发。比如著名的 AdBlock plus。

插件(Plug-in)指的是通过调用 Webkit 内核 NPAPI 来扩展内核功能的一种组件,工作在内核层面,理论上可以用任何一种生成本地二进制程序的语言开发,比如C/C++、Delphi等。比如Flash player插件,就属于这种类型。一般在网页中 object 或者 embed 标签声明的部分,就要靠插件来渲染。

从安全性上来看,由于插件一般实现的都是比较底层的功能,所以一旦出现问题,往往就会牵涉到整个操作系统,像 Flash 就属于经常被扒出高危漏洞的那一类。相比较之下,扩展出现问题,其危害性往往类似于浏览器漏洞。不过 Chrome Extension 在为用户带来便利的同时,也的确带来了不少安全问题,即便是在 Chrome 应用商店中的应用也不能保证绝对安全,Google 自己也下线过一些有安全隐患的扩展。

二、Chrome 插件能做什么

image.png
扩展允许你通过使用 API 修改浏览器行为和访问网页内容来“扩展”浏览器。
扩展 API 允许扩展的代码访问浏览器本身的特性: 激活选项卡、修改网络请求等等。

插件能力概述:

API备注
自定义扩展用户界面控制一个扩展的显示的图标Action
添加触发操作的键盘快捷键Commands
添加页面右键菜单Context Menus
向地址栏添加关键字功能Omnibox
创建新标签卡、书签页或历史记录页Override Pages
在工具栏中动态显示图标。Page Actions
构建扩展工具无障碍扩展服务Accessibility (a11y)
有趣的事情发生时,做出检测和反应Service Workers
使用语言和语言环境Internationalization
获得OAuth2访问令牌Identity
管理已安装和正在运行的扩展插件Management
通过 Content Script 与其父扩展进行通信Message Passing
让用户自定义扩展Options Pages
修改一个扩展的权限Permissions
存储和检索数据Storage
修改和监听 Chrome 浏览器创建、组织和操作书签行为Bookmarks
从用户的本地配置文件中删除浏览数据Browsing Data
以编程方式启动、监视、操作和搜索下载Downloads
管理 Chrome 的字体设置Font Settings
与浏览器访问页面的记录交互History
控制 Chrome 的隐私特性Privacy
管理 Chrome 的代理设置Proxy
从浏览会话查询和还原选项卡和窗口Sessions
在浏览器中创建、修改和重新排列选项卡Tabs
访问用户访问次数最多的 URLTop Sites
更改浏览器的整体外观Themes
在浏览器中创建、修改和重新排列窗口Windows
修改和监听网页扩展临时访问当前活动选项卡的权限Active Tab
自定义网站特性,如 cookies、 JavaScript 和插件Content Settings
在网页上下文中运行 JavaScript 代码Content Scripts
浏览和修改浏览器的 Cookie 系统Cookies
使用 XMLHttpRequest 从远程服务器发送和接收数据Cross-Origin XHR
在不需要许可的情况下对页面内容执行操作Declarative Content
捕获屏幕、单个窗口或选项卡的内容Desktop Capture
将选项卡的源信息保存为 MHTMLPage Capture
与标签页互动媒体流交互Tab Capture
接收 in-flight 导航请求状态的通知Web Navigation
提供规则告诉 Chrome 如何拦截、阻止或修改 in-flight 的请求。Declarative Net Request
打包、部署和更新使用 Chrome Web Store 托管和更新扩展Chrome Web Store
在指定的网络或其他软件上分发扩展Other Deployment Options
扩展 Chrome DevTools测试网络交互,调试 JavaScript,修改 DOM 和 CSSDebugger
为 Chrome 开发工具添加功能Devtools

几个插件例子:

image.pngimage.pngimage.pngimage.png
Google 翻译Adblock PlusArtemis and BritomartisOctotree - GitHub code tree)

一句话总结:Chrome扩展插件是用前端的技术栈,来定制浏览器的功能,改善用户体验

三、主要构成

image.png 一个chrome插件通常由3类文件组成:

  1. 配置文件 manifest.json
  2. 图片、css等资源文件
  3. js脚本文件,包括popup.js、background.js、content_script.js等

3.1 Manifest.json

每一个扩展都有一个json格式的清单文件,用于配置扩展的名称、版本号、图标、权限、脚本路径等信息;
文件内容如下所示:

{
  "manifest_version": 3,
  "name": "MStars",
  "description": "A chrome extension for sgfe",
  "options_page": "options.html",
  "background": {
    "service_worker": "background.bundle.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": "master-34.png"
  },
  "chrome_url_overrides": {
    "newtab": "newtab.html"
  },
  "icons": {
    "128": "master.png"
  },
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*",
        "<all_urls>"
      ],
      "js": [
        "contentScript.bundle.js"
      ],
      "css": [
        "content.styles.css"
      ]
    }
  ],
  "web_accessible_resources": [
    {
      "resources": [
        "injectScript.bundle.js",
        "content.styles.css",
        "master.png",
        "master-34.png",
        "vs/*"
      ],
      "matches": [
        "http://*/*",
        "https://*/*",
        "<all_urls>"
      ]
    }
  ],
  "permissions": [
    "webRequest",
    "storage",
    "contextMenus",
    "bookmarks"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "content_security_policy": {
    "extension_pages": "script-src 'self';object-src 'none'"
  }
}

3.1.1 Manifest V2

自2022年1月17日起,Chrome 应用商店已经停止接受新的 Manifest V2扩展。
2023年6月 Chrome 115开始,关闭对 Manifest V2扩展的支持。
2024年1月,Chrome 应用商店将删除所有的 Manifest V2扩展。
image.png
(Manifest V2 support timeline)

3.1.2 Manifest V3

2020年年底推出V3版本,在安全性、隐私性和性能方面得到了增强;还可以使用更现代的开放 Web 技术,比如 service workspromises

Manifest V3 is available beginning with Chrome 88, and the Chrome Web Store begins accepting Manifest V3 extensions in January 2021.

详细文档:developer.chrome.com/docs/extens…

3.2 popup

3.2.1 概述

popup 是点击插件图标时打开的一个页面,点击 popup 之外的区域会收起,一般用来做一些临时性的交互。
image.png
(Web Vitals)

3.2.2 简单例子

1、 创建 manifest.json 文件:

{
  "manifest_version": 3,
  "name": "Hello Extensions",
  "description": "Base Level Extension",
  "version": "1.0",
  "action": {
    "default_popup": "hello.html",
    "default_icon": "hello_extensions.png"
  }
}

2、创建 html 文件:

<html>
  <body>
    <h1>Hello Extensions</h1>
  </body>
</html>
  1. 点击 action 后,就会看到:
    image.png

3.3 content-scripts

3.3.1 概述

content-scripts 是在网页上下文中运行的文件。通过使用标准的文档对象模型(Document Object Model,DOM) ,它们能够读取浏览器访问的网页的详细信息,对它们进行更改。

如主题、样式、布局定制、广告拦截等等。

大部分浏览器插件都在围绕 content-scripts 做一些事情,background、popup、options等都为之服务。
配置示例:

"content_scripts": [
  {
    "matches": [
      "http://*/*",
      "https://*/*",
      "<all_urls>"
    ],
    "js": [
      "contentScript.bundle.js"
    ],
    "css": [
      "content.styles.css"
    ]
  }
],

3.3.2 特性

1)和原始页面共享 DOM,但是不能共享 JS,不能访问页面中的 JS(比如变量)。
2)网络请求受到同源策略限制。
3)只能访问以下 Chrome API:

其他 API 都不能直接访问,但是可以通过通信让 background 来进行调用。

3.3.3 简单例子

在页面中增加一个按钮。

function inJectBtn() {
  if (document.querySelector('#openInIDE')) return;
  document
    .querySelector('.repo-detail-base-info .btn-box')
    .insertAdjacentHTML(
      'beforeend',
      '<button id="openInIDE" type="button" class="mtd-btn mtd-btn-warning"><span><div class="mtd-button-content"><span class="mtdicon mtdicon-link-o"></span><span>Open In WebIDE</span></div></span></button>'
    );
  document.querySelector('#openInIDE').addEventListener('click', () => {
    window.open('https://xxx.com/');
  });
}
function init() {
  var heartBeat = setInterval(() => {
    var wantedEl = document.querySelector('.repo-detail-base-info .btn-box');
    if (wantedEl) {
      clearInterval(heartBeat);
      inJectBtn();
    }
  }, 1000);
}

init();

3.3.4 延伸阅读:如何访问页面中的 JS

content-scripts 不能访问页面中的 js,它可以操作 DOM,但是 DOM 却不能调用它,所以就有了通过 DOM 操作的方式向页面动态注入 JS 的操作。

function injectJs(jsPath) {
  jsPath = jsPath || 'injectScript.bundle.js';
  var temp = document.createElement('script');
  temp.setAttribute('type', 'text/javascript');
  // chrome-extension://mapfodeofmlldcgdgahpjiefememgeei/injectScript.bundle.js
  temp.src = chrome.runtime.getURL(jsPath);
  temp.onload = function () {
    // 放在页面不好看,执行完后移除掉
    this.parentNode?.removeChild(this);
  };
  document.head.appendChild(temp);
}


injected 的内容需要在资源列表中进行声明:

"web_accessible_resources": [
  {
    "resources": [
      "injectScript.bundle.js",
    ],
    "matches": [
      "http://*/*",
      "https://*/*",
      "<all_urls>"
    ]
  }
]

例子:拦截 xhr 请求。

// "injectScript.bundle.js"
xhook.after(function (request, response) {
  if (request.url.match(/rest\/api.+files.+/)) {
    console.log(response?.data)
  }
});

3.4 background

3.4.1 概述

插件是基于事件的用来修改或增强 Chrome 浏览体验的程序。事件是浏览器触发的,例如导航到新页、删除书签或关闭选项卡。插件在 background 中监视这些事件,然后根据指定的指令进行响应。
background 一旦加载完成,只要执行某个操作(比如发起网络请求或调用 Chrome API)就会一直运行,此外,在关闭所有可见视图和所有消息端口之前,不会被卸载。

总而言之,background 为所有的视图和消息端口服务。

background 在需要的时候被加载,空闲的时候被卸载。一些事件的例子包括:

  • 插件首次安装或者更新版本;
  • backgroud 正在监听一些事件,这些事件被触发;
  • content script 或者其他扩展发送消息;
  • 插件中的其他视图调用 runtime.getBackgroundPage

3.4.2 简单例子

创建一个右键菜单。

function createContextMenus() {
  chrome.contextMenus.create({
    type: 'normal',
    id: 'savePage',
    title: '保存页面',
    checked: false,
  });
}
chrome.runtime.onInstalled.addListener(() => {
  createContextMenus();
});

四、其他展现形式

4.1 homepage_url

插件主页,免费广告位!
image.png

{ "homepage_url": "https://km.xxxx.com", }

4.2 Options页面

4.2.1 概述

插件的配置页面。

4.2.2 配置

有两种配置方法,对应两种展现形式。

{
  // 方式1 
  "options_page": "options.html",
  // 方式2 优先级高
  "options_ui": {
    "page": "options.html"
  },
}
image.pngimage.png

4.3 Devtools

开发者工具。

devtools1.pngdevtools2.png

4.4 override(覆盖特定页面)

可以将 Chrome 默认的一些特定的页面改为使用扩展提供的页面:

页面名称url配置备注
新标签页chrome://newtab"chrome_url_overrides":
{
"newtab": "newtab.html"
}
历史记录chrome://history"chrome_url_overrides":
{
"history": "history.html"
}
书签chrome://bookmarks"chrome_url_overrides":
{
"bookmarks": "bookmarks.html"
}

注意点:

  1. 不能替换隐身模式下的新标签页;
  2. 一个插件只能覆盖一个页面。

4.5 Omnibox

Chrome和其他浏览器相比一个最大的区别就是地址栏——其实不仅仅是地址栏,而是一个多功能的输入框,Google将其称为omnibox(中文为“多功能框”)。我们熟悉的一个功能就是用户可以直接在omnibox搜索关键字,Chrome也将omnibox开放给开发者,这使得omnibox更加强大。
image.png

五、通讯

由于 content script 运行在网页的上下文中,而不在扩展中,因此它们通常需要某种方式来与扩展的其余部分进行通信。

从浏览器插件内的视角来看通讯:
image.png

5.1 简单的一次性请求

5.1.1 从 content-scripts 发起请求

chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

5.1.2 发送消息到 content-scripts

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

5.1.3 接受消息

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"});
  }
);

5.2 长链接

可以使用runtime.connecttabs.connect 建立一个长链接进行通讯。

// port1
var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

// port2
chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

5.3 跨插件通讯

除了在插件中的不同组件之间发送消息之外,还可以使用消息传递 API 与其他插件进行通信。
可以使用 runtime.onMessageExternalruntime.onConnectExternal 来监听传入的请求和连接。
监听消息:

// For simple requests:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

发送消息:

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

5.4 从网页发送信息

插件可以接收和响应来自常规网页的消息。要使用这个特性,必须在 Manifent.json 中指定要与哪些网站通信。
通讯方式和跨插件通讯方式类似。

5.5 Native通讯

Chrome 插件可以与原生应用进行通讯,原生应用可以通过注册一个 Native Messaging Host,Chrome 以一个单独的进程启动 Host,并使用标准输入和标准输出流进行通信。
详情可参考:developer.chrome.com/docs/apps/n…

六、如何查看某个插件的源码

  1. 开源的, 如github.com/adblockplus…
  2. 本地查看
    1. 第一步:打开开发者模式,找到对应插件的id
    2. 第二步:本地插件目录,/Users/mac/Library/Application Support/Google/Chrome/Default/Extensions

七、最佳实践推荐

喜欢使用 React + TS:
github.com/chibat/chro…
github.com/lxieyang/ch…
喜欢使用 rxjs:
github.com/alibaba/bro…
不想要任何框架:
github.com/SimGus/chro…

八、适配其他浏览器

切换到 chromium 内核的浏览器适配工作还是比较小的,firefox 也支持 chrome API。

我们只需要对照各浏览器厂商提供的开发文档,关注我们用到的 API 是否有差异,有差异的部分,做一下兼容即可。

可参考各浏览器的 extension 开发文档:

firefox:developer.mozilla.org/en-US/docs/…

Edge:learn.microsoft.com/zh-cn/micro…

360:open.se.360.cn/open/extens…

搜狗:ie.sogou.com/open/doc/

九、发布

chrome开发者中心:chrome.google.com/webstore/de…

firefix发布文档:addons.mozilla.org/en-US/devel…

edge发布文档:learn.microsoft.com/zh-cn/micro…

十、未来

  • content-sctipts + Mockjs 自动填充表单?
  • UI检查,自动化测试?
  • 性能、依赖检测,Lighthouse?
  • 账号管理&一键登录?

可以在评论区说说你们用到的场景~~

参考

chrome 插件开发指南(字节跳动技术团队)
Chrome Extension 扩展程序迁移至 Manifest V3
Chromium扩展(Extension)的Content Script加载过程分析
一种开发 Chrome 扩展程序的新姿势
Message passing
Native Messaging