【转】从浏览器原理出发聊聊Chrome插件

2023-12-11  本文已影响0人  涅槃快乐是金

浏览器架构演进

单进程浏览器时代

单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。在 2007 年之前,市面上浏览器都是单进程的。

很多功能模块运行在一个进程里,是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素。

多进程浏览器时代

早期架构

从图中可以看出,早期的架构已经对浏览器的能力进行了拆分,主要拆分为三类:浏览器进程、插件进程和渲染进程。每个页面是运行在单独的渲染进程中的,同时页面里的插件也是运行在单独的插件进程之中,进程之间是通过 IPC 机制进行通信。这就解决了单进程时代浏览器的各种问题:

近期架构

相较之前,近期的架构又有了很多新的变化。

从图中可以看出,最新的 Chrome 浏览器包括:1 个浏览器主进程、1 个 GPU 进程、1 个网络进程、多个渲染进程和多个插件进程。

当前架构

目前Chrome浏览器的架构正在发生一些改变,称为面向服务的架构(SOA),目的是将和浏览器本身(Chrome)相关的部分拆分为一个个不同的服务,服务化之后,这些功能既可以放在不同的进程里面运行也可以合并为一个单独的进程运行。这样做的主要原因是让Chrome在不同性能的硬件上有不同的表现。当Chrome运行在一些性能比较好的硬件时,浏览器进程相关的服务会被放在不同的进程运行以提高系统的稳定性。相反如果硬件性能不好,这些服务就会被放在同一个进程里面执行来减少内存的占用。

插件运行机制

在运行机制前,我们先来回顾一下打开页面会发生什么:

打开页面发生了什么

1.检查状态码,非200执行状态码对应的处理逻辑;
2.200响应处理:检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,不再进行后续的渲染,如果是html则通知浏览器进程准备渲染进程进行渲染;

1.浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程;

1.渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”;
2.渲染进程接收完数据后,向浏览器发送确认消息;
3.浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面;

打开插件发生了什么

插件的运行相较于页面会有简化

1.我们打开浏览器,新增一个空白tab页

2.tab栏空白处右键,选择任务管理器,打开任务管理器面板

3.可以看到运行了6个进程,分别是浏览器进程、GPU进程、网络进程、存储进程、渲染进程和扩展进程。

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

插件基本介绍

版本发展

chrome插件存在三个版本,分别是Manifest V1、Manifest V2和Manifest V3。其中MV1版本已经被废弃了,目前市面上存在MV2和MV3版本,以MV2为主流,在被MV3慢慢取代。时间线:

Manifest V2新特性

https://developer.chrome.com/docs/extensions/mv2/manifestVersion/#manifest-v1-changes

Manifest V3新特性

切换MV3会带来的问题

// V2 background.js
let saveUserName = "";

// 其他页面,比如content-script或者popup中存储数据
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    saveUserName = name;
  }
});

// 点击popup时展示数据
chrome.action.onClicked.addListener((tab) => {
  // 这里saveUserName可能为空字符串
  console.log(saveUserName, "saveUserName");
});
// V3 service worker
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

为什么切换MV3?

从Manifest V1到Manifest V2,可以看到Chrome想提高插件的隐私和安全,同时也优化了不少API。而Manifest V3除了安全性更完善外,还在性能上下了功夫。Manifest V3 的核心非常明确,就是限制扩展对系统资源的使用。一直以来高资源占用都是 Chrome 为人诟病的痛点,而且扩展由于在后台运行,如果出现问题,更是难以定位和管理。虽然增加了诸多限制,但Manifest V3还是有优点的:

这些变化可以让 Chrome 变得更加流畅,对于用户来说是好事。

展示形式

Chrome插件有以下常见的8中展现形式:

browserAction(浏览器右上角)

在浏览器右上角扩展程序一栏显示,包含一个图标、名称和popup

pageAction(地址栏右侧)

pageAction指的是在当某些特定页面打开才显示的图标。在早些版本的Chrome是将pageAction放在地址栏的最右边,左键单击弹出popup,右键单击则弹出相关默认的选项菜单。而新版的Chrome更改了这一策略,pageAction和普通的browserAction一样也是放在浏览器右上角,只不过没有点亮时是灰色的,点亮了才是彩色的,灰色时无论左键还是右键单击都是弹出选项。

右键菜单

通过开发Chrome插件可以自定义浏览器的右键菜单,主要是通过chrome.contextMenus API实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等。

override(覆盖特定页面)

使用override可以将Chrome默认的一些特定页面替换掉,改为使用扩展提供的页面。扩展可以替代如下页面:

devtools(开发者工具)

Chrome允许插件在开发者工具(devtools)上开发,主要表现在:

React Developer Tools

option(选项页)

插件的设置页面,可以在右上角入口右键,有一个选项标签

omnibox

omnibox是向用户提供搜索建议的一种方式,可以在搜索栏输入特定的标识然后按Tab进入搜索。

桌面通知

Chrome提供了一个chrome.notificationsAPI以便插件推送桌面通知,暂未找到chrome.notifications和HTML5自带的Notification的显著区别及优势。在后台JS中,无论是使用chrome.notifications还是Notification都不需要申请权限(HTML5方式需要申请权限),直接使用即可。

核心介绍

manifest.json

这是一个Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_version、name、version3个是必不可少的。

Manifest V2

{
// 清单文件的版本,这里先使用2演示
"manifest_version": 2,
// 插件的名称
"name": "...",
// 插件的版本
"version": "1.0.0",
// 插件描述
"description": "...",
// 图标,一般偷懒全部用一个尺寸的也没问题
"icons": {
"16": "img/icon.png",
"48": "img/icon.png",
"128": "img/icon.png"
  },
// 会一直常驻的后台JS或后台页面
"background": {
"scripts": ["js/background.js"]
  },
// 浏览器右上角图标设置,browser_action、page_action、app必须三选一
"browser_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
  },
// 当某些特定页面打开才显示的图标
"page_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
  },
// 需要直接注入页面的JS
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["js/content-script.js"],
"css": ["css/custom.css"],
// 代码注入的时机,document_start, document_end, document_idle,默认document_idle
"run_at": "document_start"
    },
  ],
// 权限申请
"permissions": [
"contextMenus", // 右键菜单
"tabs", // 标签
"notifications", // 通知
"webRequest", // web请求
"webRequestBlocking",
"storage", // 插件本地存储
"https://*/*" // 可以通过executeScript或者insertCSS访问的网站
  ],
// 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
"web_accessible_resources": ["js/inject.js"],
"homepage_url": "...", // 插件主页
"chrome_url_overrides": { // 覆盖浏览器默认页面
"newtab": "newtab.html"
  },
"options_ui": { // 插件选项页
"page": "options.html",
"chrome_style": true
  },
"omnibox": { "keyword" : "..." }, // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
"default_locale": "zh_CN", // 默认语言
"devtools_page": "devtools.html", // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件
"content_security_policy": "...", // 安全策略
"web_accessible_resources": [ // 可以加载的资源
    RESOURCE_PATHS
  ]
}

Manifest V3(仅展示与V2版本的不同点)

{
"manifest_version": 3,
"background": {
"service_worker": js/background.js"
  },
  "action": { //browser_action 和 page_action,统一为 Action
    "default_icon": "img/icon.png",
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
  }
  "content_security_policy": {
    "extension_pages": "...",
    "sandbox": "..."
  },
  "web_accessible_resources": [{
    "resources": [RESOURCE_PATHS]
  }]
}

content-scripts

是Chrome插件中向页面注入脚本的一种形式(虽然名为script,其实还可以包括css的),借助content-scripts我们可以实现通过配置的方式轻松向指定页面注入JS和CSS。content-scripts和原始页面共享DOM,但不共享JS。如要访问页面JS(例如某个JS变量),只能通过injected js来实现。content-scripts不能访问绝大部分chrome API,除了下面这4种:

这些API绝大部分时候都够用了,有需要调用其它API的话,可以通过通信让background或service worker来帮忙调用

background

后台是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,可以跨域访问任何网站而无需要求对方设置CORS。background的概念在MV3版本中变为了service worker,区别在于生命周期变短了,service worker是短暂的基于事件的脚本,所以不适合用来保存全局变量。

popup

popup是点击右上角图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。权限级别和background差不多,就是生命周期比较短。

injected-script

chrome插件中其实没有injected-script这一概念,这是开发者们在开发过程中衍生出来的一种概念,指的是通过DOM操作的方式向页面注入的一种JS。因为content-script无法访问页面中的JS,虽然可以操作DOM,但是DOM却不能调用它,也就是无法在DOM中通过绑定事件的方式调用content-script中的代码。但是在网页中增加一个按钮来调用插件的能力是一个比较常见的需求,所以诞生了injected-script。

插件通信机制

讲通信机制之前,先回顾一下插件中存在的脚本类型。Chrome插件的JS主要可以分为这5类:injected script、content-script、popup js、background js和devtools js。

权限对比

通过权限对比可以看到,每一种脚本在权限上都不相同,所以各种脚本间的相互通信就非常重要,这也是插件能够实现众多功能的基础。

通信概览

一些常见插件的实现思路

埋点日志检测

一般业务中都会进行一些埋点上报,埋点的本质就是发送一些带特定参数的请求,前端本地调试的时候想实时查看埋点信息通常需要去查看上报接口的入参,或者去对应的埋点平台查看,这样非常不方便。基于这个,我们可以使用插件来帮助我们快速的可视化查看埋点信息:

页面注入小工具

插件的另一个常见用法就是往页面注入一些工具代码,比如去除页面广告工具。

总结

参考资料:
《浏览器工作原理与实践》:https://time.geekbang.org/column/intro/100033601?tab=catalog
《Inside look at modern web browser》:https://developer.chrome.com/blog/inside-browser-part1/
《图解浏览器的基本工作原理》:https://zhuanlan.zhihu.com/p/47407398
《Welcome to Manifest V3》:https://developer.chrome.com/docs/extensions/mv3/intro/
MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP
web_accessible_resources:https://developer.chrome.com/docs/extensions/mv2/manifest/web_accessible_resources/

作者|闵子

原文链接

上一篇下一篇

猜你喜欢

热点阅读