chrome扩展训练营 | 3.谷歌扩展开发能力详解

1,598 阅读23分钟

王志远,微医前端技术部

系列文目录

小节主题文档期待产出补充
学习起始篇juejin.cn/post/714860…扩展能做到什么/如何学习扩展/扩展基础组成概念
开发调试发布juejin.cn/post/714861…熟悉谷歌扩展的开发流程/调试流程/发布流程
谷歌扩展核心机制详解:数据流处理能力、UI 能力、浏览器特性(历史/书签/下载/网络请求/等等)本文扩展能力知识体系学习,根据文档实现所有相关 demo
综合实战:一键合并窗口、消息通知一键合并窗口、消息通知、实现自己 promisify 版谷歌 api
框架升级:Vue 开发实现一图一诗(新开 tab 页打开自己的内容)使用 Vue 开发扩展,实现自己对扩展脚手架
落地:debug-plugin 集成谷歌扩展原调试插件重构为谷歌扩展并集成进项目

本文前言

本文我们将进入具体的 chrome 扩展开发世界,体系化的学习这一技能;这也是本系列文章的基石,重中之重!内容也很多,期待是日后开发的小伙伴想到需求就可以在这搜索关键词快速想起来,然后 cv 实现需求。包括

  • 注册文件Manifest
  • 数据流处理能力:用户页面/后台/跨域/数据存储
  • 扩展UI能力:browser_action/popup/右键菜单/桌面提醒/地址栏
  • 浏览器特性:书签/cookies/历史/标签/自定义页面/书签/下载/网络请求

学完本篇,你将对chrome扩展的能力了然于胸,跟上跟上

Manifest.json

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

信息字段

必须包含
  • name:扩展名
  • version:扩展版本
  • manifest_version:目前支持 2/3

可选

可以对谷歌扩展进行模块划分

  • 弹出层:涉及 popup / browser_action / page_action

  • 全局运行后台:background,存在如下字段(一般只配置 scripts)

    • scripts:Chrome 会在扩展启动时自动创建一个包含所有指定脚本的页面
    • page:Chrome 会将指定的 html 文件作为后台页面运行
    • persistent:默认 true,表示无论扩展是否在工作,都会一直在后台运行;设置为 false 可以有效减少对内存的消耗(也就是 Chrome 提出来的 Event Page)
  • 用户页面:content_script

  • 选项页面:options_page

{
  // 清单文件的版本,这个必须写,而且必须是 2
  "manifest_version": 2,
  // 插件的名称
  "name": "demo",
  // 插件的版本
  "version": "1.0.0",
  // 插件描述
  "description": "简单的 Chrome 扩展 demo",
  // 图标,一般偷懒全部用一个尺寸的也没问题
  "icons": {
    "16": "img/icon.png",
    "48": "img/icon.png",
    "128": "img/icon.png"
  },
  // 会一直常驻的后台 JS 或后台页面
  "background": {
    // 2 种指定方式,如果指定 JS,那么会自动生成一个背景页
    "page": "background.html"
    //"scripts": ["js/background.js"]
  },
  // 浏览器右上角图标设置,browser_action、page_action、app 必须三选一
  "browser_action": {
    "default_icon": "img/icon.png",
    // 图标悬停时的标题,可选
    "default_title": "这是一个示例 Chrome 插件",
    "default_popup": "popup.html"
  },
  // 当某些特定页面打开才显示的图标
  /*"page_action":
	{
		"default_icon": "img/icon.png",
		"default_title": "我是 pageAction",
		"default_popup": "popup.html"
	},*/
  // 需要直接注入页面的 JS
  "content_scripts": [
    {
      //"matches": ["http://*/*", "https://*/*"],
      // "<all_urls>" 表示匹配所有地址
      "matches": ["<all_urls>"],
      // 多个 JS 按顺序注入
      "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
      // JS 的注入可以随便一点,但是 CSS 的注意就要千万小心了,因为一不小心就可能影响全局样式
      "css": ["css/custom.css"],
      // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认 document_idle
      "run_at": "document_start"
    },
    // 这里仅仅是为了演示 content-script 可以配置多个规则
    {
      "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
      "js": ["js/show-image-content-size.js"]
    }
  ],
  // 权限申请
  "permissions": [
    "contextMenus", // 右键菜单
    "tabs", // 标签
    "notifications", // 通知
    "webRequest", // web 请求
    "webRequestBlocking",
    "storage", // 插件本地存储
    "http://*/*", // 可以通过 executeScript 或者 insertCSS 访问的网站
    "https://*/*" // 可以通过 executeScript 或者 insertCSS 访问的网站
  ],
  // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
  "web_accessible_resources": ["js/inject.js"],
  // 插件主页,这个很重要,不要浪费了这个免费广告位
  "homepage_url": "https://www.baidu.com",
  // 覆盖浏览器默认页面
  "chrome_url_overrides": {
    // 覆盖浏览器默认的新标签页
    "newtab": "newtab.html"
  },
  // Chrome40 以前的插件配置页写法
  "options_page": "options.html",
  // Chrome40 以后的插件配置页写法,如果 2 个都写,新版 Chrome 只认后面这一个
  "options_ui": {
    "page": "options.html",
    // 添加一些默认的样式,推荐使用
    "chrome_style": true
  },
  // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
  "omnibox": { "keyword": "go" },
  // 默认语言
  "default_locale": "zh_CN",
  // devtools 页面入口,注意只能指向一个 HTML 文件,不能是 JS 文件
  "devtools_page": "devtools.html"
}

更多字段可见:developer.chrome.com/extensions/…

数据流基础能力

  • content_script:操作用户正在浏览的页面
  • background:常驻后台的逻辑实现
  • 跨域请求
  • 扩展通信
  • 数据存储

操作用户正在浏览的页面

这其实可以理解为就是 xss 攻击:往用户当前的页面注入自己的逻辑脚本。这里涉及的是 content_script,数组类型,组成元素包含字段

  • matches:用于判断哪些页面会被注入脚本
  • exclude_matches:定义了哪些页面不会被注入脚本
  • css/js:指定需要被注入的 css 和 js 文件
  • run_at:定义何时进行注入,
  • all_frames:定义脚本是否会被注入到 iframes 中
  • include_globs / excude_globs:全局 URL 匹配

总结:【最终脚本是否会被注入由 matches、exclude_matches 、include_globs 和 exclude_globs 的值共同决定】。

简单的说,如果 URL 匹配 mathces 值的同时也匹配 include_globs 的值,会被注入;如果 URL 匹配 exclude_matches 的值或者匹配 exclude_globs 的值,则不会被注入。

注意点

  1. content_script 只共享 DOM:这意味着是和当前页面的 js 隔离的
  2. DOM 的自定义属性不会被共享

实战案例:永远找不到的百度搜索

  1. 只处理谷歌搜索页:matches 中配置["*://www.baidu.com/"]
  2. 隐藏搜索按钮:引入 content_scripts 中的 js,实现 display:none 逻辑

项目目录

.
|-- js
|   |-- cannot_touch.js
|-- manifest.json

manifest.json

{
  "manifest_version": 2,
  "name": "永远点不到的搜索按钮",
  "version": "1.0",
  "description": "让你永远也点击不到 baidu 的搜索按钮",
  "content_scripts": [
    {
      "matches": ["*://www.baidu.com/"],
      "js": ["js/cannot_touch.js"]
    }
  ]
}

js/cannot_touch.js

function btn_move(el, mouseLeft, mouseTop) {
  debugger;
  var leftRnd = (Math.random() - 0.5) * 20;
  var topRnd = (Math.random() - 0.5) * 20;
  var btnLeft = mouseLeft + (leftRnd > 0 ? 100 : -100) + leftRnd;
  var btnTop = mouseTop + (topRnd > 0 ? 30 : -30) + topRnd;
  btnLeft =
    btnLeft < 100
      ? btnLeft + window.innerWidth - 200
      : btnLeft > window.innerWidth - 100
      ? btnLeft - window.innerWidth + 200
      : btnLeft;
  btnTop =
    btnTop < 100
      ? btnTop + window.innerHeight - 200
      : btnTop > window.innerHeight - 100
      ? btnTop - window.innerHeight + 200
      : btnTop;
  el.style.position = "fixed";
  el.style.left = btnLeft + "px";
  el.style.top = btnTop + "px";
}

function over_btn(e) {
  if (!e) {
    e = window.event;
  }
  btn_move(this, e.clientX, e.clientY);
}

document.getElementById("su").onmouseover = over_btn;

效果如下

常驻后台的逻辑实现

全局运行后台:background 字段

  • scripts:Chrome 会在扩展启动时自动创建一个包含所有指定脚本的页面
  • page:Chrome 会将指定的 html 文件作为后台页面运行
  • persistent:默认 true,表示无论扩展是否在工作,都会一直在后台运行;设置为 false 可以有效减少对内存的消耗(也就是 Chrome 提出来的 Event Page)

在 manifest.json 中配置

{
  ...
  "background": {
        "scripts": [
            "js 文件相对路径"
        ]
    },
}

注意:这个你永远看不到 background 的界面,只能调试它的代码,调试方法如下

  1. 访问扩展管理后台
  2. 访问背景页

实战:监视百度网站在线状态

要求如果网站在线就将扩展图标显示为绿色,不在线就显示为红色。实现思路如下

  1. 每隔 5 秒发起一次连接请求,请求失败则代表不在线
  2. 当需要改变图标时,需要调用chrome.browserAction.setIcon

manifest.json

{
  "manifest_version": 2,
  "name": "百度在线状态",
  "version": "1.0",
  "description": "监视百度是否在线",
  "icons": {
    "16": "images/icon16.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  },
  "browser_action": {
    "default_icon": {
      "19": "images/icon19.png",
      "38": "images/icon38.png"
    }
  },
  "background": {
    "scripts": ["js/status.js"]
  },
  "permissions": ["http://www.google.cn/"]
}

js/status.js

function httpRequest(url, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      callback(true);
    }
  };
  xhr.onerror = function () {
    callback(false);
  };
  xhr.send();
}

setInterval(function () {
  httpRequest("http://www.google.cn/", function (status) {
    chrome.browserAction.setIcon({
      path: "images/" + (status ? "online.png" : "offline.png"),
    });
  });
}, 5000);

跨域请求

域名/端口/协议有任意不同,则视为跨域,浏览器默认阻止请求,但在扩展中不受控制,只需要在权限属性permissions中声明需要跨域的权限即可。

比如:请求百度内容

{
  "permissions": ["*://*.baidu.com/*"]
}

我们可以封装一个请求库

function httpRequest(url, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      callback(xhr.responseText);
    }
  };
  xhr.send();
}

实战:显示百度对名称的搜索结果

  1. 申请接口请求授权:manifest 中配置【permissions】
  2. 获取用户输入关键词,调用接口,将结果返回:逻辑实现在 my_result.js 中

效果如下

项目结构

.
|-- js
|   |-- my_result.js
|-- manifest.json
|-- popup.html

manifest.json

{
  "manifest_version": 2,
  "name": "显示百度对名称对搜索结构",
  "version": "1.0",
  "description": "显示百度对名称对搜索结构",
  "browser_action": {
    "default_title": "显示百度对名称对搜索结构",
    "default_popup": "popup.html"
  },
  "permissions": ["*://*.baidu.com/*"],
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}

popup.html

<html>
  <head>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      body {
        width: 400px;
        height: 100px;
      }

      div {
        line-height: 100px;
        font-size: 42px;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <input type="text" id="keyword" placeholder="keyword" />
    <div id="start">start</div>
    <div id="resultDiv"></div>
    <script src="js/my_result.js"></script>
  </body>
</html>

js/my_result.js

function typeCheck(type) {
  let types = [
    "Array",
    "Object",
    "Number",
    "String",
    "Undefined",
    "Boolean",
    "Function",
    "Map",
  ];
  let map = {};
  types.forEach((type) => {
    map[type] = function (target) {
      return Object.prototype.toString.call(target) == `[object ${type}]`;
    };
  });
  return map[type];
}
function creatDom(domOpts, parentSelector) {
  let { tag, text, opts = {}, data = {}, children = [] } = domOpts;
  if (Object.keys(opts).length === 0) {
    opts = data;
  }
  //创建一个 div
  var dom = document.createElement(tag);
  if (text) {
    dom.innerHTML = text; //设置显示的数据,可以是标签.
  }

  for (const key in opts) {
    if (key === "style" && typeCheck("Object")(opts[key])) {
      let styleOpts = opts[key];
      for (const styleKey in styleOpts) {
        dom.style[styleKey] = styleOpts[styleKey];
      }
    }
    if (key === "style" && typeCheck("String")(opts[key])) {
      dom[key] = opts[key];
    }
    if (key === "class") {
      dom.className = opts[key];
    }
    if (key === "props") {
      let propOpts = opts[key];
      for (const propKey in propOpts) {
        dom[propKey] = propOpts[propKey];
      }
    }

    if (key === "attrs") {
      let propOpts = opts[key];
      for (const propKey in propOpts) {
        dom[propKey] = propOpts[propKey];
      }
    }
    if (key === "on") {
      let eventOpts = opts[key];
      for (const eventKey in eventOpts) {
        let fn = eventOpts[eventKey];
        dom.addEventListener(eventKey, fn);
      }
    }
  }
  children.forEach((child) => {
    return dom.appendChild(creatDom(child));
  });
  if (parentSelector) {
    el = typeCheck("String")(parentSelector)
      ? document.querySelector(parentSelector)
      : el;
    el.appendChild(dom);
  }
  return dom;
}
function httpRequest(url, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      callback(xhr.responseText);
    }
  };
  xhr.send();
}

document.querySelector("#start").addEventListener("click", () => {
  let wd = encodeURIComponent(document.querySelector("#keyword").value);
  // 百度接口
  let searchUrl = `https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=37155,36548,37115,37357,36885,37403,37404,36789,37259,26350,37344,37371&wd=${wd}&req=2&bs=%E7%8E%8B%E5%BF%97%E8%BF%9C&pbs=%E7%8E%8B%E5%BF%97%E8%BF%9C&csor=2&pwd=%E7%8E%8B%E5%BF%97%E8%BF%9C&cb=callback&_=1663922576575`;
  // 这个函数的名称不能修改 原因是百度搜索结果采用 jsonp 接收
  function callback(searchResult) {
    let liDomOpts = searchResult.g.map((g) => {
      let { q } = g;
      return {
        type: "li",
        text: q,
      };
    });
    let domOpts = {
      tag: "ul",
      children: liDomOpts,
    };
    creatDom(domOpts, "#resultDiv");
  }
  httpRequest(searchUrl, function (result) {
    debugger;
    // 这里触发会导致错误,所以需要在 manifest.json 中配置内容协议【"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"】
    eval(result);
    // document.getElementById('resultDiv').innerText = result;
  });
});

谷歌扩展的通信

  • 扩展间的通信:popup/background/content_script
  • 不同扩展间的通信:

这些通信都依赖于两对 Api

  • 短连接:chrome.runtime.sendMessage 和 chrome.runtime.onMessage
  • 长连接:chrome.runtime.connect 和 chrome.runtime.onConnect

短连接

发送
chrome.runtime.sendMessage(extensionId, message, options, callback);
  • extensionId:string | number,所发送消息的目标扩展,如果不指定,则默认为发起此消息的扩展本身
  • message:Any,消息内容
  • options:Object,配置对象
  • callback:Function,回调,会用于接受消息结果
监听
chrome.runtime.onMessage.addListener(function (
  message,
  sender,
  senderResponse
) {});
  • callback:Function,回调,会用于接受消息结果,参数如下
    • message:Any,消息内容
    • sender:Object,消息发送者相关信息,属性如下
      • tab:发起消息的标签
      • id
      • url
      • tlsChannelId:
    • senderResponse:响应函数,用于回传信息
实战:popup 和 background 消息通信

popup.js

chrome.runtime.sendMessage("hello", function (response) {
  alert(response);
});

background.js

chrome.runtime.onMessage.addListener(function (
  message,
  sender,
  senderResponse
) {
  senderResponse(`${message} from background`);
});

存储数据

存在三种数据存储方式:localStorage/chrome.storage 和 Web SQL Database

类型特点缺点补充
localStorage使用方法简单只能存储字符串基于域名
chrome.storage.StorageArea支持任意类型数据异步;需要声明权限 storage这里的 StorageArea 是指【local 或 sync】;如果设置 sync,数据会自动同步;content_script 可以直接不经过 background 读取数据;隐身模式下也可以读取之前数据
Web SQL Database数据存储空间大复杂,需要写 sql

方法

我们详解下 chrome.storage.StorageArea,存在五个方法

get:读取数据
chrome.storage.StorageArea.get(keys, callback);

其中 keys 支持四种类型

  • 字符串:同 localStorage
  • 数组:一次取多个
  • 对象:先读取对象属性名数组数据,如果属性名不存在对应数据,则返回对应属性值(相当于设置默认值了)
  • null:返回所有存储数据
set:写入数据
chrome.storage.StorageArea.set(items, callback);

items:Object,其属性值存在三种情况

  • 字符串|数字型|数组型:同 localStorage
  • 对象|函数:会被存储为{}
  • 日期 | 正则:会存储为字符串形式
remove:删除数据
chrome.storage.StorageArea.remove(keys, callback);

其中 keys 支持四种类型

  • 字符串:同 localStorage
  • 数组:一次删除多个
clear:删除所有
chrome.storage.StorageArea.clear(callback);
getBytesInUse:获取数据所占用的空间

单位是字节

chrome.storage.StorageArea.getBytesInUse(keys, callback);

其中 keys 支持三种类型

  • 字符串:同 localStorage
  • 数组:一次取多个
  • null:返回所有存储数据

钩子

存在一个 onChanged 事件,用于监听存储区的数据发生改变。

chrome.storage.onChanged.addListener(function (changes, StorageArea) {});
  • changes:词典对象,key 是更改的属性名称;val 包含两个属性
    • oldValue
    • newValue
  • StorageArea:local 或 sync

扩展的 UI 界面

  • browser_action | page_action :Object
    • default_icon:Object?,工具栏图标信息,19/38/54,默认是 19,如果是视网膜屏则会取相应倍数的图片;

browserAction

存在对应配置对象chrome.browserAction

default_icon:图标设置

对应 menifest.json 文件中的配置(也可以直接设置一张图片)

{
  ...
  “browser_action": {
    "default_icon": {
        "19": "images/icon19.png",
        "38": "images/icon38.png"
    }
	}
}
setIcon:动态修改图标
chrome.browserAction.setIcon(detail, callback);
  • detail:Object,图标信息,存在如下属性(不必同时设置 imageData 和 path)
    • imageData:图片像素数据(可以从 canvas 获取)
    • path:Object | String,对象时结构为{size: imagePath},imagePath 是图片在扩展根目录下的相对路径
    • tabId:限定了浏览哪个标签页时,图标会被修改

实战:图标不断旋转的扩展

实现图标一直在旋转,一秒旋转一圈的效果

  1. 我们找 20 张图片,内容相同,存在倾斜度
  2. 在后台(即 background)实现定时器,每 50 毫秒替换一次图片,这样 1s 后就旋转了一次

项目目录(此项目需要在 github 上获取图片资源)

.
|-- js
|   |-- background.js
|-- manifest.json

manifest.json

{
  "browser_action": {
    "default_icon": {
      "19": "images/icon19.png",
      "38": "images/icon38.png"
    }
  }
}

background.js

function chgIcon(index) {
  if (index === undefined) {
    index = 0;
  } else {
    index = index % 20;
  }
  chrome.browserAction.setIcon({
    path: { 19: "images/icon19_" + index + ".png" },
  });
  chrome.browserAction.setIcon({
    path: { 38: "images/icon38_" + index + ".png" },
  });
  setTimeout(function () {
    chgIcon(index + 1);
  }, 50);
}
chgIcon();

效果

2022-09-24 14.07.18

default_popup:Popup 页面,点击图标的弹出层

对应 menifest.json 文件中的配置

{
  ...
  “browser_action": {
    "default_popup": "popup.html"
	}
}
设计理念
  • 关闭后会销毁所有状态,相当于关闭了个标签页,所以建议只用于数据展示
  • 不支持内嵌 script,需要引入外部 script 实现逻辑
  • 建议先给出页面 body 的尺寸(小于 500px),避免用户在最开始看到一个很小的窗口
  • 尝试自定义右键菜单
  • 设计滚动条

default_popup:标题和 badge

对应 menifest.json 文件中的配置

{
  ...
  “browser_action": {
    "default_popup": "popup.html"
	}
}
设计理念
  • 关闭后会销毁所有状态,相当于关闭了个标签页,所以建议只用于数据展示
  • 不支持内嵌 script,需要引入外部 script 实现逻辑
  • 建议先给出页面 body 的尺寸(小于 500px),避免用户在最开始看到一个很小的窗口
  • 尝试自定义右键菜单
  • 设计滚动条
setBadgeText:设置 badge 显示的内容(字体颜色默认白色,不可修改)
chrome.browserAction.setBadgeText({
  txt: "Dog",
});
setBadgeBackgroundColor:设置 badge 显示的背景色(默认红色)
chrome.browserAction.setBadgeBackgroundColor({
  color: [0, 255, 0, 128],
});

Page Action

和 browser 类似,没有 badge;存在主要是为了在特定标签页下才显示,存在处理对象及方法如下

chrome.pageAction.show(integer tabId);
chrome.pageAction.hide(integer tabId);

右键菜单

存在对应配置对象chrome.contextMenus,包含三个操作方法:create/update 和 remove

对应 menifest.json 文件中的配置

{
  ...
  "icons": {
    "16": "icon16.png"
  },
  “permissions": {
    "contxtMenus"
	}
}

create:创建

chrome.contextMenus.create({
  type: "类型",
  title: "标题",
  id: "id",
  contexts: ['何时触发此菜单'],
  documentUrlPatterns: ["限定 url"],
  targetUrlPatterns: ["限定图片/视频和音频等 url"],
  onclick: Function // 当菜单项被点击时触发的函数
  ...
})

配置对象更多属性可见:open.chrome.360.cn/extension_d…

通常由后台页面来调用,即通过后台页面创建自定义菜单。如果后台页面是Event Page,通常在 onIstalled 事件中调用。

type:菜单类型

存在四种菜单

  • 普通菜单:normal,支持下级菜单
  • 复选菜单:checkbox
  • 单选菜单:radio,连续对单选菜单会被认为是对同一设置的选项,且单选菜单会自动在两端生成分割线
  • 分割线:separator
// 普通菜单 ==========
chrome.contextMenus.create({
  type: "normal",
  title: "Menu A",
  id: "a",
});
// 子菜单
chrome.contextMenus.create({
  type: "normal",
  title: "Menu F",
  id: "f",
  parentId: "a",
});
chrome.contextMenus.create({
  type: "normal",
  title: "Menu G",
  id: "g",
  parentId: "a",
});
// 单选菜单 ===========
chrome.contextMenus.create({
  type: "radio",
  title: "Menu B",
  id: "b",
  checked: true,
});
chrome.contextMenus.create({
  type: "radio",
  title: "Menu C",
  id: "c",
});
// 复选菜单 ===========
chrome.contextMenus.create({
  type: "checkbox",
  title: "Menu D",
  id: "d",
  checked: true,
});
// 分割线
chrome.contextMenus.create({
  type: "separator",
});
// 复选菜单 ===========
chrome.contextMenus.create({
  type: "checkbox",
  title: "Menu E",
  id: "e",
});

contexts:何处右键触发

Array,枚举数组,支持值如下

  • all:
  • page:默认值,在所有页面唤起右键菜单时都显示自定义菜单
  • frame
  • selection:选中文本
  • link
  • editable
  • image
  • video
  • audio
  • launcher:只对 Chrome 应用有效

修改方法

  • update:动态更改菜单属性,需指定需要更改菜单等 id 和所需要更改的属性
  • remove:删除指定菜单
  • removeAll:删除所有菜单

实战:使用 百度 翻译当前用户所选文本

效果

2022-09-24 14.13.49

项目目录

.
|-- images
|   |-- icon16.png
|-- js
|   |-- background.js
|   |-- content.js
|-- manifest.json

Manifest.json

{
  "manifest_version": 2,
  "name": "baidu Translate",
  "version": "1.0",
  "description": "Translate what you select with baidu",
  "background": {
    "scripts": ["js/background.js"]
  },
  "icons": {
    "16": "images/icon16.png"
  },
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["js/content.js"]
    }
  ],
  "permissions": ["contextMenus"]
}

background.js

chrome.contextMenus.create({
  type: "normal",
  title: "使用百度翻译……",
  contexts: ["selection"],
  id: "cn",
  onclick: translate,
});

function translate(info, tab) {
  var url = `https://fanyi.baidu.com/translate?aldtype=16047&query=${info.selectionText}#zh/en/${info.selectionText}`;
  window.open(url, "_blank");
}

chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
  chrome.contextMenus.update("cn", {
    title: "使用百度翻译“" + message + "”",
  });
});

content.js

window.onmouseup = function () {
  var selection = window.getSelection();
  if (selection.anchorOffset != selection.extentOffset) {
    chrome.runtime.sendMessage(selection.toString());
  }
};

桌面提醒

menifest.json 中的配置

  • permisssion:申请桌面通知权限
  • web_accessible_resource:桌面窗口显示的图片也需要申请权限
{
  ...
  "permisssion": [
    "notifications"
  ],
  "web_accessible_resource": [
    "images/*.png"
  ]
}

对应处理对象webkitNotification 和 chrome.notification

let notification = webkitNotification.createNotification(
  "",
  "Notification Demo",
  "Merry you"
);
notification.show();
// notification.cancel() 主动关闭

notification 实例存在如下钩子

  • ondisplay
  • onerror
  • onclose
  • onclick

更多信息可见:dev.chromium.org/developers/…

Omnibox:多功能栏(地址栏)

menifest.json 中的配置

{
  ....
  "omnibox": {
 		"keyword": "xxx"
	},
  "icons": {
    "16": "小图标"
  }
}
  • keyword 是指用户输入的指定关键词

对应处理对象 omnibox

chrome.omnibox;

存在方法

  • onInputStarted:开始输入
  • onInputChanged:输入变化
  • onInputEntered:执行指令,即回车或鼠标点击
  • onInputCancelled:取消输入
chrome.omnibox.onInputChanged.addListener(function (text, suggest) {
  suggest([
    {
      content: text,
      description: "Search " + text + " in Wikipedia",
    },
  ]);
});

chrome.omnibox.onInputEntered.addListener(function (text, disposition) {
  switch (disposition) {
    case "currentTab": //do something in the current tab
      break;
    case "newForegroundTab": //do something in a new tab and active it
      break;
    case "newBackgroundTab": //do something in a new tab
      break;
  }
});

实战:实时查询美元汇率

实现效果

项目结构

.
|-- images
|   |-- icon16.png
|-- js
|   |-- background.js
|-- manifest.json

manifest.json

{
  "manifest_version": 2,
  "name": "weiyi USD Price",
  "version": "1.0",
  "description": "微医自己的汇率查询工具",
  "background": {
    "scripts": ["js/background.js"]
  },
  "icons": {
    "16": "images/icon16.png"
  },
  "omnibox": {
    "keyword": "usd"
  },
  "permissions": ["*://query.yahooapis.com/*"]
}

Js/background.js

function httpRequest(url, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      callback(xhr.responseText);
    }
  };
  xhr.send();
}

function updateAmount(amount, exchange) {
  amount = Number(amount);
  if (isNaN(amount) || !amount) {
    exchange([
      {
        content: "$1 = ¥" + price,
        description: "$1 = ¥" + price,
      },
      {
        content: "¥1 = $" + (1 / price).toFixed(6),
        description: "¥1 = $" + (1 / price).toFixed(6),
      },
    ]);
  } else {
    exchange([
      {
        content: "$" + amount + " = ¥" + (amount * price).toFixed(2),
        description: "$" + amount + " = ¥" + (amount * price).toFixed(2),
      },
      {
        content: "¥" + amount + " = $" + (amount / price).toFixed(6),
        description: "¥" + amount + " = $" + (amount / price).toFixed(6),
      },
    ]);
  }
}

function gotoYahoo(text, disposition) {
  window.open("http://finance.yahoo.com/q?s=USDCNY=X");
}

var url =
  "http://query.yahooapis.com/v1/public/yql?q=select%20Rate%20from%20yahoo.finance.xchange%20where%20pair%20in%20(%22USDCNY%22)&env=store://datatables.org/alltableswithkeys&format=json";
var price;

httpRequest(url, function (r) {
  price = JSON.parse(r);
  price = price.query.results.rate.Rate;
  price = Number(price);
});

chrome.omnibox.setDefaultSuggestion({
  description: "微医自己的汇率查询工具呀~",
});

chrome.omnibox.onInputChanged.addListener(updateAmount);

chrome.omnibox.onInputEntered.addListener(gotoYahoo);

浏览器特性

  • 书签
  • cookies
  • 历史
  • 标签
  • 自定义页面
  • 下载
  • 网络请求
  • 代理
  • 系统信息

书签

支持能力

  • 读取书签
  • 添加/分类/排序书签

manifest.json 中的处理

"permissions": [
  "bookmarks"
]
对应处理对象
chrome.bookmarks;
其他数据结构
// 书签对象 bookmark
{
  id: String,标签 id,chrome 管理
  title: String,书签标题
  parentId: String?,父节点 id
  index: Number?, 书签在父节点中的位置,从 0 开始
  url: String?,书签对应超链接
  dateAdded: String?,从 1970.1.1 至创建时间经过的毫秒数
  dateGroupModified: String?,从 1970.1.1 至修改时间经过的毫秒数
  children: Array?,子书签数组
}
存在方法

create:创建书签

move:调整书签位置,支持跨越父节点

update:更新书签,更新时未指定的属性将不会更改

移除书签

  • remove:移除书签(不支持包含书签的书签分组)
  • removeTree:删除包含书签的书签分组

获取书签信息

  • 书签树
    • getTree:获取完整书签树
    • getSubTree:获取指定节点的后代书签节点(包含父节点)
    • getChildren:获取指定节点的子书签节点(不包含父节点)
  • get:返回指定节点(可指定多个)不包含 children 属性的书签对象数组
  • getRecent:获取指定数量的最近添加书签
  • search:返回指定字符串模糊查询的书签对象
create:创建书签 / 书签分类
chrome.bookmarks.create(bookmark, callback);

chrome.bookmarks.create(
  {
    parentId: "1",
    index: 0,
    title: "Google",
    url: "http://www.google.com/",
  },
  function (bookmark) {
    console.log(bookmark);
  }
);

注意:

  1. 根节点 id 时"0",其下不允许创建书签和书签分组,默认有三个书签分组:书签栏 / 其他书签 / 移动设备书签。
  2. 如果创建时不指定 parentId,则会默认加入到其他书签中
  3. create 支持指定的书签属性只有上述代码中所列出的 4 个【parentId、index 、title 和 url】,其他属性均不支持指定。
  4. 如果不指定 index,这个书签就将自动添加到相应父节点的尾部

当创建的书签不包含 url 属性,则视为书签分类

move:调整书签位置
chrome.bookmarks.move(
  bookmarkId,
  {
    parentId: String,
    index: Number,
  },
  callback
);

chrome.bookmarks.move(
  "16",
  {
    parentId: "7",
    index: 4,
  },
  function (bookmark) {
    console.log(bookmark);
  }
);
update:更新书签
chrome.bookmarks.move(bookmarkId, bookmark, callback);

chrome.bookmarks.update(
  "16",
  {
    title: "Gmail",
    url: "https://mail.google.com/",
  },
  function (bookmark) {
    console.log(bookmark);
  }
);
remove removeTree :删除书签
chrome.bookmarks.remove(bookmarkId, function () {});

chrome.bookmarks.removeTree(bookmarkId, function () {});

chrome.bookmarks.remove("16", function () {
  console.log("Bookmark 16 has been removed.");
});

chrome.bookmarks.removeTree("6", function () {
  console.log("Bookmark group 6 has been removed.");
});
书签树

getTree:获取完整书签树

chrome.bookmarks.getTree(function (bookmarkArray) {
  console.log(bookmarkArray);
});

getSubTree:获取指定节点的后代书签节点(包含父节点)

chrome.bookmarks.getSubTree(bookmarkId, function (bookmarkArray) {
  console.log(bookmarkArray);
});

getChildren:获取指定节点的子书签节点(不包含父节点)

chrome.bookmarks.getChildren(bookmarkId, function (bookmarkArray) {
  console.log(bookmarkArray);
});
get:返回指定节点不包含 children 属性的书签对象数组
chrome.bookmarks.get(["16", "17"], function (bookmarkArray) {
  console.log(bookmarkArray);
});
getRecent:获取指定数量的最近添加书签
chrome.bookmarks.getRecent(5, function (bookmarkArray) {
  console.log(bookmarkArray);
});
search:返回指定字符串模糊查询的书签对象
chrome.bookmarks.search("google", function (bookmarkArray) {
  console.log(bookmarkArray);
});

钩子函数

  • onCreated:书签创建行为回调
  • onRemoved:书签删除行为回调
  • onChanged:书签更新行为回调
  • onMoved:书签移动行为回调
  • onChildrenReordered:书签分组下子节点顺序更改回调
  • onImportBegan / onImportEnded:导入书签开始/结束回调
onCreated:书签创建行为回调
chrome.bookmarks.onCreated.addListener(function (bookmark) {
  console.log(bookmark);
});
onRemoved:书签删除行为回调
chrome.bookmarks.onRemoved.addListener(function (id, bookmark) {
  console.log(bookmark);
});
onChanged:书签更新行为回调
chrome.bookmarks.onChanged.addListener(function (id, changInfo) {
  let { title, url } = changInfo;
});
onMoved:书签移动行为回调
chrome.bookmarks.onMoved.addListener(function (id, moveInfo) {
  let { parentId, index, oldParentId, oldIndex } = changInfo;
});
onChildrenReordered:书签分组下子节点顺序更改回调
chrome.bookmarks.onChildrenReordered.addListener(function (id, reorderInfo) {
  //	reorderInfo 是包含顺序更改后子节点 id 的数组
});
onImportBegan / onImportEnded:导入书签开始/结束回调
chrome.bookmarks.onImportBegan.addListener(function () {});
chrome.bookmarks.onImportEnded.addListener(function () {});

Cookies

manifest.json 中的处理

{
  ...
  "permissions": [
    "cookies",
    "*://*.域名.com", // 如果想管理所有域名下的 cookies 可以配置为"<all_urls>"
  ]
}

对应处理对象

chrome.cookies;
其他数据结构
// cookie 对象 cookie
{
  name: String,名称
  value: String,值
  domain: 域
  hostOnly:是否只允许完全匹配 domain 的请求访问
  path:路径
  secure:是否只允许安全连接调用
  httpOnly:是否禁止客户端调用
  session:是否是 session cookie
  expirationDate:过期时间,默认在浏览器关闭时销毁
  storeId:包含此 cookie 的 cookie stroe id
}
// cookie store 对象
{
  id: String,cookie store 的 id
  tabIds: 共享这个 cookie store 的所有 tabId 数组
}
存在方法

读取 cookie

  • get:读取指定 name/url/storeId?的 Cookie,name 和 url 必须指定
    • 如果同 url 下存在多个 name 相同 cookie,返回 path 最长;如果长度相同,返回最早创建的。
  • getAll:获取所有符合条件的 Cookies,支持匹配条件包括 cookie 的任何一个或多个属性,不指定则返回所有有权访问的 cookies

操作 cookie

  • set:设置 cookie;url 必须指定且具有访问权限(manifest 中配置),创建失败时回调回得到 null;如果不指定 expirationDate 则默认在浏览器关闭时销毁
  • remove:删除 cookie,指定 url/name 和 storeId

cookieStore

  • getAllCookieStores:获取全部的 cookie store
相关钩子
  • onChanged 事件:监听 cookie 的设置和删除动作
get:读取指定 name/url/storeId?的 Cookie
chrome.cookies.get(
  {
    url: "https://github.com",
    name: "dotcom_user",
  },
  function (cookie) {
    console.log(cookie.value);
  }
);
getAll:获取所有符合条件的 Cookies
chrome.cookies.getAll({}, function (cookies) {
  console.log(cookies);
});
set:设置 cookie
chrome.cookies.set(
  {
    url: "http://github.com/test_cookie",
    name: "TEST",
    value: "foo",
    secure: false,
    httpOnly: false,
  },
  function (cookie) {
    console.log(cookie);
  }
);
remove:删除 cookie
chrome.cookies.remove(
  {
    url: "http://www.google.com",
    name: "_ga",
  },
  function (result) {
    console.log(result);
  }
);
onChanged:监听 cookie 的设置和删除动作
chrome.bookmarks.onChanged.addListener(function (changeInfo) {
  let {
    removed, // 是否是删除行为
    cookie, // 被设置或删除的 cookie 对象
    cause, // cookie 变化原因,枚举值:evicted/expired/explicit/expired_overwrite/overwrites
  } = changeInfo;
});

历史

用于记录用户访问过页面的信息

支持能力

manifest.json 中的处理

{
  ...
  "permissions": [
    "history"
  ]
}

对应处理对象

chrome.history;
其他数据结构
// 历史对象 history
{
  id: ""
  url: ""
  title: 标题
  lastVisitTime: 上次访问时间
  visitCount: 访问次数
  typeCount: String,用户通过在地址栏键入访问此历史的次数
}
// 访问对象 visit
{
  id: 与指定 URL 匹配的对象的 id
  visitId: 是这个访问结果的 id, 唯一
  visitTime: 毫秒数
  referringVisitId
  transition: 此访问记录打开的方式
}

关于 transition 的补充:Chrome 对每个访问记录进行了划分,存在 11 类,对应属性就是 transition

常见四种

  • link:超链接打开
  • typed:地址栏输入网站打开
  • reload:刷新页面
  • form_submit:提交表单(脚本提交不算)

浏览器 UI 和设置两种

  • auto_bookmark:浏览器建议方式打开(菜单)
  • auto_toplevel:浏览器默认打开(浏览器主页或命令行附带参数)

嵌入式框架两种

  • auto_subframe:自动加载的嵌入式框架打开(常见广告打开方式)
  • manual_subframe:用户手动加载的嵌入式框架打开(用户操作商品菜单查看不同款式商品页面)

omnibox 搜索建议(地址栏)三种

  • generated:通过搜索建议打开
  • keyword:通过输入关键词生成的 URL 打开(和 generated 的区别就在于 url 不是搜索引擎生成的)
  • keyword_generated:通过输入关键词生成的 URL 打开
存在方法

读取历史

  • search:读取匹配指定文字/时间区间/条目的历史结果,结果是 history 对象数组
  • getVisits: 获取指定 URL 的访问结果,必须指定完整 URL,返回结果会精准匹配,结果是 visit 对象数组

写入历史

  • addUrl:添加历史,将特定的 url 以当前时间为访问时间添加至历史

删除历史

  • deleteUrl:删除指定历史
  • deleteRange:删除置顶时间段段历史
  • deleteAll:删除全部历史
相关钩子
  • onVisited:用户访问历史
  • onVisitRemoved:历史被删除
search:读取匹配指定文字/时间区间/条目的历史结果
// 搜索 1 天内与【王志远】有关的前二十条历史页面
chrome.history.search(
  {
    text: "王志远",
    startTime: new Date().getTime() - 24 * 3600 * 1000, // 距 1970 年 1 月 1 日的毫秒数
    endTime: new Date().getTime(), // 距 1970 年 1 月 1 日的毫秒数
    maxResults: 20,
  },
  function (historyItemArray) {
    console.log(historyItemArray);
  }
);
getVisits: 获取指定 URL 的访问结果
chrome.history.getVisits(
    url: 'http://www.google.com/'
}, function(visitItemArray){
    console.log(visitItemArray);
});
addUrl:添加历史
chrome.history.addUrl(
  {
    url: "http://baidu.com",
  },
  function () {
    console.log("baidu has been added to history.");
  }
);
删除历史
chrome.history.deleteUrl(
  {
    url: "http://www.baidu.com",
  },
  function () {
    console.log("baidu has been deleted from history.");
  }
);

chrome.history.deleteRange(
  {
    startTime: new Date().getTime() - 24 * 3600 * 1000,
    endTime: new Date().getTime(),
  },
  function () {
    console.log("History in past 24 hours has been deleted.");
  }
);

chrome.history.deleteAll(function () {
  console.log("All history has been deleted.");
});
onVisited:用户访问历史
chrome.history.onVisited.addListener(function (historyItem) {
  console.log(historyItem);
});
onVisitRemoved:历史被删除
chrome.history.onVisitRemoved.addListener(function (removedObject) {
  let {
    allHistory, // Boolean,如果历史均被删除则为 true,同时 urls 是[]
    urls, // 所有被删除历史的 url
  } = removedObject;
});

标签

manifest.json 中的处理

{
  ...
  "permissions": [
    "tabs" // 大多数操作是不需要申请这个权限的,但如果有关 url/title 和 favIconUrl 的操作,就需要声明这个权限
  ]
}

对应处理对象

chrome.tabs;
其他数据结构
// tab 标签对象
{
    id: 标签 id,
    index: 标签在窗口中的位置,以 0 开始,
    windowId: 标签所在窗口的 id,
    openerTabId: 打开此标签的标签 id,
    highlighted: 是否被高亮显示,
    active: 是否是活动的,
    pinned: 是否被固定,
    url: 标签的 URL,
    title: 标签的标题,
    favIconUrl: 标签 favicon 的 URL,
    status :标签状态,loading 或 complete,
    incognito: 是否在隐身窗口中,
    width: 宽度,
    height: 高度,
    sessionId: 用于 sessions API 的唯一 id
}
// queryObj
{
    active: 是否是活动的,
    pinned: 是否被固定,
    highlighted: 是否正被高亮显示,
    currentWindow: 是否在当前窗口,
    lastFocusedWindow: 是否是上一次选中的窗口,
    status: 状态,loading 或 complete,
    title: 标题,
    url: 所打开的 url,
    windowId: 所在窗口的 id,
    windowType: 窗口类型,normal、popup、panel 或 app,
    index: 窗口中的位置
}
存在方法

获取标签信息

  • get:获取指定 id 的标签
  • getCurrent:获取运行脚本本身所在的标签
  • query:获取所有符合指定条件的标签,条件对象见上

操作标签

  • create:创建标签,类似在浏览器打开新的标签页,但更丰富(url/窗口位置/活动状态等等)
  • duplicate:复制指定标签
  • update:更新标签,如果不指定 tabId 会默认更改当前窗口的活动标签(注意,如果将标签高亮,即 highlighted 属性更新为 true,active 属性也会随之改为 true)
  • move:移动标签,支持批量迁移
  • reload:重载指定标签(可以指定 bypassCache 为 true,强制刷新跳过缓存)
  • remove:删除标签

插入内容(content_script 只能想匹配条件的页面注入)

  • executeScript:向指定标签注入脚本
  • insertCSS:向指定标签注入 css 内容

特殊功能

  • detectLanguage:获取当前标签页面的现实语言
  • captureVisibleTab:获取指定窗口活动标签可见部分的截图(这里需要注意,扩展只有声明 activeTab 或<all_url>权限能获取到活动标签的截图)
相关钩子

标签的钩子特别多,我们分两类记录

易理解钩子

- onCreated
- onUpdated
- onMoved
- onActivated
- onHighlighted
- onDetached
- onAttached
- onRemoved
- onReplaced

不易理解钩子

onHighlighted:标签被高亮显示时所触发的事件,active 和 highlight 是有区别的,

  • active 是指标签在当前窗口中正被显示
  • highlight 只是标签的颜色被显示成了白色——如果此标签没有被选中正常情况下是浅灰色。

onDetached:标签脱离窗口时所触发的事件,导致此事件触发的原因是用户在两个不同的窗口直接拖拽标签。

onAttached:标签附着到窗口上时所触发的事件,同样是用户在两个不同的窗口直接拖拽标签导致的

onReplaced:标签被其他标签替换时触发的事件

get:获取指定 id 的标签
chrome.tabs.get(tabId, function (tab) {
  console.log(tab);
});
getCurrent:获取运行脚本本身所在的标签
chrome.tabs.getCurrent(function (tab) {
  console.log(tab);
});
query:获取所有符合置顶条件的标签
chrome.tabs.query(queryObj, function (tabArray) {
  console.log(tabArray);
});

chrome.tabs.query({
    active: true
}, function(tabArray){
    console.log(tabArray);
});
create:创建标签
chrome.tabs.create(
  {
    windowId: "", // 创建标签所在的窗口 id,不指定则在当前窗口打开
    index: 0,
    url: "http://www.baidu.com",
    active: true,
    pinned: false,
    openerTabId: "", // 打开此标签的标签 id,如果指定则所创建的标签必须与此标签在同一个窗口
  },
  function (tab) {
    console.log(tab);
  }
);
duplicate:复制指定标签
chrome.tabs.duplicate(tabId, function (tab) {
  console.log(tab);
});
update:更新标签
chrome.tabs.update(tabId, {
    url: 'http://www.baidu.com'
	}, function(tab){
}
move:移动标签
chrome.tabs.move(
  tabId | tabIds,
  {
    windowId: wId,
    index: 0, // 如果 index-1 则会把指定标签移动到指定窗口的最后面
  },
  function (tabs) {
    console.log(tabs);
  }
);
reload:重载指定标签
chrome.tabs.reload(
  tabId,
  {
    bypassCache: true,
  },
  function () {
    console.log("The tab has been reloaded.");
  }
);
remove:删除标签
chrome.tabs.remove(tabId, function () {
  console.log("The tab has been remove.");
});
detectLanguage:获取当前标签页面的现实语言
chrome.tabs.remove(tabId, function (lang) {
  console.log(lang);
});
captureVisibleTab:获取指定窗口活动标签可见部分的截图
chrome.tabs.captureVisibleTab(
  windowId,
  {
    format: "jpeg",
    quality: 50,
  },
  function (dataUrl) {
    console.log(dataUrl);
  }
);
executeScript:向指定标签注入脚本
// 注入脚本文件
chrome.tabs.executeScript(
  tabId,
  {
    file: "js/ex.js",
    allFrames: true,
    runAt: "document_start", // 支持 document_start document_end 和 document_idle
  },
  function (resultArray) {
    console.log(resultArray);
  }
);
// 注入脚本代码
chrome.tabs.executeScript(
  tabId,
  {
    code: 'document.body.style.backgroundColor="red"',
    allFrames: true,
    runAt: "document_start",
  },
  function (resultArray) {
    console.log(resultArray);
  }
);
insertCSS:向指定标签注入 css 内容
// 注入 css 文件
chrome.tabs.insertCSS(
  tabId,
  {
    file: "css/insert.css",
    allFrames: false,
    runAt: "document_start",
  },
  function () {
    console.log("The css has been inserted.");
  }
);
// 注入 css 内容
钩子实例
chrome.tabs.onCreated.addListener(function(tab){
    console.log(tab);
});

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){
    console.log('Tab '+tabId+' has been changed with these options:');
    console.log(changeInfo);
});

chrome.tabs.onMoved.addListener(function(tabId, moveInfo){
    console.log('Tab '+tabId+' has been moved:');
    console.log(moveInfo);
});

chrome.tabs.onActivated.addListener(function(activeInfo){
    console.log('Tab '+activeInfo.tabId+' in window '+activeInfo.windowId+' is active now.');
});

chrome.tabs.onHighlighted.addListener(function(highlightInfo){
    console.log('Tab '+activeInfo.tabId+' in window '+activeInfo.windowId+' is highlighted now.');
});

chrome.tabs.onDetached.addListener(function(tabId, detachInfo){
    console.log('Tab '+tabId+' in window '+detachInfo.oldWindowId+' at position '+detachInfo.oldPosition+' has been detached.');
});

chrome.tabs.onAttached.addListener(function(tabId, attachInfo){
    console.log('Tab '+tabId+' has been attached to window '+detachInfo.newWindowId+' at position '+detachInfo.newPosition+' .');
});

chrome.tabs.onRemoved.addListener(function(tabId, removeInfo){
    console.log('Tab '+tabId+' in window '+removeInfo.windowId+', and the window is '+(removeInfo.isWindowClosing?'closed.':'open.'));
});

chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId){
    console.log('Tab '+removedTabId+' has been replaced by tab '+addedTabId+'.');
)

自定义页面:Override Pages

支持使用自定义页面替换 Chrome 相应的默认页面,目前支持

  • 书签页面:bookmarks
  • 历史记录:history
  • 新标签页面:newtab

manifest.json 中的处理

只需要配置即可,不需要逻辑或对应处理对象

{
  ...
  "chrome_url_overrides": {
    "bookmarks": "自己的页面地址",
    "history": "自己的页面地址",
    "newtab": "自己的页面地址"
  }
}

下载

官方文档:developer.chrome.com/docs/extens…

manifest.json 中的处理

{
  ...
  "permissions": [
    "downloads"
  ]
}

对应处理对象

chrome.download;
其他数据结构
// downloadOptions
{
    url: 下载文件的 url,
    filename: 保存的文件名,支持相对路径,但不支持绝对路径 或	..上级目录
    conflictAction: 重名文件的处理方式, 目前取值只能是 uniquify(在文件名后添加带括号的序号保证文件名唯一)、overwrite(覆盖)和 prompt (给出提示让用户自行决定重命名或者覆盖)
    saveAs: 是否弹出另存为窗口,
    method: 请求方式(POSTGET),
    headers: 自定义 header 数组,
    body: POST 的数据
}
存在方法
  • download:触发下载,第一个参数是 downloadOptions,第二个是 callback
相关钩子

实战:下载当前页面所有图片

实现效果

项目结构

.
|-- background.js
|-- main.js
|-- manifest.json

manifest.json

{
  "manifest_version": 2,
  "name": "Save all images",
  "version": "1.0",
  "description": "Save all images in current tab",
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "permissions": ["activeTab", "contextMenus", "downloads"]
}

main.js

[].map.call(document.getElementsByTagName("img"), function (img) {
  return img.src;
});

background.js

chrome.runtime.onInstalled.addListener(function () {
  chrome.contextMenus.create({
    id: "saveall",
    type: "normal",
    title: "保存所有图片",
  });
});

chrome.contextMenus.onClicked.addListener(function (info, tab) {
  if (info.menuItemId == "saveall") {
    chrome.tabs.executeScript(tab.id, { file: "main.js" }, function (results) {
      if (results && results[0] && results[0].length) {
        results[0].forEach(function (url) {
          chrome.downloads.download({
            url: url,
            conflictAction: "uniquify",
            saveAs: false,
          });
        });
      }
    });
  }
});

网络请求

提供了完整的生命周期支持用户对请求对自定义处理,从而做到诸如【阻断连接/更改 header 或重定向】的目的;

官网文档:developer.chrome.com/docs/extens…

注意:如下 header 属性不支持更改

  • Authorization
  • Cache-Control
  • Connection
  • Content-Length
  • Host
  • If-Modified-Since
  • If-None-Match
  • If-Range
  • Partial-Data
  • Pragma
  • Proxy-Authorization
  • Proxy-Connection
  • Transfer-Encoding

manifest.json 中的处理

{
  ...
  "permissions": [
    "webRequest",
    "webRequestBlocking", // 如果需要阻止网络请求,需要声明此属性
    "*://*.baidu.com/" // 相关被操作的 URL
  ]
}

对应处理对象

chrome.webRequest;
其他数据结构
// 回调接收对象
{
  requestId
  url
  method
  frameId
  parentFrameId
  tabId
  type
  timeStamp
}
  • type 可能的值包括 main_frame/sub_frame/stylesheet/script/image/object/xmlhttprequest/other
  • 除了 onBeforeRequest 和 onErrorOccurred 事件外,其他所有事件返回的信息对象均包含 HttpHeaders 属性;
  • onHeadersReceived 、onAuthRequired 、onResponseStarted 、onBeforeRedirect 和 onCompleted 事件均包括 statusLine 属性以显示请求状态,如'HTTP/0.9 200 OK' 。
  • 其他的属性还包括 scheme 、realm 、challenger 、isProxy 、ip 、fromCache 、statusCode 、redirectUrl 和 error 等

实战:常见操作

删除所有连接中的 User-Agent

chrome.webRequest.onBeforeSendHeaders.addListener(
  function (details) {
    for (
      var i = 0, headerLen = details.requestHeaders.length;
      i < headerLen;
      ++i
    ) {
      if (details.requestHeaders[i].name == "User-Agent") {
        details.requestHeaders.splice(i, 1);
        break;
      }
    }
    return { requestHeaders: details.requestHeaders };
  },
  {
    urls: ["<all_urls>"],
  },
  ["blocking", "requestHeaders"]
);

请求重定向

“chrome.webRequest.onBeforeRequest.addListener(
    function(details){
        return {redirectUrl: details.url.replace( "www.google.com.hk", "www.google.com")};
    },
    {
        urls: [
            "*://www.google.com.hk/*"
        ]
    }
   },
    [
        "blocking"
    ]
);

代理

通过代理可以做到匿名访问等目的,存在 http 代理和 socket 代理等,可以通过 pac 脚本指定代理访问规则

manifest.json 中的处理

{
  ...
  "permissions": [
    "proxy"
  ]
}

对应处理对象

chrome.;
其他数据结构
// 代理设置对象
{
  mode: 代理模式,direct 直接连接/auto_detect 通过 WPAD 协议自动获取 pac 脚本/pac_script 使用指定脚本/fixed_servers 固定代理服务器/system 使用系统设置
  rules: ?,指定不同协议可以通过不同代理
  pacScript: ?
}
存在方法
  • setting.set:设置代理服务器
  • setting.get:获取当前浏览器代理设置
相关钩子
setting.set
var config = {
    mode: "fixed_servers",
    rules: {
        proxyForHttp: {
            scheme: "socks5",
            host: "1.2.3.4",
            port: 1080
        },
        proxyForHttps: {
            scheme: "socks5",
            host: "1.2.3.5",
            port: 1080
        },
        proxyForFtp: {
            scheme: "http",
            host: "1.2.3.6",
            port: 80
        }
        bypassList: ["foobar.com"]
    }
};
chrome.proxy.settings.set(
    {value: config},
    function() {
});

setting.get

chrome.proxy.settings.get({}, function (config) {
  console.log(config.value);
});

具体实现可以参考【SwitchSharp 源码分析】:code.google.com/p/switchysh…

系统信息

chrome 提供了获取系统 cpu/内存/存储设置信息的能力

manifest.json 中的处理

{
  ...
  "permissions": [
    "system.cpu",
    "system.memory",
    "system.storage",
  ]
}

对应处理对象

chrome.system.cpu;
chrome.system.memory;
chrome.system.storage;
其他数据结构
// cpu
{
   numOfProcessors
   archName
   modelName
   features
   processors: 为一个记录所有逻辑处理器信息的数组。
}
// 内存
{
  capacity 总容量
  availableCapacity 可用容量
}
// 存储空间
[{
  id
  name
  type: fixed 本地磁盘/removable 可移动磁盘/unknown 未知设备
  capacity
}]
存在方法
  • getInfo:获取对应信息对象
  • getAvailableCapacity:(仅 storage 支持)获取指定设置剩余空间
  • ejectDevice:(仅 storage 支持)移除移动磁盘
相关钩子
  • onAttached:监听可移动设备的插入
  • onDetached:监听可移动设备的拔出

尾声

到此,我们会发现,从具体实现层面,我们已经知道了 chrome 能做到哪些能力,可以发现这篇的篇幅远远大于其他,原因在于包含具体的分类划分以及对应小实战,记忆总会遗忘,不可能看篇文章就永远会了这个技能,只希望留个印象,日后需要的时候来这一搜就能快速上手。希望能帮到大家啦~

话说,是不是摩拳擦掌跃跃欲试啦?后续以实战为主,冲冲冲