如果你也被某些网站的广告所困扰, 请看这里

背景

你一定在网上遇到过如下这种场景:查找资料,根据搜索结果,点击进入某个网页,网页打开之后,结果发现,网站的广告喧宾夺主,抢夺眼球,扰乱视线,不关闭根本无法正常阅读和学习。于是你就去关闭那些广告,可是当你点击之后,发现又被带到另外一个页面。一个广告就够人心生厌烦的呢,然而页面上这样的广告还不止一个,令人不胜厌烦。有没有什么方法,治理一下这种不讲武德的广告,重新还阅读一个安静,不被打扰的环境。
如果你也被某些网站的广告所困扰, 请看这里

效果展示

在网上找了一下,看到有文章说,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面板查看。

如果你也被某些网站的广告所困扰, 请看这里

移除广告功能实现

实现思路:

  1. 在后台脚本中,处理当前激活的tab页启用/禁用扩展操作,给网页注入脚本发相应的指令,并记录设置的开关状态。另外,每次切换标签页的时候,刷新扩展开关状态,指示当前页面设置的状态值。
  2. 初次打开网页时,网页注入脚本在收到后台移除广告指令时,移除当前页面的广告。再入打开时,读取当前网页的扩展开关设置值,执行相应的操作。

配置清单文件

开发一个扩展之前,首先要定义扩展使用的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"]
}

编写后台脚本

后台脚本的功能是:

  1. 监听扩展图标点击事件,默认不开启移除网页广告功能。点击扩展图标后,开启当前页面的广告移除功能,再次点击,关闭当前页面的广告移除功能。
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 });
}
  1. 监听活动的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 作者:去伪存真

(0)
上一篇 2023年6月7日 上午11:08
下一篇 2023年6月8日 上午10:00

相关推荐

发表回复

登录后才能评论