摘要:这一步可以参考应用商店上传扩展程序一文最后终于搞定,线上可见学习资源建立扩展程序插件开发攻略如何成为一名应用开发者扩展的开发下一步插件功能丰富化插件可在网页上高亮展示标记的文本用重构需要使用框架吗注本文源码位于仓库,线上产品见和
概述 chrome扩展程序十一在家无聊时开发了这个项目。其出发点是想通过chrome插件,来保存网页上选中的文本。后来就顺手把前后端都做了(Koa2 + React):
chrome插件源码
插件对应的前后端源码
chrome扩展程序大家应该都很熟悉了,它可以通过脚本帮我们完成一些快速的操作。通过插件可以捕捉到网页内容、标签页、本地存储,或者用户的操作行为;它也可以在一定程度上改变浏览器的UI,例如页面上右键的菜单、浏览器右上角点击插件logo后的弹窗,或者浏览器新标签页
开发缘由按照惯例,开发前多问问自己 why? how?
why:
我在平常看博文时,对于一些段落想进行摘抄或者备注,又懒得复制粘贴
how:
一个chrome扩展程序,可以通过鼠标右键的菜单,或者键盘快捷键快速保存当前页面上选择的文本
如果没有选择文本,则保存网页链接
要有对应的后台服务,保存 user、cliper、page (后话,本文不涉及)
还要有对应的前端,以便浏览我的保存记录 (后话,本文不涉及)
先上个成果图:
clip 有剪辑之意,因此项目命名为 cliper
这两天终于安奈不住买了服务器,终于把网址部署了,也上线了chrome插件:
cliper
cliper extension
manifest.json在项目根目录下创建manifest.json文件,其中会涵盖扩展程序的基本信息,并指明需要的权限和资源文件
{ // 以下为必写 "manifest_version": 2, // 必须为2,1号版本已弃用 "name": "cliper", // 扩展程序名称 "version": "0.01", // 版本号 // 以下为选填 // 推荐 "description": "描述", "icons": { "16": "icons/icon_16.png", "48": "icons/icon_48.png", "64": "icons/icon_64.png", "128": "icons/icon_128.png" }, "author": "ecmadao", // 根据自己使用的权限填写 "permissions": [ // 例如 "tab", "storage", // 如果会在js中请求外域API或者资源,则要把外域链接加入 "http://localhost:5000/*" ], // options_page,指右键点击右上角里的插件logo时,弹出列表中的“选项”是否可点,以及在可以点击时,左键点击后打开的页面 "options_page": "view/options.html", // browser_action,左键点击右上角插件logo时,弹出的popup框。不填此项则点击logo不会有用 "browser_action": { "default_icon": { "38": "icons/icon_38.png" }, "default_popup": "view/popup.html", // popup页面,其实就是普通的html "default_title" : "保存到cliper" }, // background,后台执行的文件,一般只需要指定js即可。会在浏览器打开后全局范围内后台运行 "background": { "scripts": ["js/vendor/jquery-3.1.1.min.js", "js/background.js"], // persistent代表“是否持久”。如果是一个单纯的全局后台js,需要一直运行,则不需配置persistent(或者为true)。当配置为false时转变为事件js,依旧存在于后台,在需要时加载,空闲时卸载 "persistent": false }, // content_scripts,在各个浏览器页面里运行的文件,可以获取到当前页面的上下文DOM "content_scripts": [ { // matches 匹配 content_scripts 可以在哪些页面运行 "matches" : ["http://*/*", "https://*/*"], "js": ["js/vendor/jquery-3.1.1.min.js", "js/vendor/keyboard.min.js", "js/selection.js", "js/notification.js"], "css": ["css/notification.css"] } ] }
综上,我们一共有三种资源文件,针对着三个运行环境:
browser_action
控制logo点击后出现的弹窗,涵盖相关的html/js/css
在弹窗中,会进行登录/注册的操作,并将用户信息保存在本地储存中。已登录用户则展现基本信息
background
在后台持续运行,或者被事件唤醒后运行
右键菜单的点击和异步保存事件将在这里触发
content_scripts
当前浏览的页面里运行的文件,可以操作DOM
因此,我会在这个文件里监听用户的选择事件
注:
content_scripts中如果没有matches,则扩展程序无法正常加载,也不能通过“加载未封装的扩展程序”来添加。如果你的content_scripts中有js可以针对所有页面运行,则填写"matches" : ["http://*/*", "https://*/*"]即可
推荐将background中的persistent设置为false,根据事件来运行后台js
不同运行环境JS的绳命周期如上所述,三种JS有着三种运行环境,它们的生命周期、可操作DOM/接口也不同
content_scriptscontent_scripts会在每个标签页初始化加载的时候进行调用,关闭页面时卸载
内容脚本,在每个标签页下运行。虽然它可以访问到页面DOM,但无法访问到这个里面里,其他JS文件创建的全局变量或者函数。也就是说,各个content_scripts(以及外部JS文件)之间是相互独立的,只有:
"content_scripts": [ { "js": [...] } ]
js所定义的一个Array里的各个JS可以相互影响。
background官方建议将后台js配置为"persistent": false,以便在需要时加载,再次进入空闲状态后卸载
什么时候会让background的资源文件加载呢?
应用程序第一次安装或者更新
监听某个事件触发(例如chrome.runtime.onInstalled.addListener)
监听其他环境的JS文件发送消息(例如chrome.runtime.onMessage.addListener)
扩展程序的其他资源文件调用了runtime.getBackgroundPage
browser_actionbrowser_action里的资源会在弹窗打开时初始化,关闭时卸载
browser_action里定义的JS/CSS运行环境仅限于popup,并且会在每次点开弹窗的时候初始化。但是它可以调用一些chrome api,以此来和其他js进行交互
除此以外:
browser_action的HTML文件里使用的JS,不能直接以的形式行内写入HTML里,需要独立成JS文件再引入
如果有其他第三方依赖,比如jQuery等文件,也无法通过CDN引入,而需要保持资源文件到项目目录后再引入
不同运行环境JS之间的交互虽然运行环境和绳命周期都不相同,但幸运的是,chrome为我们提供了一些三种JS都通用的API,可以起到JS之间相互通讯的效果。
chrome.runtime消息传递
通过runtime的onMessage、sendMessage等方法,可以在各个JS之间传递并监听消息。举个栗子:
在popup.js中,我们让它初始化之后发送一个消息:
chrome.runtime.sendMessage({ method: "showAlert" }, function(response) {});
然后在background.js中,监听消息的接收,并进行处理:
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (message.method === "showAlert") { alert("showAlert"); } });
以上代码,会在每次打开插件弹窗的时候弹出一个Alert。
chrome.runtime的常用方法:
// 获取当前扩展程序中正在运行的后台网页的 JavaScript window 对象 chrome.runtime.getBackgroundPage(function (backgroundPage) { // backgroundPage 即 window 对象 }); // 发送消息 chrome.runtime.sendMessage(message, function(response) { // response 代表消息回复,可以接受到通过 sendResponse 方法发送的消息回复 }); // 监听消息 chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { // message 就是你发送的 message // sender 代表发送者,可以通过 sender.tab 判断消息是否是从内容脚本发出 // sendResponse 可以直接发送回复,如: sendResponse({ method: "response", message: "send a response" }); });
需要注意的是,即便你在多个JS中注册了消息监听onMessage.addListener,也只有一个监听者能收到通过runtime.sendMessage发送出去的消息。如果需要不同的监听者分别监听消息,则需要使用chrome.tab API来指定消息接收对象
举个栗子:
上文说过,需要在content_scripts中监听选择事件,获取选择的文本,而对于右键菜单的点击则是在background中监听的。那么需要把选择的文本作为消息,发送给background,在background完成异步保存。
// content_scripts 中获取选择,并发送消息 // js/selection.js // 获取选择的文本 function getSelectedText() { if (window.getSelection) { return window.getSelection().toString(); } else if (document.getSelection) { return document.getSelection(); } else if (document.selection) { return document.selection.createRange().text; } } // 组建信息 function getSelectionMessage() { var text = getSelectedText(); var title = document.title; var url = window.location.href; var data = { text: text, title: title, url: url }; var message = { method: "get_selection", data: data } return message; } // 发送消息 function sendSelectionMessage(message) { chrome.runtime.sendMessage(message, function(response) {}); } // 监听鼠标松开的事件,只有在右键点击时,才会去获取文本 window.onmouseup = function(e) { if (!e.button === 2) { return; } var message = getSelectionMessage(); sendSelectionMessage(message); };
// background 中接收消息,监听右键菜单的点击,并异步保存数据 // js/background.js // 创建一个全局对象,来保存接收到的消息值 var selectionObj = null; // 首先要创建菜单 chrome.runtime.onInstalled.addListener(function() { chrome.contextMenus.create({ type: "normal", title: "save selection", id: "save_selection", // 有选择才会出现 contexts: ["selection"] }); }); // 监听菜单的点击 chrome.contextMenus.onClicked.addListener(function(menuItem) { if (menuItem.menuItemId === "save_selection") { addCliper(); } }); // 消息监听,接收从 content_scripts 传递来的消息,并保存在一个全局对象中 chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (message.method === "get_selection") { selectionObj = message.data; } }); // 异步保存 function addCliper() { $.ajax({ // ... }); }
通过chrome.runtime.connect(或者chrome.tabs.connect)可以建立起不同类型JS之间的长链接。
信息的发送者需要制定独特的信息类型,发送并监听信息:
var port = chrome.runtime.connect({type: "connection"}); port.postMessage({ method: "add", datas: [1, 2, 3] }); port.onMessage.addListener(function(msg) { if (msg.method === "answer") { console.log(msg.data); } });
而接受者则要注册监听,并判断消息的类型:
chrome.runtime.onConnect.addListener(function(port) { console.assert(port.type == "connection"); port.onMessage.addListener(function(msg) { if (msg.method == "add") { var result = msg.datas.reduce(function(previousValue, currentValue, index, array){ return previousValue + currentValue; }); port.postMessage({ method: "answer", data: result }); } }); });chrome.tabs
要使用这个API则需要先在manifest.json中注册:
"permissions": [ "tabs", // ... ]
// 获取到当前的Tab chrome.tabs.getCurrent(function(tab) { // 通过 tab.id 可以拿到标签页的ID }); // 通过 queryInfo,以Array的形式筛选出符合条件的tabs chrome.tabs.query(queryInfo, function(tabs) {}) // 精准的给某个页面的`content_scripts`发送消息 chrome.tabs.sendMessage(tabId, message, function(response) {});
举个栗子:
在background.js中,我们获取到当前Tab,并发送消息:
chrome.tabs.getCurrent(function(tab) { chrome.tabs.sendMessage(tab.id, { method: "tab", message: "get active tab" }, function(response) {}); }); // 或者 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.sendMessage(tabs[0].id, { method: "tab", message: "get active tab" }, function(response) { }); });
然后在content_scripts中,进行消息监听:
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (message.method === "tab") { console.log(message.message); } });chrome.storage
chrome.storage是一个基于localStorage的本地储存,但chrome对其进行了IO的优化,可以储存对象形式的数据,也不会因为浏览器完全关闭而清空。
同样,使用这个API需要先在manifest.json中注册:
"permissions": [ "storage", // ... ]
chrome.storage有两种形式,chrome.storage.sync和chrome.storage.local:
chrome.storage.local是基于本地的储存,而chrome.storage.sync会先判断当前用户是否登录了google账户,如果登录,则会将储存的数据通过google服务自动同步,否则,会使用chrome.storage.local仅进行本地储存
注:因为储存区没有加密,所以不应该储存用户的敏感信息
API:
// 数据储存 StorageArea.set(object items, function callback) // 数据获取 StorageArea.get(string or array of string or object keys, function callback) // 数据移除 StorageArea.remove(string or array of string keys, function callback) // 清空全部储存 StorageArea.clear(function callback) // 监听储存的变化 chrome.storage.onChanged.addListener(function(changes, namespace) {});
举栗子:
我们在browser_action完成了用户的登录/注册操作,将部分用户信息储存在storage中。每次初始化时,都会检查是否有储存,没有的话则需要用户登录,成功后再添加:
// browser_action // js.popup.js chrome.storage.sync.get("user", function(result) { // 通过 result.user 获取到储存的 user 对象 result && setPopDOM(result.user); }); function setPopDOM(user) { if (user && user.userId) { // show user UI } else { // show login UI } }; document.getElementById("login").onclick = function() { // login user.. // 通过 ajax 请求异步登录,获取到成功的回调后,将返回的 user 对象储存在 storage 中 chrome.storage.sync.set({user: user}, function(result) {}); }
而在其他环境的JS里,我们可以监听storage的变化:
// background // js/background.js // 一个全局的 user 对象,用来保存用户信息,以便在异步时发生 userId var user = null; chrome.storage.onChanged.addListener(function(changes, namespace) { for (key in changes) { if (key === "user") { console.log("user storage changed!"); user = changes[key]; } } });
正式发布大体上,我们目前为止理清了三种环境下JS的不同,以及他们交流和储存的方式。除此以外,还有popup弹窗、右键菜单的创建和使用。其实使用这些知识就足够做出一个简单的chrome扩展了。
其实我觉得整个过程中最蛋疼的一步就是把插件正式发布到chrome商店了。
首先,你要在开发者信息中心进行登记,缴费5刀。这一步可以参照如何成为一名Chrome应用开发者一文来通过验证和支付。但需要注意的是,我在尝试时使用的账户为中国google账户,因此完全无法支付,直到重新注册了一个香港账户才搞定
之后,要填写一系列的发布信息。google对icon和banner的尺寸要求的相当严格。。这一步可以参考Google Chrome 应用商店上传扩展程序一文
最后终于搞定,线上可见:cliper extension
学习资源建立 Chrome 扩展程序
Chrome插件(Extensions)开发攻略
如何成为一名Chrome应用开发者
chrome扩展的开发
下一步?插件功能丰富化
插件可在网页上高亮展示标记的文本
用es6 + babel重构
需要使用框架吗?
注:本文源码位于github仓库:cliper-chrome,线上产品见:cliper 和 cliper extension
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/87969.html
摘要:了解扩展程序开发本文大量借鉴图灵电子书扩展及应用开发首发版首先,我尝试来用简单几句话描述一下扩展程序扩展主要用于对浏览器功能的增强,它强调与浏览器相结合。提供了接口,允许扩展对用户的历史进行管理。 了解Chrome扩展程序开发 本文大量借鉴图灵电子书-Chrome扩展及应用开发(首发版) 首先,我尝试来用简单几句话描述一下Chrome扩展程序: Chrome扩展主要用于对浏览器功能的增...
摘要:关于我的博客掘金专栏路易斯专栏原文链接扩展开发定制请求响应头域本文共字,阅读需分钟。那么,我会放弃吗反向代理显然不会,既然问题出在上,我去掉就行了。然而无论多少次的学习和模仿,最终的目的还是为了使用,故开发一款定制请求的势在必行。 本文首发于《程序员》杂志2017年第9、10、11期,下面的版本又经过进一步的修订。 关于 Github:IHeader 我的博客:louis blog ...
摘要:扩展应用模块功能介绍扩展应用由很多部分组成,其中主要模块为为了避免由于翻译原因导致的问题,因此在下文中对相关模块的称呼一律采用上面的英文。附录官方开发文档英建议有英文阅读能力的人阅读此文档。 概述 本文通过对chrome插件的各个部分进行快速的介绍,从而让大家了解插件各个部分的关系,并且知道如何将其进行组装成一个完整的chrome插件。 由于chrome官方文档中对于如何从零开发一个c...
摘要:一般而言,扩展会对用户浏览的页面进行相应的操作和一些数据传递,本案例的本质是,当用户浏览网页版微博时,扩展会向当前页面注入预先写好的,这样便对微博网页版进行了样式重构。采用这样的方法依次处理所有你不想看到的元素,你的微博便会简洁很多。 0x00. 前言 微博现在也是变得越来越臃肿,广告越来越多,早已不再是微博了,这让微博深度用户的我感到十分焦灼。由于之前就尝试写过 Chrome 插件,...
摘要:以下示例将阻止所有对的请求。从存储请求和阻止请求的对象中删除当前选项卡的属性。收听消息告知后台进程阻止的列表已被用户更新。两者都提供类似的功能和事件处理程序。 前言 当我们浏览网站时,都会发送许多请求来获取网页内容。这些请求中有些是重要的,而有些是我们不需要,因为它们可能是广告或建议等。在本文中,将创建一个有助于阻止和取消阻止所选URL的Chrome扩展插件,让你选择你打开的网址及该打...
摘要:配置文件每一个扩展都有一个格式的文件,叫。此消息发送后会触发扩展内每个页面的事件。和持续长时间的保持会话需要在和扩展建立一个长时间存在的通道。内容脚本发送消息到扩展程序建立通道,并给通道命名利用通道发送一条消息。 这次的练习是做一个Chrome的扩展,分享一下入门开发过程。因为在消息传递那块纠结了特别久,所以我会重点总结消息传递那块。 这次做这个插件的功能很简单,就是点击按钮后可以对当...
阅读 3594·2023-04-26 02:32
阅读 3820·2021-11-23 10:05
阅读 2278·2021-10-08 10:04
阅读 2678·2021-09-22 16:06
阅读 3597·2021-09-22 15:27
阅读 739·2019-08-30 15:54
阅读 1668·2019-08-30 13:50
阅读 2593·2019-08-29 13:56