当前位置: 华文世界 > 数码

chrome插件之tabs

2024-01-12数码

一、前面的话

本文主要的内容是帮助读者朋友梳理chrome插件的tabs能力,如果您是第一次阅读本文,也建议您在阅读完本文后,尝试看看我下面的这些系列文章,它们可以更好的帮您认识和了解chrome插件:

  • chrome插件之从0到1
  • chrome插件之通信(V3版)
  • chrome插件之manifest配置
  • chrome插件之玩转action
  • 1.能力

    一款浏览器插件具备非常强大的能力,它不仅可以向当前所有的站点里注入脚本,对网站的功能进行扩展。更重要的是它还可以和浏览器的 标签系统 进行交互,从而创建、修改、管理每一个tab,而这一切都基于插件系统为我们提供的 tabs相关的API ,chrome不仅提供了我们用于操作和管理tabs的API,而且还提供了和 content 脚本之间的通信方法。

    温馨提示:

    Tabs API 只能在background脚本中以及option脚本、popup脚本、由chrome创建的tab中访问到,在content脚本中是无法访使用的。

    换句话说,chrome有选择性的给不同的脚本环境注入了不同的chrome对象,导致提供的API具备差异性。

    该图是我们在特定环境下可以通过chrome.tabs访问的所有的api,这些就是chrome为我们内置的提供给开发者的 能力

    2.权限

    在之前的文章中我们提到过,如果要使用某些特别的API,我们需要在插件配置文件 manifest.json 中配置相应的 权限声明 ,但幸运的是对于tabs相关的部分API不需要在manifest.json中显式的配置「tabs」就可以直接使用。比如说: 创建一个新的tab,重新加载某个tab,或者导航到另外一个URL等等。

    但是下面的这些API在使用的时候,则需要加上相关的配置才可以使用,比如说:

  • permission
  • 如果你希望通过特定的条件找到某些tabs,你需要使用 chrome.tabs.query(queryInfo , callback) 这个API,这个时候就需要显示的在manifest.json中permission中添加「tabs」声明。
  • host permission 如果你希望能够对指定的tab动态的在其中注入并执行一段脚本,或者注入、移除某一段css样式,那么你可能需要用到这些API:
  • chrome.tabs. executeScript () // 注入一段脚本并执行

    chrome.tabs. insertCss () // 注入一段css样式

    chrome.tabs. removeCss () // 移除一段css样式

  • 这个时候就需要在manifest.json中显式的声明需要命中的url。
  • {

    // manifest.json "host_permissions":[ "<all_urls>" ] // 支持正则匹配正则

    }

    二、API

    接下来我们一一通过案例来认识他们,从而感受每一个API的具体行为以及他们的使用条件、注意事项等等。

    1. 创建

    我们可以通过这个API来创建一个新的tab,这个tab和普通的站点不一样,属于插件所属的页面,因此支持跨域请求、获取更多的chrome提供的方法。

    // background.js

    chrome.runtime.onInstalled. addListener (({reason}) => {

    if (reason === 'install' ) {

    chrome.tabs. create ({

    url: "newtab.html" // 相对于background脚本的路径下需要有一个newtab.html文件

    });

    }

    });

    上面的脚本意味着在插件第一次安装完成之后,就会立马创建一个新的标签页,所以如果我们想要在任何时候创建一个新的tab,就可以使用这个API,行业内很多插件的工作台都是创建一个新的tab页进行工作的,比如著名的代理插件 SwitchySharp

    该api默认支持,不需要额外的manifest配置

    2. 查找

    我们可能有这样的需要,获取当前浏览器窗口处于激活状态的tab页面,因为对于同一个窗口,有且只有会一个tab是展示在用户面前的,我们把这样的tab称为激活状态,这个时候我们就需要用到下面的api。

    async function getCurrentTab () { let queryOptions = { active: true }; let [tab] = await chrome.tabs. query (queryOptions); return tab; }

    调用上面的方法,你就可以获得当前窗口激活的那个tab的实例对象了,从这个对象中,你可以获取到对应的tab唯一的id、url、图标等信息。

    值得注意的是,如果chrome浏览器打开了多个窗口,就意味着可能每一个窗口都会存在一个激活的tab,因此我们获取的tab就会是多个,这个时候如果只是解构出第一个可能是不够严谨的。

    因此我们可以通过添加搜索条件来精确的查找:

    async function getCurrentTab () {

    let queryOptions = {

    active: true , currentWindow: true

    };

    let [tab] = await

    chrome.tabs. query (queryOptions);

    return tab;

    }

    通过添加一个参数 currentWindow ,意味着只搜索脚本运行所在窗口的激活的tab,这个时候肯定只会查找出唯一的一个tab,解构第一个就不会有问题。

    搜索条件除了上述之外,还有下面可以选择:

    参数

    类型

    作用

    active

    boolean

    是否处于激活状态

    audible

    boolean

    是否处于播放音频状态

    currentWindow

    boolean

    是否处于脚本所在窗口内

    groupId

    number

    是否处于某个分组内

    highlighted

    boolean

    是否处于高亮状态

    index

    number

    窗口从左往右第index个tab

    pinned

    boolean

    是否处于被固定的状态

    status

    unloaded/loading/complete

    匹配tab的status为该status的tabs

    title

    string

    匹配tab的title为该title的tabs

    url

    string

    匹配tab的url为该url的tabs

    windowId

    number

    特定窗口下的tabs

    windowType

    normal/popup/panel/app/devtools

    特定的窗口类型下所在的tabs

    被固定是指那些通过右键点击tab的时候,选择固定在最左侧的标签,并且可以固定多个。

    该api默认支持,需要额外的manifest配置,需要显式声明「tabs」的permissions

    3.发送消息

    我们可以很方便的给指定的tab发送消息,一般来说我们可以在content脚本中做消息的监听,接收到消息后使其执行不同的业务逻辑。

    chrome.tabs. sendMessage (

    tabId: number, // 目标tab的id

    message: any, // 发送信息

    options?: object, // 其他配置项

    callback?: function , // 回调函数 )

    上面这个是V3版本的插件使用的,在V2版本中我们使用chrome.tabs.sendRquest()

    // 在v3版本中已废弃

    chrome.tabs. sendRequest (

    tabId: number, // 目标tab的id

    request: any, // 发送信息

    callback?: function , // 回调函数

    )

    4.修改

    如果我们希望修改一个tab的一些参数信息,我们可以选择使用下面这个API:

    chrome.tabs. update ( tabId?: number, updateProperties: object, callback?: function , )

    其中updateProperties的值就是上面提到的queryOptions的属性保持一致,例如我们可以动态的更改指定tab的title、url、pinned等状态属性!

    5.缩放比

    当我们按住ctrl的同时再滑动鼠标滚轮的话就可以调整页面的缩放比例,这个可能大家平时都深有体会,但是实际上这个也可以通过插件给我们提供的API来动态的进行调整:

    chrome.tabs. setZoom ( tabId?: number, zoomFactor: number, // 缩放比例 callback?: function , )

    6.移动/移除/刷新

    我们介绍的第一个API就展示了如何创建一个新的tab,他会默认创建在最末尾,也就是最右侧,如果这个放置位置我们不满意,我们也可以将其放置在我们想要的位置。

    移动

    chrome.tabs. move (

    tabIds: number | number [],

    moveProperties: object ,

    callback?: function ,

    )

    type moveProperties = { index?: number , // 想要移动至的index索引位置. `-1` 移动至窗口末尾.

    windowId?: number // 移动至的窗口id

    }

    移除

    chrome.tabs. remove ( tabIds: number | number[], callback?: function , )

    刷新

    chrome.tabs. reload ( tabId?: number , reloadProperties?: object , callback?: function , ) type reloadProperties = { bypassCache?: boolean // 是否绕过本地缓存 默认不绕过,也就是使用本地缓存。 }

    7.导航

    我们可以通过插件来控制一个tab的前进后退(如果他们都曾有过跳转的记录)

    chrome.tabs. goBack ( // 回到最近的一次历史记录 tabId?: number , callback?: function , ) chrome.tabs. goForward ( // 去到下一个历史记录,如果有的话 tabId?: number , callback?: function , )

    8.丢弃/复制

    当我们的tab开的特别多的时候,浏览器会有个小优化,对于某些长时间不用的tab,浏览器会清空内存中对其的状态存贮,因此当我们再次将其激活时会重新加载!这个过程插件也提供了API可以帮助我们做到:

    chrome.tabs. discard ( tabId?: number , callback?: function , ) chrome.tabs. duplicate ( // 这个API与discard相反,可以帮助我们复制一个一摸一样的tab标签 tabId: number , callback?: function , )

    9.分组

    如果我们希望将某些具备相似特征的网站分成一个组,使其能够在视图上更好的被察觉,那么我们就可以通过插件为我们提供的API来进行实现:

    第一步:筛选出希望分到同一组的tabs

    const tabs = await chrome.tabs. query ({ url: [ "https://developer.chrome.com/*" ], });

    根据前面的知识,我们很容易就可以知道tabs就是域名为 "developer.chrome.com" 开头的所有站点的tab集合;

    第二步:将他们分为一组

    const tabIds = tabs. map (({ id }) => id); const group = await chrome.tabs. group ({ tabIds });

    上图中就可以看到所有符合条件的站点就被分为同一个组了,这个API的使用方式是:

    chrome.tabs. group ( options: GroupOptions , callback?: function , ) type GroupOptions = { tabIds?: number [], // 希望被分组的tab的id的集合 groupId?: number , // 已有的分组 createProperties?:{ windowId?: number // 希望分组被创建在那个窗口, 默认是脚本所在窗口 } }

    额外的话:

    如果我们希望在分组上再加上一个样式或者字样作为标记的话,也可以这样做:

    // 第一步: 在manifest.json中添加「tabGrpups」的权限 { ... "permissions":[ "tabGroups" ] } //第二步: chrome.tabGroups.update(group, { title: "这是分组1" , color:'red' });

    就可以修改这个分组的一些特征,上面是增加了一个标题,效果如下:

    三、实战

    以上我们介绍了基本的API,接下来我们通过一些案例来实际感受一下每个API的作用:

    准备以下的项目:

    manifest.json

    {

    "name": "tabs demo" ,

    "description": "tabs demo" ,

    "version": "1.0" ,

    "manifest_version": 3 ,

    "action": {

    "default_popup": "popup.html" ,

    "default_icon":

    {

    "16": "/images/get_started16.png" ,

    "32": "/images/get_started32.png" ,

    "48": "/images/get_started48.png" ,

    "128": "/images/get_started128.png"

    }

    },

    "content_scripts": [

    {

    "js": [ "content.js" ],

    "matches": [ "<all_urls>" ] } ],

    "background":

    {

    "service_worker": "background.js"

    },

    "icons": {

    "16": "/images/get_started16.png" ,

    "32": "/images/get_started32.png" ,

    "48": "/images/get_started48.png" ,

    "128": "/images/get_started128.png"

    },

    "permissions": [ "tabs" , "tabGroups" ] }

    content.js / background.js

    // content.js let color = "" ; console . log ( "content.js" ); chrome.runtime.onMessage. addListener ((message, sender, sendResponse) => { color = document .body. style.color; document .body. style.background = message; sendResponse ( "changed" ); }); // background.js console . log (chrome);

    newtab.html

    <!DOCTYPE html > <html lang = "en" > <head> <meta charset = "UTF-8" /> <meta name = "viewport" content = "width=device-width, initial-scale=1.0" /> <title> chrome插件 </title> </head> <body> <p> 我是一个由chrome插件创建的页面 </p> </body> </html>

    popup.html

    <!DOCTYPE html > <html lang = "en" > <head> <meta charset = "UTF-8" /> <meta name = "viewport" content = "width=device-width, initial-scale=1.0" /> <title> Document </title> </head> <body> <p> <p> 创建新的页面 </p> <button id = "create-tab" >创建 </button> </p> <p> <p> 查找符合条件的tab </p> <div> <span> 是否激活 </span> <span> 是 </span> <input type = "radio" name = "isActive" value = "1" /> <span> 否 </span> <input type = "radio" name = "isActive" value = "0" /> </div> <div> <span> 是否属于当前窗口: </span> <span> 是 </span> <input type = "radio" name = "isCurrentWindow" value = "1" /> <span> 否 </span> <input type = "radio" name = "isCurrentWindow" value = "0" /> </div> <div> <span> url(支持正则): </span> <input type = "text" id = "url" /> </div> <div> <span> title </span> <input type = "text" id = "title" /> </div> <div> <span> index </span> <input type = "text" id = "index" /> </div> <div> <span> 是否被固定 </span> <span> 是 </span> <input type = "radio" name = "pinned" value = "1" /> <span> 否 </span> <input type = "radio" name = "pinned" value = "0" /> </div> <div> <span> status </span> <select name = "status" id = "status" > <option value = "unloaded" >unloaded </option> <option value = "loading" >unloaded </option> <option value = "complete" >unloaded </option> </select> </div> <button id = "query-tab" >查找 </button> <div> <div> 查找结果: </div> <div id = "search-result" > </div> </div> </p> <p> <p> 发送消息 </p> <input type = "color" id = "send-value" /> <button id = "send-btn" >变色吧 </button> </p> <p> <p> 删/改/移/丢弃/复制 </p> <div> <input type = "text" id = "move-index" /> <button id = "move" >移动当前的tab </button> </div> <div> <button id = "remove" >移除当前的tab </button> </div> <div> <button id = "reload" >刷新当前的tab </button> </div> <div> <input type = "text" id = "discard-value" /> <button id = "discard" >丢弃 </button> </div> <div> <button id = "duplicate" >复制 </button> </div> <div> <input type = "text" id = "update-value" /> <button id = "update" >更新 </button> </div> </p> <p> <p> 缩放比 </p> <div> <input type = "text" id = "zoom" /> <button id = "zoom-btn" >调整 </button> </div> </p> <p> <p> 分组 </p> <div> <input type = "text" id = "group-title" /> <button id = "group" >使用查询的结果进行分组 </button> </div> </p> <p> <p> 导航 </p> <div> <button id = "goForward" >前进 </button> <button id = "goBack" >前进 </button> </div> </p> <script src = "./popup.js" > </script> </body> </html>

    popup.js

    document . getElementById ( "create-tab" ). addEventListener ( "click" , () => { chrome.tabs. create ({ url: "newtab.html" , // 相对于background脚本的路径下需要有一个newtab.html文件 }); }); let Tabs = []; const getSelect = (list) => { const yes = list[ 0 ]; const no = list[ 1 ]; if (yes.checked) { return yes.defaultValue === "1" ; } if (no.checked) { return no.defaultValue === "1" ; } return false ; }; document . getElementById ( "query-tab" ). addEventListener ( "click" , async () => { const active = getSelect ( document . getElementsByName ( "isActive" )); const currentWindow = getSelect ( document . getElementsByName ( "isCurrentWindow" ) ); const pinned = getSelect ( document . getElementsByName ( "pinned" )); const url = document . getElementById ( "url" ).value; const title = document . getElementById ( "title" ).value; const index = document . getElementById ( "index" ).value; const queryOptions = { active, currentWindow, pinned, }; if (url) { queryOptions.url = url; } if (title) { queryOptions.title = title; } if (index) { queryOptions.index = index - 0 ; } console . log (queryOptions); const tabs = await chrome.tabs. query (queryOptions); document . getElementById ( "search-result" ).innerHTML = JSON . stringify ( tabs. map (({ id }) => ({ id })) ); Tabs = tabs; }); document . getElementById ( "send-btn" ). addEventListener ( "click" , async () => { const color = document . getElementById ( "send-value" ).value; const [tab] = await chrome.tabs. query ({ active: true , currentWindow: true }); const response = await chrome.tabs. sendMessage (tab.id, color); console . log (color, response); }); const getCurrentTab = async () => { const [tab] = await chrome.tabs. query ({ active: true , currentWindow: true }); return tab.id; }; document . getElementById ( "move" ). addEventListener ( "click" , async () => { const index = document . getElementById ( "move-index" ).value - 0 ; const tabIds = await getCurrentTab (); chrome.tabs. move (tabIds, { index }); }); document . getElementById ( "remove" ). addEventListener ( "click" , async () => { const tabIds = await getCurrentTab (); chrome.tabs. remove (tabIds); }); document . getElementById ( "reload" ). addEventListener ( "click" , async () => { const tabId = await getCurrentTab (); chrome.tabs. reload (tabId); }); document . getElementById ( "discard" ). addEventListener ( "click" , async () => { const tabId = document . getElementById ( "discard-value" ).value - 0 ; chrome.tabs. discard (tabId); }); document . getElementById ( "duplicate" ). addEventListener ( "click" , async () => { const tabId = await getCurrentTab (); chrome.tabs. duplicate (tabId); }); document . getElementById ( "zoom-btn" ). addEventListener ( "click" , async () => { const tabId = await getCurrentTab (); const zoomFactor = document . getElementById ( "zoom" ).value - 0 ; chrome.tabs. setZoom (tabId, zoomFactor); }); document . getElementById ( "group" ). addEventListener ( "click" , async () => { const tabIds = Tabs . map (({ id }) => id); const title = document . getElementById ( "group-title" ).value; const group = await chrome.tabs. group ({ tabIds }); chrome.tabGroups. update (group, { color: "red" , title }); }); document . getElementById ( "goForward" ). addEventListener ( "click" , async () => { const tabId = await getCurrentTab (); chrome.tabs. goForward (tabId); }); document . getElementById ( "goBack" ). addEventListener ( "click" , async () => { const tabId = await getCurrentTab (); chrome.tabs. goBack (tabId); });

    以上的资源我会放到github上,大家可以download下来直接在自己的浏览器上运行,查看效果,也希望有收获后给不吝star哈!。

    下面是我本地的测试效果:

    创建页面/发送消息

    查询

    删/改/更新

    分组

    有了以上的武器,就可以玩转tabs啦!一起开始开发chrome插件吧!

    四、最后的话

    以下是我的其他文章,欢迎阅读

    保姆级讲解JS精度丢失问题(图文结合)

    shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?

    从0到1开发一个浏览器插件(通俗易懂)

    用零碎时间个人建站(200+赞)

    另外我有一个自己的网站,欢迎来看看 new-story.cn

    创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。