Logo

Chrome 插件开发

photo

2022年04月29日

chrome 插件的控制能力

除了支持传统的一切 web API、JavaScript API 以外,chrome插件额外支持以下API(chrome.xxx):

  • 浏览器窗口(chrome.window)
  • tab标签(chrome.tabs)
  • 书签(chrome.bookmark)
  • 历史(chrome.history)
  • 下载(chrome.download)
  • 网络请求(chrome.webRequest)
  • 各类事件监听?
  • 自定义右键菜单(chrome.contextMenus)
  • 开发者工具扩展(chrome.devtool)
  • 插件管理(chrome.extension)

chrome 插件的主要模块

1. manifesh.json

{
	 // 清单文件的版本,必须是2
	"minifest_version": 2,
	”name“: "demo",
	"version": "1.0.0",
	"description": "简单的chrome插件demo",
	"icons": {
		"16": "img/icon.png",
		"48": "img/icon@48x.png",
		"128": "img/icon@128x.png",
	},
	// 会一直常驻在后台运行的js文件或html页面。有 2 种指定方式:
	// ①指定js,那么会自动生成一个背景页
	"background": {
		"scripts": ["js/background.js"]
	},
	// ②指定html页面,可以通过script标签引入多个js文件
	// "background": {
	//		"page": "background.html"
	// },
	"browser_action": {
		"default_icon": "img/icon.png",
		// (可选)鼠标悬停在右上角图标上的标题
		"default_title": "这是一个示例chrome插件",
		"default_popup": "popup.html"
	},
	// 对某些特性网页才显示的图标
	"page_action": {
		"default_icon": "img/icon.png",
		"default_title": "这是page action",
		"default_popup": "popup.html"
	},
	// 需要注入页面的JS
	"content_scripts": [
		{
			// ["http://*/*", "https://*/*"] 表示匹配这两个协议的所有地址
			// "<all_urls>" 表示匹配所有地址
			"matches": ["<all_urls>"],
			// 多个JS按顺序注入
			"js": ["js/util.js", "js/content.js"],
			// CSS的注入容易影响全局,一定要谨慎!
			"css": ["css/custom.css"],
			// 代码注入的时间,可选值:"document_start", "document_end", "document_idle",最后一个表示页面空闲时,默认document_idle
			"run_at": "document_start"
		},
		// content_script 可配置多个规则
		{
			"matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
			"js": ["js/show-image-size.js"]
		}
	],
	// 申请权限
	"premissions": [
		"contextMenus",
		"tabs",
		"notifications",
		"webRequest",
		"storage",
		"http://*/*", // 可以通过executeScript或者insertCSS访问的网站???
		"https://*/*", // 可以通过executeScript或者insertCSS访问的网站???
	],
	// 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的???
	"web_accessible_resources": ["js/inject.js"],
	// 插件主页,广告位
	"homepage_url": "http://github.com/...",
	// 覆盖浏览器默认页
	"chrome_url_overrides": {
		// 覆盖浏览器默认的新标签页
		"newtab": "newTab.html"
	},
	// 插件配置页
	"options_ui": {
		"page": "options.html",
		// 添加官方的默认样式,推荐使用
		"chrome_style": true
	},
	//
	"omnibox": { "keyword": "go" },
	//
	"default_locale": "zh_CN",
	// devtools 页面入口??注意只能指向一个HTML文件,不能是JS文件
	"devtools_page": "devtools.html"
}

2. background

  • 常驻在后台一直运行的脚本或页面,生命周期最长,随着浏览器打开而打开,随着浏览器关闭而关闭。通常把需要一直运行的、启动就运行的、全局代码放在这里面。
  • background的权限非常大,几乎可以调用所有的 chrome API(除了 devtools),而且它可以无限跨域,也就是可以跨域访问任意网站而无需对方设置CORS。
    • 【有待验证:】(其实不止是background,所有的直接通过chrome-extension://id/xx.html这种方式打开的网页都可以无限制跨域。)
  • 后台页面只可调试代码逻辑,是没有界面的。

3. content_scripts

注意:

  • content_scripts 里的js可以访问页面的DOM,但不能访问页面里的JS环境对象(比如某个JS变量),只能通过 injected js 来实现。
  • content_scripts 不能访问大部分的 chrome API,除了下面几种:
    • chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest) 这是???
    • chrome.i18n 国际化
    • chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage) 访问配置文件,本页url
    • chrome.storage 访问本地存储
  • 这些API绝大部分时候都够用了,非要调用其它API的话,你还可以通过通信来实现让background来帮你调用

Content script是在一个特殊环境中运行的,这个环境成为isolated world(隔离环境)。它们可以访问所注入页面的DOM,但是不能访问里面的任何javascript变量和函数。 对每个content script来说,就像除了它自己之外再没有其它脚本在运行。 反过来也是成立的: 页面里的javascript也不能访问content script中的任何变量和函数。

4. popup

  • popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互,或者作为一个需要频繁操作的快捷入口。
  • popup可以包含任意你想要的HTML内容,并且会自适应大小。可以通过default_popup字段来指定popup页面,也可以调用setPopup()方法。
  • 【注意】需要特别注意的是,由于单击图标打开popup,焦点离开又立即关闭,所以popup页面的生命周期一般很短,需要长时间运行的代码千万不要写在popup里面。
  • 在权限上,它和background非常类似,它们之间最大的不同是生命周期的不同,popup中可以直接通过chrome.extension.getBackgroundPage()获取background的window对象。

5. 五种JS之间的消息通信(比较关键!)

  • 记忆的关键在于,理解这些通信路径的目的和使用场景!
  • popup和background其实几乎可以视为一种东西,因为它们可访问的API都一样、通信机制一样、都可以跨域。
  • 阅读这篇文章有助于理解插入脚本和内容脚本直接的通信方式:https://crxdoc-zh.appspot.com/extensions/content_scripts

注:第一列为发起方,第一行为接收
在这里插入图片描述

  1. popup 访问 background:
    popup 可以直接调用 background 的JS方法,也可以直接访问background的DOM:
// popup.js
let bg = chrome.extension.getBackgroundPage()
bg.xxx() // 访问bg的方法
console.log(bg.document.title) // 访问BG的DOM
  1. background 访问 popup:(前提是popup已经打开):
// background.js
let views = chrome.extension.getViews({type: 'popup'})
if (views.length) {
	console.log(views[0].document.title)
}
  1. popup 或 background 主动向 content 发送消息
    网上有很多老代码用的是 chrome.extension.onMessage,没有搞清楚 chrome.extension 和 chrome.runtime 之间的区别,可能只是别名。建议统一使用 chrome.runtime。
// popup.js 或 background.js
function sendMessageToContent (message, callback) {
	chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
		chrome.tabs.sendMessage(tabs[0].id, message, (response) => {
			if (callback) callback(response)
		})
	})
}
sendMessageToContent({action: 'test', data: '你好,我来自popup!'}, (response) => {
	console.log('来自content的回复:', response)
})
// content.js 接收
chrome.runtime.onMessage.addListener((request, sender, senderResponse) => {
	if (request.action === 'test') {
		console.log(request.data)
	}
	senderResponse('我收到了你的消息---来自content')
})
  1. content 主动向 popup 或 background 发送消息。注意事项:
    • content 向 popup 主动发送消息的前提是 popup 必须打开,否则需要利用 background 作中转;
    • 如果 background 和 popup 存在多个地方监听,那么他们都可以同时收到消息,但只有一个可以 senderResponse,一个先发送了,另外的就无效了。
// content.js
chrome.runtime.sendMessage({data: '你好,我来自content'}, (response) => {
	console.log('收到来自后台的回复:', response)
})
// background.js 或 popup.js
chrome.runtime.onMessage.addListener((request, sender, senderResponse) => {
	console.log('收到来自content的消息')
	console.log(request, sender, senderResponse)
	senderResponse('我来自后台,我已收到你的消息')
})
  1. 嵌入页面的JS 与 content 之间
    (待续。。。暂未发现使用场景)
    主要通过window.postMessage和window.addEventListener来实现二者消息通讯。

  2. 补充:长连接
    相对于 chrome.tabs.sendMessage、chrome.runtime.sendMessage 这类短连接而言的,类似于 websocket 的概念。
    接口:port = chrome.tabs.connect 和 port = chrome.runtime.connect
    监听:chrome.runtime.onConnect.addListener((port) => {…})

6. 简单了解:event_pages

鉴于background生命周期太长,长时间挂载后台可能会影响性能,所以Google又弄一个event-pages,在配置文件上,它与background的唯一区别就是多了一个persistent参数:

{
	"background": {
		"scripts": ["event-page.js"],
		"persistent": false
	}
}

它的生命周期是:在被需要时加载,在空闲时被关闭,什么叫被需要时呢?比如第一次安装、插件更新、有content-script向它发送消息,等等。

7. 有待深入:injected-script,涉及到消息通讯

  • 使用场景:因为content-script有一个很大的“缺陷”,也就是无法访问页面中的JS,虽然它可以操作DOM,但是DOM却不能调用它,也就是无法在DOM中通过绑定事件的方式调用content-script中的代码(包括直接写onclick和addEventListener2种方式都不行),但是,“在页面上添加一个按钮并调用插件的扩展API”是一个很常见的需求

Chrome插件的 8 种展示方式

  • browserAction(浏览器右上角)
    • 图标
    • tooltip
    • badge
  • pageAction(地址栏右侧)
  • contextMenus(右键菜单)
  • override(覆盖特定页)
  • devtool(开发者工具)
  • option(选项页)
  • omnibox(向地址栏注册一个关键字以提供搜索建议)
  • notification(桌面通知)

几种脚本的API权限对比

权限对比

零碎知识

  • 没有严格的项目结构要求,只要求保证根目录有一个 manifest.json 即可。

  • chrome插件除了Chrome浏览器之外,还可以运行在所有webkit内核的国产浏览器,比如360极速浏览器、360安全浏览器、搜狗浏览器、QQ浏览器等等。

  • 调试:打开开发者模式。在 chrome://extensions 里找到本插件,点击“背景页”,就可以打开 Devtools

    • 如果要调试vue页面
    • 全局安装包 npm install -g @vue/devtools
    • 执行命令 vue-devtools 启动
    • 在页面中加入
    • 参见 https://www.jianshu.com/p/036c8eda1e7c
  • chrome.* API 中的方法通常是异步的:它们不等待操作完成就立即返回。如果您需要知道某个操作的结果,您应该向方法传递一个回调函数。

常见问题

  • 关于插件ID

    • chrome依靠插件ID而不是插件名字,来判断是否为同一个插件。
    • 开发测试时,使用的是未压缩的文件夹,而其插件ID是根据插件所在的绝对路径计算而来的。
    • 想要为插件定制一个不变的ID,需要每次打包都输入同一个私钥文件,如图
      在这里插入图片描述
  • 关于 pem 密钥文件有何用?

    • 用于生成插件ID,区分插件的。假设,你当前插件叫做MyFirstExtension,版本号为1.0,而下一次升级时,你想把产品名称改为MySecondExtension,版本号为2.0。但,如何保证Chrome认为它们是一款插件呢?这个时候,就需要pem密钥文件了,再次生成crx文件时,选择pem文件。所以该文件要保存包,以便后续更新。
  • 【与chrome插件无关】在websocket协议头里无法通过自定义header字段添加 Token 鉴权信息。

    • 解决方法:只能通过其他方式传递token,比如cookie。
  • 如果要让chrome插件与本地原生应用通讯,需要把插件id配置在 chrome 的 manifest.json 文件里:

    • /Users/pangyue/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.thunder.chrome.host.json
    • mac迅雷下载支持就是这样干的
  • chrome.pageCapture 模块,有个 saveAsMHTML 方法,可以为网页保存快照在本地。但MHTML文件不能已在线的方式访问,只能在本地文件系统下访问。

MHTML is a standard format supported by most browsers. It encapsulates in a single file a page and all its resources (CSS files, images…).
Note that for security reasons a MHTML file can only be loaded from the file system and that it can only be loaded in the main frame.

  • 获取当前窗口ID
chrome.windows.getCurrent(function (currentWindow) {
	console.log('当前窗口ID:' + currentWindow.id);
});
  • 获取当前标签页ID
// 方法一:
function getCurrentTabId (callback) {
	chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
	{
		if (callback) callback(tabs.length ? tabs[0].id: null);
	});
}
// 方法二:(更稳妥)
function getCurrentTabId2() {
	chrome.windows.getCurrent(function(currentWindow) {
		chrome.tabs.query({active: true, windowId: currentWindow.id}, function(tabs) {
			if (callback) callback(tabs.length ? tabs[0].id: null);
		});
	});
}
  • 本地存储
  • chrome.storage是针对插件全局的,你在background中保存的数据,在content-script也能获取到;
  • webRequest
    请求拦截:
// web请求监听,最后一个参数表示阻塞式,需单独声明权限:webRequestBlocking
chrome.webRequest.onBeforeRequest.addListener(details => {
	// cancel 表示取消本次请求
	if(!showImage && details.type == 'image') return {cancel: true};
	// 简单的音视频检测
	// 大部分网站视频的type并不是media,且视频做了防下载处理,所以这里仅仅是为了演示效果,无实际意义
	if(details.type == 'media') {
		chrome.notifications.create(null, {
			type: 'basic',
			iconUrl: 'img/icon.png',
			title: '检测到音视频',
			message: '音视频地址:' + details.url,
		});
	}
}, {urls: ["<all_urls>"]}, ["blocking"]);
  • Extension context invalidated. 报错
    • 重新安装或者自动更新扩展程序时,现有内容脚本会失去与扩展程序其余部分的连接——即关闭端口,它们将无法使用runtime.sendMessage()——但内容脚本本身仍然可以继续工作,因为它们已经被注入。
    • 写插件自动更新功能要注意该问题。

参考

  • 官方文档:https://developer.chrome.com/extensions
  • 中文文档:https://crxdoc-zh.appspot.com/extensions/api_index
  • 中文文档2:http://chrome.cenchy.com/
  • 360浏览器扩展开发文档:
    • http://open.se.360.cn/open/extension_dev/overview.html
    • http://open.chrome.360.cn/extension_dev/overview.html
  • Mozilla:https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/What_are_WebExtensions
  • 下载chrome插件crx文件离线安装:https://crxdl.com/
  • 国内插件市场:https://chromecj.com/tag
  • 【博客】Chrome插件(扩展)开发全攻略:https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html
  • 【博客系列文章】https://www.cnblogs.com/champagne/p/
  • 一种开发 Chrome 扩展程序的新姿势:
    • 这篇文章提出了插件开发的痛点 – 不同运行环境之间的通讯问题。但可惜解决方案没有vue版本,只有react版本。
    • https://mp.weixin.qq.com/s?__biz=Mzg4MjE5OTI4Mw==&mid=2247490045&idx=1&sn=5141c0a6c6bc495f0733f620e18d3594&chksm=cf5b0661f82c8f7731f510475af28b37fcacdbf42cbc0282fe6f9482103463de1dd213ad5801&scene=132#wechat_redirect
本文为原创文章,请注意保留出处!
谷歌插件开发 2022年04月29日

前言作为一个前端开发者,学习谷歌插件的开发也是十分必要的导入开发一个谷歌插件的第一步就是先学会...谷歌插件开发

热门文章

修复群晖Synology Drive client右键菜单缺失问题 本教程主要解决windows10右键菜单中没有SynologyDrive菜单的问题,整体思路是找到...修复群晖SynologyDriveclient右键菜单缺失问题 作者:Pastore Antonio
1820 浏览量
docker如何查看一个镜像内部的目录结构及其内部都有哪些文件 前言:有时候我们会在docker上下载一个镜像,或者是上传一个镜像到docker上,甚至有时候就是在...docker如何查看一个镜像内部的目录结构及其内部都有哪些文件 作者:Pastore Antonio
1803 浏览量
configure: error: Package requirements (oniguruma) were not met configure:error:Packagerequirements(oniguruma)...configure:error:Packagerequirements(oniguruma)werenotmet 作者:Pastore Antonio
1533 浏览量
Adobe Acrobat Pro 激活 这里记录了一些AdobeAcrobat的激活教程和组件。浏览量:1,685 作者:Pastore Antonio
1531 浏览量
追寻日出,找回自己 为什么我要去追寻日出?其实我是一个很懒的人,每次都起不来,直到有一次我在租房中睡到了大天亮,阳光照...追寻日出,找回自己 作者:Pastore Antonio
1509 浏览量