十一在家无聊时开辟了这个项目。其起点是想经由历程chrome插件,来保留网页上选中的文本。后来就顺手把前后端都做了(Koa2 + React):
概述
chrome扩大顺序
chrome扩大顺序人人应当都很熟习了,它能够经由历程剧本帮我们完成一些疾速的操纵。经由历程插件能够捕捉到网页内容、标签页、当地存储,或许用户的操纵行动;它也能够在肯定程度上转变阅读器的UI,比方页面上右键的菜单、阅读器右上角点击插件logo后的弹窗,或许阅读器新标签页
开辟启事
依据通例,开辟前多问问本身 why? how?
why:
我在平常看博文时,关于一些段落想举行摘抄或许备注,又懒得复制粘贴
how:
一个chrome扩大顺序,能够经由历程鼠标右键的菜单,或许键盘快捷键疾速保留当前页面上挑选的文本
假如没有挑选文本,则保留网页链接
要有对应的背景效劳,保留 user、cliper、page (后话,本文不触及)
还要有对应的前端,以便阅读我的保留纪录 (后话,本文不触及)
先上个结果图:
clip 有剪辑之意,因而项目命名为 cliper
这两天终究安奈不住买了效劳器,终究把网址布置了,也上线了chrome插件:
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_scripts
content_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_action
browser_action
里的资本会在弹窗翻开时初始化,封闭时卸载
browser_action
里定义的JS/CSS运转环境仅限于popup,而且会在每次点开弹窗的时刻初始化。然则它能够挪用一些chrome api
,以此来和其他js举行交互
除此以外:
browser_action
的HTML文件里运用的JS,不能直接以<script></script>
的情势行内写入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
进修资本
下一步?
插件功用丰富化
插件可在网页上高亮展现标记的文本
用
es6
+babel
重构须要运用框架吗?
注:本文源码位于github堆栈:cliper-chrome,线上产物见:cliper 和 cliper extension