chrome插件之脚本注入

一、前面的话

今天我们会介绍一个特别牛的特性,那就是脚本注入

如果您阅读过我插件系列前面的文章,就能够知道我们主要通过contentScript来扩展站点的功能,因为在contentScript环境中我们可以获取到站点的DOM对象,进而扩展站点的行为,但是contentScript是静态的,也就是说我们在开发的时候写一份,每一个匹配到的站点就会执行它。但是如果我们的需求是对某几个特定的站点执行一些个性化的扩展的话就有些力不从心了。

我们举个简单的例子:

假设我们现在要统计一些数据,针对百度和google两种搜索引擎,测试一下他们对同一词条的搜索结果的差异。我们当然可以使用肉眼去看,比如像下面这样。

google

百度

但我们不想用肉眼去看,这个时候我们就可以开发一款插件,这款插件会自动识别两种搜索引擎的搜索结果,然后统计他们的搜索词条的标题和数量。

任何需求都可以抽象成若干个简单的小问题,我们将这个需求抽象成下面这样:

我们希望实现在特定的时机,在a.com中执行a逻辑,b.com中执行b逻辑!

为什么不使用contentScript

可能大家首先会想要使用contentScript来做,我就在contentScript中写如下这样的代码就好了:

js 复制代码
// content.js

 if(当前的url是 a.com){
    // 执行a逻辑
 }
 
 if(当前的url是 a.com){
   // 执行b逻辑
 }

这样做看似是可以的,但是要知道contentScript是每次刷新页面的时候都会执行的,但是需求是在特定的时候,可能是在popup中点了某个按钮之后。可千万不能每次刷新页面都执行,因为每次都执行的话会产生一些脏数据,干扰统计的结果。

那这个时候怎么办呢?

这就需要在这个特定的时机,如果能够往某个站点注入一段脚本就好了。幸运的是,chrome早就为我们提供了这样的能力 -- chorome脚本注入

二、MV2版本

如果你是使用MV2版本来进行插件开发的话,那么就可以使用tabs来实现脚本注入,需求在manifest.json中追加这样的配置:

json 复制代码
{
   ...
   "manifest_version": 2,
   permissions:["tabs"]
   ...
}

然后这样使用它:

js 复制代码
chrome.tabs.executeScript(  
  tabId?: number,  
  details: [InjectDetails],  
  callback?: function,  
)

这个InjectDetails是这样的配置:

ts 复制代码
type InjectDetails {
  allFrames?:boolean,
  code?:string, 
  file?:string, // 路径
  cssOrigin?:"author" | "user",
  frameId?:number
}

InjectDetails 这个类型的每个字段的含义会在下文中和MV3版本一起说明,因为使用tabs的方式注入脚本的方式已经被MV3版本给替换了,它变成了全新的一个方式 --scripting

二、MV3版本

要是用MV3版本的脚本注入,首先需要做的就是在manifest.json中追加下面的配置

json 复制代码
{
  ...
  "manifest_version": 3,
  "permissions": ["scripting", "activeTab"],
  "host_permissions":["<all_urls>"]
  ...
}

为什么还需要加上host_permissions呢?其实是因为scripting只是给你提供了scripting相关的API,但是要真的使用的话,还得让站点允许你去注入脚本,因为注入脚本是一个非常具有侵入性的动作,所以需要使用host_permissions来允许你做一些侵入性的事情,被host_permissions匹配的站点就会允许你去注入脚本,否则是不可以的,如果你不配置host_permissions就使用scripting,会报这个错:

上面配置的manifest.json中就是所有站点都允许注入脚本。

使用

js 复制代码
function getTabId() { ... }

chrome.scripting
    .executeScript({
      target : {tabId : getTabId()},
      files : [ "script.js" ],
    })
    .then(() => console.log("script injected"));

你可以往一个特定的tab中注入一段脚本并运行。

我们都知道每个网页中可以嵌入iframe,而一个站点中iframe的javascript运行时和主站点的javascript运行时是相互隔离的,他们的变量也不能够互相访问。

如果我希望注入的脚本也能够在main站点下的所有iframe中都生效的话,我可以这样做:

js 复制代码
function getTabId() { ... }

chrome.scripting
    .executeScript({
      target : {tabId : getTabId(), allFrames : true}, // 指定allFrames为true,默认为false,只在main中注入并执行,不能穿透所有的iframe。
      files : [ "script.js" ],
    })
    .then(() => console.log("script injected in all frames"));

同样的,你也可以让脚本注入除main之外特定的几个iframe,只需要知道他们的iframeId即可。

javascript 复制代码
function getTabId() { ... }

chrome.scripting
    .executeScript({
      target : {tabId : getTabId(), frameIds : [ frameId1, frameId2 ]},
      files : [ "script.js" ],
    })
    .then(() => console.log("script injected on target frames"));

上面的例子是注入一个文件,我们如果只是希望注入一个简单的函数也是可以的,我们可以这样做。

javascript 复制代码
// background.js
function getTabId() { ... }
function getTitle() { return document.title; } // 这个document是被注入的环境的DOM对象。

chrome.scripting
    .executeScript({
      target : {tabId : getTabId()},
      func : getTitle,
    })
    .then((title) => console.log("injected a function" , title)); // 可以在回调中拿到返回值

上面是注入脚本的例子,如果我们要注入css可以这样:

css 复制代码
function getTabId() { ... }
const css = "body { background-color: red; }";

chrome.scripting
    .insertCSS({
      target : {tabId : getTabId()},
      css : css,
      origin:"author" | "user"
    })
    .then(() => console.log("CSS injected"));

这个origin有两种类型,一种是"author",一种是"user",我们可以通过这里来认识它。

"Style origin"(样式来源)是一个用于标识样式规则应用的来源的概念。在Web开发中,页面的样式可以来自多个不同的来源,这些来源可以相互覆盖或叠加,从而决定元素最终的外观。

下面是一些常见的样式来源:

  1. User Agent Stylesheet(用户代理样式表): 这是浏览器内置的样式表,用于定义HTML元素的默认外观。例如,浏览器会为标题元素(<h1>, <h2>, 等)和段落元素(<p>)定义默认字体、颜色和间距等样式。
  2. Author Stylesheet(作者样式表): 这是由网页开发人员或设计师编写的样式表,用于自定义页面的外观。作者样式表包括在HTML文档中的<style>标签或外部CSS文件中。
  3. User Stylesheet(用户样式表): 这是由用户定义的自定义样式表,用于覆盖网页上的任何样式规则。用户样式表通常是通过浏览器插件或扩展来添加的。
  4. Element Inline Styles(元素内联样式): 这是直接在HTML元素上使用style属性定义的样式规则。内联样式具有最高的优先级,因为它们直接应用于元素本身。

当页面上存在来自多个样式来源的规则时,样式来源的优先级通常按照以下顺序决定(从高到低):

  1. 元素内联样式(内联样式表)。
  2. 用户样式表(用户定义的自定义样式)。
  3. 作者样式表(网页开发人员定义的样式)。
  4. 用户代理样式表(浏览器的默认样式)。

我们在注入css样式的时候,可以选择以何种方式来注入样式。

模块

上面的展示了如何注入一段脚本,但是其实他们都是一锤子买卖,注入之后立马就运行了,然后就结束了。我们也可以选择注入一个模块,可以实现脚本的多次执行。

js 复制代码
chrome.scripting.registerContentScripts(  
  scripts: RegisteredContentScript[],  
  callback?: function,  
)
ts 复制代码
type RegisteredContentScript {
  allFrames?:boolean, // 是否注入子代frames中
  css?:string[], // 对匹配的站点注入的css文件路径的集合
  excludeMatches?:string[], // 排除那些站点
  id:string, // 唯一标示
  js?:string[], // 对匹配的站点注入的js文件路径的集合
  matches?:[], // 匹配哪些站点
  persistAcrossSessions?:boolean,
  runAt?:"document_start" | "document_idle" | "document_end",// 执行时机
}

通过上面的配置,我们可以注入一个脚本模块,来指定什么时候可以执行脚本;

js 复制代码
chrome.scripting.registerContentScripts(  
  scripts: [
    {
       id:"test",
       js:['script.js'],
       matches:["https://example.com/*"],
       runAt:"document_start"
    }
  ] 
)

上面的例子就代表所有匹配上 example.com 的站点每次在DOM开始加载的时候执行script.js脚本。

既然有注册,那就有取消,我们可以这样来取消我们注册的javascritp模块:

ts 复制代码
chrome.scripting.unregisterContentScripts(  
  filter?: { ids:string[] },  
  callback?: function,  
)

三、小试牛刀

我们尝试做一个需求,插件安装之后,弹出一个页面,页面中有一个按钮,点击之后可以让google的页面变绿,让百度的页面变红。

json 复制代码
{
  "name": "scripting demo",
  "description": "scripting demo",
  "version": "1.0",
  "manifest_version": 3,
  "action": {
    "default_icon": {
      "16": "/images/get_started16.png",
      "32": "/images/get_started32.png",
      "48": "/images/get_started48.png",
      "128": "/images/get_started128.png"
    }
  },
  "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", "scripting", "activeTab"],
  "host_permissions": ["<all_urls>"]
}
js 复制代码
// background
chrome.runtime.onInstalled.addListener(async () => {
  chrome.tabs.create({
    url: "demo.html",
  });
});
demo.js 复制代码
function func() {
  document.body.style.background = "red";
}

function funcGoogle() {
  document.body.style.background = "green";
}

document.getElementById("start").addEventListener("click", async () => {
  const googleTabs = await chrome.tabs.query({
    url: "https://www.google.com/*",
  });

  const baiduTabs = await chrome.tabs.query({
    url: "https://www.baidu.com/*",
  });

  console.log(googleTabs, baiduTabs);
  baiduTabs.forEach((tab) => {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      func,
    });
  });

  googleTabs.forEach((tab) => {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      func: funcGoogle,
    });
  });
});
html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>scripting 演示效果</h1>

    <h1>点下面的按钮,可以让google变绿,百度变红</h1>
    <button id="start">start</button>
    <script src="./demo.js"></script>
  </body>
</html>

演示效果:

四、资源

上面的案例我放在github仓库啦:地址在这里

插件系列文章:

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

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

相关推荐
并不会1 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、1 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜1 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师1 小时前
CSS的三个重点
前端·css
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具6 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端