目录
一、学习前准备知识
* 扩展插件加载到chrome浏览器的步骤
- 地址栏输入:chrome://extensions/
- 开启开发者模式
- 点击加载已解压的扩展程序
- 选择文件夹(manifest.json目录)
* webstorm配置chrome扩展插件开发提示
- 下载:https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/chrome/index.d.ts
- webstorm settings:languages & frameworks > javascript >libraries
- 点击add > 点击+号 > 点击attach files > 选择声明文件
* chrome通知失效解决步骤
- 地址栏输入:chrome://flags
- 搜索栏输入:notifications
- Enable system notifications选项设置为:Disabled
- relaunch:重启浏览器
二、浏览器扩展插件开发(MV3)
1、hello world
- manifest.json
{
"manifest_version": 3,
"name": "有钱",
"version": "1.0.0",
"description": "描述信息",
"homepage_url": "http://liujinkai.com/",
"icons": {
"32": "icon32.png",
"48": "icon48.png",
"128": "icon128.png"
},
"action": {
"default_icon": {
"16": "icon16.png",
"32": "icon32.png"
},
"default_popup": "popup.html",
"default_title": "有钱"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"contentscript.js"
],
"run_at": "document_end"
}
]
}
- popup.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>有钱</title>
<script src="popup.js"></script>
<style>
#app {
width: 240px;
height: 320px;
text-align: center;
}
</style>
</head>
<body>
<div id="app">
<button id="btn">点击探索</button>
<p id="result"></p>
</div>
</body>
</html>
- popup.js
window.onload = function () {
const btn = document.getElementById("btn");
btn.onclick = () => {
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
chrome.tabs.sendMessage(tabs[0].id, {action: "checkForContent"}, response => {
const result = document.getElementById("result")
result.innerText = response?.results || "没有收到content script的内容"
})
})
}
}
- contentscript.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "checkForContent") {
sendResponse({
results: `content script发送的document.title:${document.title}`
})
}
})
2、核心机制
a、manifest.json
{
// 必填项,最新版本3
"manifest_version": 3,
// 必填项,扩展名称
"name": "扩展名称",
// 必填项,扩展的版本号
"version": "1.0.0",
// 以下为推荐项
// 右上角浏览器图标点击事件设定
"action": {...},
"default_locale": "en",
"description": "a plain text description",
// 图标,用png格式,按要求大小提供
"icons": {...},
// 以下为可选项
"author": ...,
"automation": ...,
// 可以运行在后台的脚本配置
"background": {
// 必填
"service_worker": "background.js",
// 可选
"type": ...
},
"chrome_settings_overrides": {...},
"chrome_url_overrides": {...},
"commands": {...},
"content_capabilities": ...,
// 直接注入页面的脚本文件
"content_scripts": [{...}],
"content_security_policy": {...},
"converted_from_user_script": ...,
"cross_origin_embedder_policy": {"value": "require-corp"},
"cross_origin_opener_policy": {"value": "same-origin"},
"current_locale": ...,
"declarative_net_request": ...,
"devtools_page": "devtools.html",
"differential_fingerprint": ...,
"event_rules": [{...}],
"externally_connectable": {
"matches": ["*://*.example.com/*"]
},
"file_browser_handlers": [...],
"file_system_provider_capabilities": {
"configurable": true,
"multiple_mounts": true,
"source": "network"
},
"homepage_url": "https://path/to/homepage",
// 扩展可访问的网站规则配置
"host_permissions": [...],
"import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
"incognito": "spanning, split, or not_allowed",
"input_components": ...,
"key": "publicKey",
"minimum_chrome_version": "versionString",
"nacl_modules": [...],
"natively_connectable": ...,
"oauth2": ...,
"offline_enabled": true,
"omnibox": {
"keyword": "aString"
},
"optional_host_permissions": ["..."],
"optional_permissions": ["tabs"],
"options_page": "options.html",
"options_ui": {
"page": "options.html"
},
// 权限设定
"permissions": ["tabs"],
"platforms": ...,
"replacement_web_app": ...,
"requirements": {...},
"sandbox": [...],
"short_name": "Short Name",
"storage": {
"managed_schema": "schema.json"
},
"system_indicator": ...,
"tts_engine": {...},
"update_url": "https://path/to/updateInfo.xml",
"version_name": "aString",
// 扩展内可访问的资源
"web_accessible_resources": [...]
}
b、消息通信
- 一次性的简单消息发送&接收
// contentscript.js,发起一个从content script到扩展的消息
chrome.runtime.sendMessage({greeting: "hello"}, function (response) {
console.log(response.farewell)
})
// 扩展到content script的消息,需要给出当前tab的id
chrome.tabs.query(
{active: true, currentWindow: true},
function (tabs) {
chrome.tabs.sendMessage(
tabs[0].id,
{greeting: "hello"},
function (response) {
console.log(response.farewell)
}
)
}
)
// 接收消息的一方,chrome.runtime.onMessage事件监听来处理消息
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension")
if (request.greeting === "hello") {
sendResponse({farewell: "goodbye"})
}
// return true
}
)
- 长连接模式-chrome.runtime.Port对象
// content scripts主动建立通道如下
var port = chrome.runtime.connect({name: "yisheng"});// 通道名称
port.postMessage({joke: "knock knock"});//发送消息
port.onMessage.addListener(function (msg) {// 监听消息
if (msg.question === "who's there?") {
port.postMessage({answer: "yisheng"})
} else if (msg.question === "madame who?") {
port.postMessage({answer: "madame... bovary"})
}
})
// chrome扩展程序页面主动建立通道如下
chrome.tabs.query(
{active: true, currentWindow: true},
function (tabs) {
var port = chrome.tabs.connect(// 建立通道
tabs[0].id,
{name: "yisheng"}// 通道名称
)
}
)
// content scripts或google chrome扩展程序页面
// 监听建立连接的请求如下
chrome.runtime.onConnect.addListener(function (port) {
console.assert(port.name === "yisheng")
port.onMessage.addListener(function (msg) {
if (msg.joke === "knock knock") {
port.postMessage({question: "who's there?"})
} else if (msg.answer === "madame") {
port.postMessage({question: "madame who?"})
} else if (msg.answer === "madame... bovary") {
port.postMessage({question: "i don't get it."})
}
})
})
- google chrome扩展程序之间消息模式
- google chrome扩展程序接收指定的web页面发送的消息
chrome.runtime.onMessageExternal.addListener(
function (request, sender, sendResponse) {
if (sender.url === blacklistedWebsite) {
return // don't allow this web page access
}
if (request.openUrlInEditor) {
openUrl(request.openUrlInEditor)
}
}
)
三、打包发布
* 开发者注册
- 需要支付5.00美元的一次性费用。待注册完成就可以上传自己的扩展到应用商店了
- 注册地址:https://chrome.google.com/webstore/devconsole
* 打包上传
- 将项目压缩为.zip的文件,准备上传。登录chrome应用的开发者中心
- 访问:https://chrome.google.com/webstore
四、自动更新
- 概念
* 每隔几个小时,浏览器就会检查已安装的扩展是否有更新URL。对于每一个,它都向该URL发出请求,查找更新清单xml文件
* 可以使用扩展管理页面上的立即更新扩展按钮强制进行更新
* 为了确保一个给定的更新将只适用于谷歌chrome版本或高于特定版本,添加“prodversionmin”属性即可
- manifest.json
{
"update_url": "https://myhost.com/mytestextension/updates.xml"
}
- updates.xml
<?xml version="1.0" encoding="UTF-8"?>
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
<app appid="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa">
<updatecheck codebase="https://myhost.com/mytestextension/mte_v2.crx" version="2.0"
prodversionmin="106.0.5249.119"/>
</app>
</gupdate>
五、高级知识点-数据存储
1、知识点
* google的chrome.storage.*API实现的浏览器存储
* google的chrome.cookies.*API实现的cookie存储
* IndexedDB适用复杂大数据的存储
2、chrome.storage.*
- 概念
* chrome.storage.local
- 方式只能够将数据存储在当前登录的设备本地
- 大小限制约为5M
- 如果声明权限unlimitedStorage则不受大小限制
* chrome.storage.sync
- 只要用户启用了同步,存储的数据将自动同步到任何用户登录的chrome浏览器
- 大小限制约为100kb
- manifest.json
{
"permissions": [
"storage"
]
}
- 使用
// 存储数据
chrome.storage.local.set({key: value}, function () {
console.log("value is set to " + value)
})
// 读取数据
chrome.storage.local.get(["key"], function (result) {
console.log("value currently is " + result.key)
})
3、chrome.cookie.*
- 概念
* 可以获取或修改cookie,还可以监控cookie的变化
- manifest.json
{
"host_permissions": [
"*://*.google.com/"
],
"permissions": [
"cookies"
]
}
- 使用
// 获得一个cookie对象
chrome.cookies.get(object details, function (Cookie cookie) {...})
// 设置cookie对象
chrome.cookies.set(object details, function (Cookie cookie) {...})
// 根据名字删除cookie对象
chrome.cookies.remove(object details, function (object details) {...})
// 监听cookie对象的变化
chrome.cookies.onChanged.addListener(function (object changeInfo) {...})
4、IndexedDB
- 概念
* 对于任何数量/复杂性的数据都非常快
* 没有大小的限制可存储的数据类型,如ArrayBuffer,文件,Blob,类型化数组,集合,Map
* 在Content Script中不可用,所以您必须使用消息传递
* 不好的地方是API过时而笨拙,可以使用第三方库来解决这个问题
* 使用第三方封装的库来操作IDB更方便些,这里推荐:https://jsstore.net/
- 使用
// indexedDB在service worker中使用时已经定义
var dbName = "DatabaseName";
var open = indexedDB.open(dbName, 1);
六、高级知识点-跨域访问
- 概念
* 通常出于安全的考虑,通常web页面的XHR(XMLHttpRequest)对象不能访问其他域的服务器。但是
chrome浏览器扩展没有这个限制,只要设置了跨域访问的权限,chrome浏览器扩展的XMLHttpRequest
对象可以访问声明的任何域的服务器
* 在Manifest V3中,background页面(由Service worker提供)不支持XMLHttpRequest。考虑使用fetch()方法
- manifest.json
{
"host_permissions": [
"http://www.google.com/",
"https://*.google.com/",
"http://*/"
]
}
- 使用
// axios基于promise的封装XMR(XMLHttpRequest)的异步ajax请求库
axios.get(
"https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1"
).then(response => {
console.log(response)
}).catch(error => {
console.log(error)
})
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere
xhr.open("GET", chrome.runtime.getURL("config_resources/config.json"), true);
// 访问内部位于config_resources目录下的config.json文件
xhr.send();
七、高级知识点-扩展内页面
- 概念
* chrome.runtime.getURL(string path)将扩展程序安装目录中的相对路径转换为完整的URL
* 在扩展管理页面上(chrome://extensions),找到应用(扩展)ID,
访问chrome-extension://extensionId/filename
- 使用
// 打开新的窗口
window.open(chrome.runtime.getURL("capture.html"));
// 打开新的tab页
chrome.tabs.create({"url": chrome.runtime.getURL("capture.html")}, function (tab) {
// tab opened
console.log("after created")
})
八、高级知识点-多语言
1、配置
- 概念
* 需要把所有用户可见字符串保存在文件名为messages.json的文件里,文件位于项目的根目录下的
_locales\locale_Code,其中的locale_Code有专门的国际标准规定,比如中国大陆的简体中文
对应zh_CN,而一旦有_locales目录就必须在manifest.json文件中指定默认Locale:
- manifest.json
{
"default_locale": "zh_CN"
}
2、引用
- 概念
* 在manifest.json和css文件中的引用方法如下:__MSG_key__
* 在javascript文件中的引用方法如下:chrome.i18n.getMessage("key")
- manifest.json
{
"name": "__MSG_extName__",
"default_locale": "zh_CN"
}
- javascript文件
// chrome.i18n.getMessage(...)方法还可以带第二个参数,已替换资源值中的占位符。
// 第二个参数要么是一个字符串,要么是一个字符串数组(数组中最多9个元素)
const title = chrome.i18n.getMessage("extName")
- _locales\zh_CN\messages.json
{
"extName": {
"message": "你好世界",
"description": "扩展名称"
}
}
3、预定义资源
- 概念
* @@extension_id
- chrome浏览器扩展的id,可用于动态构建与某chrome浏览器扩展相关的URL,只能用于css
和javascript文件,如:__MSG_@@extension_id__。manifest.json中不可用
* @@ui_locale
- 当前页面的Locale,可用于动态构建与Locale相关的URL
- 使用
/* 使用预定义资源 */
body {
background-image: url("chrome-extension://__MSG_@@extension_id__/background.png");
}
/* 如果扩展的ID是abcdefghijklmnopgrstuvwxyzabcdef,那么上述代码中的粗线部分变成: */
background-image: url("chrome-extension://abcdefghijklmnopgrstuvwxyzabcdef/background.png");
九、高级知识点-background
1、背景页
* 浏览器扩展是基于事件的程序。事件是浏览器的触发器,可以说扩展要想“动”起来,就离不开事件Event。
而service worker中的脚本监听这些事件,并对事件进行响应。manifest.json文件通过background
指定后台运行的脚本
2、配置
- 概念
* 一般,背景页不需要任何html,仅仅需要js文件,浏览器的扩展系统会自动根据scripts字段指定的所有js文件自动生
成背景页。如果的确需要自己的背景页,可以使用page字段
* 我们也可以在service worker中使用javascript modules。只需在清单中将类型属性设置为module
* 从popup.js中访问background.js
* background一直处于休眠状态,只在事件发生时加载并运行,触发加载的场景如下
- chrome扩展第一次被安装或升级到新版本
- 触发了后台脚本的监听事件
- content script或其他扩展发出了一个Event
- 扩展中的其他页面调用了runtime.getBackgroundPage方法
- manifest.json
{
"manifest_version": 3,
"background": {
"page": "background.html",
"service_worker": [
"background.js"
],
"type": "module"
}
}
- 使用
// background.js这将将service worker作为es模块加载
// 从而允许您在service worker中使用import关键字导入其他模块
import * as module from "./scripts/jsencrypt.min.js"
//在popup.js中调用background.js中的变量和方法
var bg = chrome.extension.getBackgroundPage()
console.log(bg.value)
3、demo:合并浏览器窗口
- manifest.json
{
"manifest_version": 3,
"name": "浏览器合并窗口",
"version": "1.0.0",
"description": "合并其它的浏览器窗口到当前的窗口",
"icons": {
"48": "icon48.png",
"128": "icon128.png"
},
"background": {
"service_worker": "background.js"
},
"action": {
"default_icon": "icon32.png",
"default_title": "窗口合并"
}
}
- background.js
let targetWindow = null // 当前激活的浏览器窗口
let tabCount = 0 // 当前激活浏览器窗口里的tab的数量
function moveTabs(windows) {
let numWindows = windows.length
let tabPosition = tabCount
for (let i = 0; i < numWindows; i++) {
let win = windows[i]
if (targetWindow.id !== win.id) {
let numTabs = win.tabs.length
for (let j = 0; j < numTabs; j++) {
let tab = win.tabs[j]
chrome.tabs.move(tab.id, {windowId: targetWindow.id, index: tabPosition})
tabPosition++
}
}
}
}
function getTabs(tabs) {
tabCount = tabs.length
chrome.windows.getAll({populate: true}, moveTabs)
}
function getWindows(win) {
targetWindow = win
chrome.tabs.query({windowId: targetWindow.id}, getTabs)
}
/**
* 点击浏览器图标绑定的事件
* @param tab
*/
function start(tab) {
chrome.windows.getCurrent(getWindows)
}
// 地址栏图标绑定点击事件
chrome.action.onClicked.addListener(start)
十、高级知识点-选项页
1、配置显示
- 概念
* 为了让用户设定扩展的功能,可能需要提供一个选项页。如果提供了选项页,在扩展管理页面
chrome://extensions上会提供一个链接
- manifest.json
{
"options_page": "options.html"
}
2、demo:消息通知
- manifest.json
{
"manifest_version": 3,
"name": "消息通知",
"version": "1.0.0",
"description": "定时显示消息通知",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"permissions": [
"notifications",
"storage"
],
"options_page": "options.html",
"background": {
"service_worker": "background.js"
}
}
- background.js
const LS = chrome.storage.local
function show() {
let time = /(..)(:..)/.exec(new Date()) // 当前时间
let hour = time[1] % 12 || 12 // 小时
let period = time[1] < 12 ? "a.m." : "p.m." // 上午、下午
chrome.notifications.create({
type: "basic",
iconUrl: "icon48.png",
title: "是时候起来溜达一下了~~",
message: hour + time[2] + " " + period,
priority: 0
})
}
// 判断是否已经初始化
async function init() {
let isInitializedInit = await LS.get(["isActivated"])
if (!isInitializedInit) {
LS.set({"isActivated": true}) // 是否激活
LS.set({"frequency": 1}) // 显示间隔,分钟
LS.set({"isInitialized": true}) // 初始化状态
}
// 浏览器是否支持通知
let window = window ?? self
if (window.Notification) {
// 加载时就先显示一下
let tmp = await LS.get(["isActivated"])
if (!tmp.isActivated) {
show()
}
let interval = 0 // 间隔分钟数
setInterval(async () => {
interval++
let isActivatedObj = await LS.get(["isActivated"])
let frequencyObj = await LS.get(["frequency"])
if (isActivatedObj.isActivated && (frequencyObj.frequency <= interval)) {
show()
interval = 0
}
}, 60000)
}
}
init()
- options.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>消息通知</title>
<script src="options.js"></script>
</head>
<body>
<form id="options">
<input type="checkbox" name="isActivated" checked>
每
<select name="frequency">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
分钟显示消息
</form>
</body>
</html>
- options.js
const LS = chrome.storage.local
function trigger(isDeactivated) {
// 设定颜色
document.getElementById("options").style.color = isDeactivated ? "graytext" : "black"
// 设定可用状态
document.getElementById("options").frequency.disabled = isDeactivated
}
// 页面加载方法
window.addEventListener("load", async function () {
// 获取页面对象
const obj = document.getElementById("options")
// 显示存储的值
let isActivatedObj = await LS.get(["isActivated"])
obj.isActivated.checked = isActivatedObj.isActivated || true
// 显示存储的值
let frequencyObj = await LS.get(["frequency"])
obj.frequency.value = frequencyObj.frequency
// 根据存储值来设定页面上的勾选状态
if (!obj.isActivated.checked) {
trigger(true)
}
// 绑定勾选的变化事件
obj.isActivated.onchange = function () {
LS.set({"isActivated": obj.isActivated.checked}) // 设定本地存储
trigger(!obj.isActivated.checked)
}
// 绑定间隔的变化事件
obj.frequency.onchange = function () {
LS.set({"frequency": obj.frequency.value}) // 设定本地存储
}
})
十一、高级知识点-vue框架开发
1、步骤
* vite脚手架初始化项目命令:npm init vite
- vue+ts需添加*.vue的类型声明
* package.json文件script属性添加键值对
- "serve": "vite build --watch"
2、核心代码(非常重要)
* https://gitee.com/YiSiYiNian/chrome-extension.git
十二、注意事项
1、安全之csp
- 概念
* csp(content security policy)的主要目的是防止跨站脚本攻击(xss)。
通俗来说:csp的实质就是白名单策略,开发者明确声明,哪些外部资源可以加载和执行
- 解决办法就是需要将页面内js写到一个js文件里引入进来
- extension_pages:配置会影响到扩展中的页面,包括html文件和service worker
~ script-src: self|none|Anyurl
~ object-src: self|none|Anyurl
~ worker-src: self|none|Anyurl
- sandbox:这个配置会影响sandbox相关的页面。在sandbox中有两个含义
~ 不能直接访问非sandbox页面(可以通过postMessage()与通信)
~ 不受扩展的内容安全策略(csp)的约束
- manifest.json
{
"content_security_policy": {
"extension_pages": "script-src 'self';object-src 'none'",
"sandbox": "sandbox allow-scripts: script-src 'self' https://example.com"
},
"sandbox": {
"page": [
"page1.html",
"directory/page2.html"
]
}
}
2、提交审核被拒
* 在提交扩展到浏览器商店的审核过程中,请遵循最少使用权限原则,不然很可能会因为
权限滥用的问题被拒绝
* 被拒的话,按照被拒绝的原因改正后重新提交即可。但是是官方的回复一般都不说明具体的原因
十三、其它浏览器扩展
1、edge
* 开发好的chrome扩展也可以提交到microsoft扩展网站,使其可供其他microsoft edge用户使用。
需要开发者账号提交,如果没有则需要注册
- edge提交地址:https://microsoftedge.microsoft.com/addons/Microsoft-Edge-Extensions-Home
- 开发者账号注册:https://partner.microsoft.com/dashboard/microsoftedge/public/login
- 详细步骤说明:https://learn.microsoft.com/zh-cn/microsoft-edge/extensions-chromium/publish/publish-extension
* 将扩展提交到合作伙伴中心
- 启动新提交
- 上传扩展包
- 提供可用性详细信息
- 选择扩展的属性
- 为扩展添加应用商店列表详细信息
- 完成提交
2、firefox
* firefox的提交步骤跟edge的差不多,也是需要有开发者账号,然后按要求提交文件即可
- firefox的扩展提交地址:https://addons.mozilla.org/zh-CN/firefox/
- firefox的开发者:https://addons.mozilla.org/zh-CN/developers/
* firefox目前还没有对manifest V3的支持,官方说在2022年底会进行支持。之后再给大家
更新firefox上的MV3的支持说明
十四、案例-一键生成网页截图工具(非常重要)
* https://gitee.com/YiSiYiNian/chrome-extension.git
十五、案例-微信群合影
* 想法
* 开发
- 页面page.js获取图片头像url后,发消息到popup.js,popup.html里绘制图形
- 构建账号体系,实现收费用户和普通用户的区别,后台用的数据库是LeanCloud(免费版)
- 花55块钱阿里云上买了个国际域名,利用github page搭建免费的网站介绍页
- 每天早上早期2个小时时间,来开发插件
* 发布
- 尽快的验证用户需求,需要尽快的开发出产品原型上线,得到用户的反馈
- 原型非常的粗糙
- 最早的版本甚至没有收费版本,因为我也不确定用户是否有这样的市场需求,免费版本
只是在生成图片的右下角留下了自己的微信二维码
* 变现
- 验证用户需求的最好办法,就是用户是否愿意付费使用
- 用户的抱怨,心理却很美
- 用户传播的力量
- 我列出了10个点,最后选定抱怨最多的头像编辑功能作为收费的点
- low办法,让整个流程跑起来,建立起变现模式
- 第1个月的付费情况和1年后的付费情况
十六、总结
* 浏览器扩展的技术,13个知识点(核心机制、开发调试、打包发布、自动更新、数据存储、跨域访问、扩展内页面、
多语言、Background、选项页面、Vue开发框架、CSP、其它浏览器扩展)
* 4个演示Demo(Hello world、消息通知、合并窗口、一图一诗)
* 2个案例(屏幕截图、微信群合影)
* 打开一扇窗,知道了小产品变现的方法
* 课程中的所有demo可以在github上下载到
- https://github.com/ljinkai/chrome-extension-demo