热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Chrome扩展程序开发

十一在家无聊时开发了这个项目。其出发点是想通过chrome插件,来保存网页上选中的文本。后来就顺手把前后端都做了(Koa2React):chrome插件

十一在家无聊时开发了这个项目。其出发点是想通过chrome插件,来保存网页上选中的文本。后来就顺手把前后端都做了(Koa2 + React):

chrome插件源码

插件对应的前后端源码

概述

chrome扩展程序

chrome扩展程序大家应该都很熟悉了,它可以通过脚本帮我们完成一些快速的操作。通过插件可以捕捉到网页内容、标签页、本地存储,或者用户的操作行为;它也可以在一定程度上改变浏览器的UI,例如页面上右键的菜单、浏览器右上角点击插件logo后的弹窗,或者浏览器新标签页

开发缘由

按照惯例,开发前多问问自己 why? how?

why:

  • 我在平常看博文时,对于一些段落想进行摘抄或者备注,又懒得复制粘贴

how:

  • 一个chrome扩展程序,可以通过鼠标右键的菜单,或者键盘快捷键快速保存当前页面上选择的文本

  • 如果没有选择文本,则保存网页链接

  • 要有对应的后台服务,保存 user、cliper、page (后话,本文不涉及)

  • 还要有对应的前端,以便浏览我的保存记录 (后话,本文不涉及)

先上个成果图:

chrome extension - login

chrome extension - info

chrome extension - frontend

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_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,不能直接以的形式行内写入HTML里,需要独立成JS文件再引入

  • 如果有其他第三方依赖,比如jQuery等文件,也无法通过CDN引入,而需要保持资源文件到项目目录后再引入

不同运行环境JS之间的交互

虽然运行环境和绳命周期都不相同,但幸运的是,chrome为我们提供了一些三种JS都通用的API,可以起到JS之间相互通讯的效果。

chrome.runtime

消息传递

普通的消息传递

通过runtimeonMessagesendMessage等方法,可以在各个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.syncchrome.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.jschrome.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



推荐阅读
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 如何去除Win7快捷方式的箭头
    本文介绍了如何去除Win7快捷方式的箭头的方法,通过生成一个透明的ico图标并将其命名为Empty.ico,将图标复制到windows目录下,并导入注册表,即可去除箭头。这样做可以改善默认快捷方式的外观,提升桌面整洁度。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
  • 如何在HTML中获取鼠标的当前位置
    本文介绍了在HTML中获取鼠标当前位置的三种方法,分别是相对于屏幕的位置、相对于窗口的位置以及考虑了页面滚动因素的位置。通过这些方法可以准确获取鼠标的坐标信息。 ... [详细]
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
  • 本文介绍了Java后台Jsonp处理方法及其应用场景。首先解释了Jsonp是一个非官方的协议,它允许在服务器端通过Script tags返回至客户端,并通过javascript callback的形式实现跨域访问。然后介绍了JSON系统开发方法,它是一种面向数据结构的分析和设计方法,以活动为中心,将一连串的活动顺序组合成一个完整的工作进程。接着给出了一个客户端示例代码,使用了jQuery的ajax方法请求一个Jsonp数据。 ... [详细]
  • springboot启动不了_Spring Boot + MyBatis 多模块搭建教程
    作者:枫本非凡来源:www.cnblogs.comorzlinp9717399.html一、前言1、创建父工程最近公司项目准备开始重构,框 ... [详细]
  • Vue基础一、什么是Vue1.1概念Vue(读音vjuː,类似于view)是一套用于构建用户界面的渐进式JavaScript框架,与其它大型框架不 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文介绍了在Linux下安装Perl的步骤,并提供了一个简单的Perl程序示例。同时,还展示了运行该程序的结果。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 解决VS写C#项目导入MySQL数据源报错“You have a usable connection already”问题的正确方法
    本文介绍了在VS写C#项目导入MySQL数据源时出现报错“You have a usable connection already”的问题,并给出了正确的解决方法。详细描述了问题的出现情况和报错信息,并提供了解决该问题的步骤和注意事项。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 本文介绍了如何使用jQuery和AJAX来实现动态更新两个div的方法。通过调用PHP文件并返回JSON字符串,可以将不同的文本分别插入到两个div中,从而实现页面的动态更新。 ... [详细]
  • express工程中的json调用方法
    本文介绍了在express工程中如何调用json数据,包括建立app.js文件、创建数据接口以及获取全部数据和typeid为1的数据的方法。 ... [详细]
author-avatar
mobiledu2502886131
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有