背景
你一定在网上遇到过如下这种场景:查找资料,根据搜索结果,点击进入某个网页,网页打开之后,结果发现,网站的广告喧宾夺主,抢夺眼球,扰乱视线,不关闭根本无法正常阅读和学习。于是你就去关闭那些广告,可是当你点击之后,发现又被带到另外一个页面。一个广告就够人心生厌烦的呢,然而页面上这样的广告还不止一个,令人不胜厌烦。有没有什么方法,治理一下这种不讲武德的广告,重新还阅读一个安静,不被打扰的环境。
效果展示
在网上找了一下,看到有文章说,Chrome扩展可以屏蔽网页中的广告。通过查找资料和调试,最终实现的效果如下: 初次进入目标页面时,点击扩展图标,启用屏蔽特定网站广告功能,立即移除特定网站的广告。下次进入时,直接清理。
Chrome扩展架构介绍
在开发Chrome扩展前,先说说Chrome扩展的构成,大多数Chrome扩展,通常由以下几部分组成:
manifest.json
:扩展的配置文件, 声明扩展的清单文件版本,名称、版本号、图标、后台和注入网页脚本文件,权限等信息。background
:它的生命周期是扩展所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,通常把需要一直运行的、启动就运行的、全局的代码放在background里面。content_script
: 是扩展注入到页面的脚本,但在页面 DOM 结构中是查看不到的。content_script 可以操作 DOM,但是它和页面其它的脚本是隔离的,访问不到其它脚本定义的变量、函数等,相当于运行在单独的沙盒里。content_script 可以调用有限的 chrome 插件 API,网络请求受到同源策略限制。
接下来我们逐一介绍一下。
manifest.json(清单文件)
它相当于扩展的地图,我们可以按图索骥,了解扩展所支持的所有功能。每一个扩展程序都需要有一个配置清单 manifest.json
文档,它提供了关于扩展程序的基本信息,例如扩展使用的清单版本, 后台脚本,注入网页脚本,所需的权限、名称、版本等。manifest.json的各项属性简介如下,清单文件每一项的具体配置请参考官方manifest说明文档。
{
/* ================必填项=================== */
// 浏览器会根据清单文件版本指定该版本拥有的功能及编码规范
"manifest_version": 3,
// 扩展名称
"name": "My Extension",
// 扩展版本
"version": "1.0.0",
/* ===================== 推荐项 =================== */
// 控制扩展在工具栏的展示,如图标,鼠标划过时的文案,点击之后的弹出页
"action": {...},
// 定义支持多语言环境的扩展的默认语言
// 如果扩展包目录存在_locals文件夹,它是_locals文件夹默认语言的子目录名称,
// 此时也是必配项,如果不存在_locals文件夹,可以不配
"default_locale": "en",
// 扩展描述--文本格式,字符最大长度是132
"description": "A plain text description",
// 扩展图标--一般是png格式,也支持BMP, GIF, ICO, JPEG
// 不同尺寸的图标用途是:
// 128*128 网上商店使用
// 48*48 扩展程序管理页面用
// 16*16 扩展页面使用
"icons": {...},
/* ===================== 可选项 ==================== */
// 作者邮箱,必须与网上商店发布扩展账号的邮箱一致
"author": "developer@example.com",
// 后台服务,扩展的事件处理程序
"background": {...},
// 通过content scripts,可以实现Chrome扩展与用户打开的Web页面之间的交互。
// 读取浏览器打开web页面的信息,和对其修改,并将信息传递给父扩展
"content_scripts": [{...}],
// 扩展提供的选项配置页面
"options_page": "options.html",
// options_page是打开一个tab页,options_ui是弹窗
"options_ui": {...},
// 覆盖chrome浏览器的一些配置项,如启动页,主页,搜索引擎等
"chrome_settings_overrides": {...},
// 覆盖chrome提供的新标签页
"chrome_url_overrides": {...},
// 配置触发扩展操作的快捷键
"commands": {...},
// 为来自扩展的请求的响应标头cross_origin_embedder_policy指定一个值
"cross_origin_embedder_policy": {...},
// 为来自扩展的请求的响应标头Cross-Origin-Opener-Policy指定一个值
"cross_origin_opener_policy": {...},
// 声明扩展修改或阻止用户页面网络请求的规则
"declarative_net_request": {...},
// 配置规则,使用declarativeNetRequest拦截/阻止/修改请求时, 无需使用declarativeContent读取页面权限
"event_rules": [{...}],
// 声明哪些扩展或页面可通过runtime.connect和runtime.sendMessage连接本扩展
"externally_connectable": {...},
// 用于指定可用于打开某些文件类型的程序或应用程序
"file_browser_handlers": [...],
// 用于指定 Chrome 扩展程序提供的文件系统的功能和支持的文件操作类型。
"file_system_provider_capabilities": {...},
// 在Chrome DevTools中添加新的UI面板和侧边栏
"devtools_page": "devtools.html",
// 访问扩展的官方主页,可以设置成个人或公司站点,不设置的话将在chrome://extensions页面显示扩展程序
"homepage_url": "https://path/to/homepage",
// 用import字段声明扩展依赖的模块
"import": [{...}],
// 用于导出模块的,它可以将一个模块或者一个变量、函数等暴露给其他模块或应用程序使用。
"export": {...},
// 允许使用 input.ime API(输入法编辑器),与 ChromeOS 一起使用
"input_components": [{...}],
// 扩展唯一标识。一般不需要指定,自动生成
"key": "publicKey",
// 扩展对chrome浏览器的最低版本要求
"minimum_chrome_version": "107",
// oauth2认证配置
"oauth2": {...},
// 向Chrome地址栏注册搜索关键字 ,在地址栏输入内容时会进行匹配
"omnibox": {...},
// 扩展API的调用授权
"permissions": ["..."],
// 使用chrome.permissions API在运行时请求声明的可选权限
// 如果 background 请求的域名是跨域的,则必须要在 host_permissions 中追加该域名
"host_permissions": [...],
// 运行时扩展需要用户授予的权限
"optional_permissions": ["..."],
// background 请求的域名是跨域的,且读取的数据需要用户授权,需要追加的域名
"optional_host_permissions": ["..."],
// 扩展所需要的插件或技术, 目前只有两种配置 3D或plugins
"requirements": {...},
// 定义在沙盒中提供的扩展页面集合。扩展的沙盒页面使用的内容安全策略在content security_policy 键中指定
"sandbox": {...},
// 用于规定扩展程序中加载的资源允许的来源和类型
"content_security_policy": {...},
// 从Chrome99起新增了一个阅读清单,开放了一些配置属性
"side_panel": {...},
// 托管存储区配置, 用managed_schema字段指明托管存储区结构的协议文件,协议文件格式是JSON Schema
"storage": {...},
// 注册文本转语音 (TTS) 引擎后,任何其它扩展或chrome应用使用tts API生成语音时,本扩展可以拦截处理
"tts_engine": {...},
// 托管在chrome网上应用商店之外的服务器扩展必须指明这个字段
"update_url": "https://path/to/updateInfo.xml",
// 扩展名称展示不全时,使用的简称配置
"short_name": "Short Name",
// 版本名称
"version_name": "1.0 beta",
// 网页和其它扩展可以访问本扩展的网页资源
"web_accessible_resources": [...],
// 在隐身模式下运扩展程序的方式
"incognito": "spanning, split, or not_allowed",
// 只能在开发模式下使用
// 配置之后,可以使用chrome.automation API提供的自动化测试功能
"automation": {...},
}
backgroud(后台)
背景后台是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的。通常用来协调扩展程序中不同类型页面的任务和监听浏览器事件,如:扩展程序被安装、打开/关闭页面,切换页面,创建新标签、添加新书签、点击扩展工具栏图标等。只有一个配置项service worker
, 用于指定 service_worker 文件。
{
// ...
"background": {
"service_worker": "background.js"
},
// ...
}
service_worker 可以使用几乎所有的Chrome API,但 service_worker 不能直接与网页的内容直接进行交互,需要与 content_scripts 进行通信来间接修改网页的内容。当下列情景发生时,service_worker才被执行:
- 扩展程序被安装或者更新。
- 所监听的事件被触发。
- 收到 content_scripts 或者 其它扩展程序的消息。
Chrome API的权限(permissions)
Chrome API需要在manifest.json的permissions中授权才能正常使用。想要系统的了解Chrome的权限知识请点击这里查看,文中用到的权限有:
权限 | 描述 |
---|---|
tabs | 允许扩展程序操作浏览器标签页,例如创建、删除、切换、获取信息等 |
activeTab | 允许扩展程序访问当前激活的标签页的内容,以及在该标签页上执行一些操作,例如注入JavaScript代码或修改页面样式 |
background | 允许扩展程序在后台运行,并随时接收来自浏览器的事件和请求 |
storage | 允许扩展程序在浏览器本地存储中读写数据 |
content_scripts(内容脚本)
content_scripts 是注入到网页中运行的文件。它可以使用标准的 Document Object Model(DOM)对象来访问网页中内容并对其进行修改。可以向页面注入js 或者 css 文件对页面进行操作和修改。但是它只能直接获取部分的 API:runtime
、 storage
和 i18n
,注入脚本有两种方式,静态注入或者在代码里手动注入。想详细了解请参考这里。
由于安全等原因,content_scripts 在一个隔绝的环境里,与它所在的tab页绑定在一起。网页本身所创建对象和函数,在 content_scripts 中是无法访问的。打开几个匹配的页面就会运行几个执行文件,而这几个不同的执行文件之间由所在tabId 区分。因此想要向某个网页的 content_script 发送信息时需要指定 tabId,如chrome.tabs.sendMessage(tabId,message)
。
另外还有几种脚本类型,popup
,devtools
,injected
,它们的权限区别如下:
JS种类 | 可访问的API | DOM访问情况 | JS访问情况 | 直接跨域 |
---|---|---|---|---|
background(背景脚本) | 可访问绝大部分API,除了devtools系列 | 不可直接访问 | 不可以 | 可以 |
content script (内容脚本) | 只能访问 extension、runtime等部分API | 可以访问 | 不可以 | 不可以 |
injected script (动态注入脚本) | 和普通JS无任何差别,不能访问任何扩展API | 可以访问 | 可以访问 | 不可以 |
popup (选项弹窗脚本) | 可访问绝大部分API,除了devtools系列 | 不可直接访问 | 不可以 | 可以 |
devtools(开发工具脚本) | 只能访问 devtools、extension、runtime等部分API | 可以访问devtools | 可以访问devtools | 不可以 |
background和content通信
如下图所示, 可以看到:background和content之间可以互相通信。
- backround给content发送和接收消息的方法
// background 给content发送消息--适合主动发消息的场景
chrome.tabs.sendMessage(tabId, message, (res) => {
console.log(res);
});
// background 接收和回复content的消息--适合应答场景
chrome.runtime.onMessage.addListener((message,sender,sendResponse)=>{
if(message.status === "done"){
sendResponse({recv:'good'})
}
})
- content给background发送和接收消息的方法
// content 主动给background 发消息
chrome.runtime.sendMessage(message);
// content接收background消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log(`打开页面收到后台的消息: ${message}`);
if(message.action === "on"){
// do something
sendResponse({status:'done'});
}
});
开发与调试
- 第一步 进入到管理扩展程序页面
- 第二步 打开开发者模式开关,加载Chrome扩展开发目录,点击Service_Worker就能看到扩展后台的打印日志。注入网页的日志可直接在打开网页的开发调试工具中的console面板查看。
移除广告功能实现
实现思路:
- 在后台脚本中,处理当前激活的tab页启用/禁用扩展操作,给网页注入脚本发相应的指令,并记录设置的开关状态。另外,每次切换标签页的时候,刷新扩展开关状态,指示当前页面设置的状态值。
- 初次打开网页时,网页注入脚本在收到后台移除广告指令时,移除当前页面的广告。再入打开时,读取当前网页的扩展开关设置值,执行相应的操作。
配置清单文件
开发一个扩展之前,首先要定义扩展使用的manifest_version(清单版本,) 以及backround(后台脚本),content_scripts(注入网页脚本),permissions(权限)等重要信息。本文要开发的扩展清单文件定义如下:
{
"manifest_version": 3,
"name": "HideAd",
"description": "屏蔽特定网站的广告",
"version": "0.0.1",
"action": {
"default_icon": "disable.png",
"default_title": "HideAd功能处于禁用状态"
},
"icons": {
"128": "hide-ad.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"js": ["content.js"],
"matches": ["https://www.jianshu.com/*"],
"run_at": "document_start"
}
],
"permissions": ["activeTab", "tabs", "background", "storage"]
}
编写后台脚本
后台脚本的功能是:
- 监听扩展图标点击事件,默认不开启移除网页广告功能。点击扩展图标后,开启当前页面的广告移除功能,再次点击,关闭当前页面的广告移除功能。
import IconUI from "./icon-ui.js";
const UI = new IconUI();
// 监听浏览器扩展(包含图标、工具提示、徽章和弹出内容)单击事件
chrome.action.onClicked.addListener((currentTab) => {
console.log("切换隐藏广告扩展开关:", currentTab);
UI.toggleHideAdSwitch(currentTab);
});
切换移除广告扩展开关的逻辑是, 读取激活tab页之前的开关设置值, 然后切换到相反状态。并执行相应状态的操作。
async toggleHideAdSwitch(tab) {
// console.log({tab});
const status = await this.getPageStatus(tab);
// 如果没有开启,则开启,反之则关闭
const action = [undefined, "off"].includes(status) ? "on" : "off";
// console.log(`开启/关闭 屏幕页面广告扩展功能: action=${action} host=${this.host}`);
this.doAction(tab.id, action);
}
获取页面开关状态,这里采用打开页面的域名作为存储页面开关状态键值对的key, 域名无法直接获取,需要从url解析。url获取逻辑是: 切换tab时,如果当前激活tab页还在加载中,tab.url是个空值,此时需要取tab.pendingUrl的值才能拿到当前页面的url, tab页加载完成后,tab.pendingUrl属性就不存在了,这时需要取tab.url的值获取当前激活tab页的url。
async getPageStatus(tab) {
const url = tab.url || tab.pendingUrl;
this.host = url?.split("/")?.[2] || "";
return await this.getStorage(this.host);
}
getStorage
的功能是从本地磁盘读取设置值。 Chrome 扩展存储 API 提供了 2 种储存区域,分别是 sync
和 local
。两种储存区域的区别在于,sync
储存的区域会根据用户当前在 Chrome
上登陆的 Google 账户自动同步数据,当断网时,sync
和 local
区域对数据的读写行为一致。使用 Chrome 存储 API 须在 Manifest.json 的 permissions
字段中声明 "storage"
权限,之后才能正常调用。
// 扩展从硬盘读取设置值
async getStorage(key) {
// 调试用--读取所有的设置值
// chrome.storage.sync.get((result)=>{
// console.log(result);
// })
const res = await chrome.storage.sync.get();
return res[key];
}
// 扩展向硬盘写入存储值
setStorage(key, val) {
// console.log("setStorage", { key, val });
return chrome.storage.sync.set({ [key]: val });
}
doAction
做的事情是:更新扩展在当前激活页展示的图标和标题,记录当前激活页的开关设置,给注入当前激活页的脚本发送对应的指令。
// 执行扩展行为
doAction(tabId, action) {
this.switchIconStatus(action);
this.setStorage(this.host, action);
this.sendMessage(tabId, action);
}
// 切换扩展图标状态
switchIconStatus(status) {
console.log("switchIconStatus", status);
if (status === "on") {
this.setIcon(this.enableIcon);
this.setTitle(this.enableTitle);
} else {
this.setIcon(this.disableIcon);
this.setTitle(this.disableTitle);
}
}
// 设置扩展标题
setTitle(title) {
chrome.action.setTitle({
title,
});
}
// 设置扩展图标
setIcon(path) {
chrome.action.setIcon({
path: {
64: path,
},
});
}
// 扩展程序向打开的页面发消息
sendMessage(tabId, action) {
chrome.tabs.sendMessage(tabId, { action });
}
- 监听活动的tab页变化事件。在已经打开的tab页之间切换时,读取当前激活tab页的移除广告开关状态值,将扩展图标设置成对应的开关状态。调用
chrome.tabs.query
方法,就能获取当前激活tab页的信息。使用 Chrome tabs API 必须要在 Manifest 的permissions
中声明tabs
权限,否则调用chrome.tabs.query方法
获取的tab信息不完整。
chrome.tabs.onActivated.addListener(async (newTab) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
// console.log(tabs[0]);
console.log("标签页切换:", tabs[0]);
UI.initPageStatus(tabs[0]);
});
});
// 初始化页面扩展图标状态
async initPageStatus(tab) {
const status = await this.getPageStatus(tab);
this.switchIconStatus(status);
}
编写注入网页脚本
注入网页脚本的功能是:
- 初次进入页面时, 接收后台发过来的信息,如果后台发送的是移除广告指令,则移除页面的广告。
// 接收后台消息
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const { action } = msg;
// console.log(`打开页面收到后台的消息: ${action}`);
action === "on" && delAdDom();
// sendResponse({ res: `打开页面执行的操作是${action}` });
// chrome.runtime.sendMessage({ host: location.host });
});
- 再次进入页面时,页面加载完成之后,读取当前页的移除广告开关设置,如果处于开启状态,则移除当前页面中的广告元素。
// 如果扩展处于开启状态,则删除页面中的广告
window.onload = async function () {
const res = await chrome.storage.sync.get();
// console.log(res);
const isOn = res[location.host] === "on";
isOn && delAdDom();
};
如何移除目标页面的广告元素? 用Chrome开发调试工具查看了一下目标网页渲染完成之后的Dom树, 找出广告元素是body元素下面的最后四个div元素, 锁定目标之后,让我们干掉它。
// 删除页面中的广告
async function delAdDom() {
document.querySelectorAll("body > div:nth-last-child(-n+4)").forEach((item) => item.parentNode.removeChild(item));
}
彩蛋
本文写的移除网页广告的插件,不具备通用性,只为抛砖引玉。结合一个真实的使用场景,把完全不懂Chrome扩展开发的初学者带入门,完整代码请点击这里下载获取。如果你已掌握本文移除网页广告的原理,那下面这种招人厌烦的弹窗,相信对你而言也是小菜一碟,分分钟就能搞定。如果你善于动手,勤于动手的话,可以实现一下这个功能,检验一下自己的学习效果。
参考文章
原文链接:https://juejin.cn/post/7241510305367736357 作者:去伪存真