谷歌插件开发笔记
插件开发相关及问题记录。
开发目标:具有登录功能及可以展示频道列表信息。
延伸问题:可以生成二维码及提取当前网页的主要内容。
-
第一阶段,原生chrome extension阶段,调用chorme的API
谷歌程序插件的本质就是一个网页展示的小窗口。html搭建界面,css完成布局,js完成交互操作。外加一些chrome的API,去特定的实现一些功能。
- 插件的配置文件。
manifest.json 文件(必须有的) 更多此文件的属性设置参考http://open.chrome.360.cn/extension_dev/manifest.html 此文件就是插件的配置文件。定义插件名称,插件图片,插件的出现位置等等 { "manifest_version": 2, //默认设置,必有 "name": "TR-iOS", //扩展程序的名字 "version": "1.0", //扩展程序的当前版本号 "icons": { //扩展程序的在设置一栏显示的图片 "16": "images/tr_icon16.png", "48": "images/tr_icon48.png", "128": "images/tr_icon128.png" }, "browser_action": { //扩展程序类型,即显示在浏览器输入url框外,而不是内 "default_title": "ios extension", //当把鼠标放到浏览器上此程序的图标上时显示的名字 "default_popup": "popup.html", //点击图标时出现的界面,与点击响应事件不同时响应。设置此项则不响应点击事件 "default_icon": { //在浏览器框显示的图片 "19": "images/tr_icon19.png", "38": "images/tr_icon38.png" } }, "permissions": [ //权限设置 "activeTab", "tabs" , "http://*/*", //默认在任何http://开始的网页都能启动此程序 "https://*/*" //默认在任何https://开始的网页都能启动此程序 ], "background": { //当程序扩展开启时,就会在后台运行的 "scripts": ["js/background.js"], //运行的js文件 "persistent": true //false为按需运行,true是一直运行 }, //更多此属性介绍参考http://open.chrome.360.cn/extension_dev/content_scripts.html "content_scripts": [ //在Web页面内运行的javascript脚本 { "matches": ["http://*/*","https://*/*"], //满足什么条件执行该js脚本 "js": ["js/Readability.js" ,"js/contentJS.js"] //js文件 写入当前界面的js文件 "run_at": "document_idle" //控制content script注入的时机。可以是document_start, document_end或者document_idle。缺省时是document_idle。 //如果是document_start, 文件将在所有CSS加载完毕,但是没有创建DOM并且没有运行任何脚本的时候注入。 //如果是document_end,则文件将在创建完DOM之后,但还没有加载类似于图片或frame等的子资源前立刻注入。 //如果是document_idle,浏览器会在document_end和发出window.onload事件之间的某个时机注入。具体的时机取决与文档加载的复杂度,为加快页面加载而优化。 } ] }
此文件定义了,插件的基本属性。主要有标题,图标,权限。及popup.html,点击出现的界面。以及启动时默认运行的background.js文件。
以及在什么时间,在什么页面下,注入到当前网页的js文件 设置。完成了最基本的设置。可以自己根据需求添加或者减少部分字段。- 配置文件可以根据实际需求不断修改。根据manifest.json文件。可以具体的在不同的文件里处理实际问题。
popup.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body style="width: 320px; height: 400px;"> <script type="text/javascript" src="js/popup.js"></script> </body> </html> **chrome不允许扩展中的HTML页面内直接内嵌js脚本,而要求所有的脚本都作为外部src来引入** 定义了界面的大小和导入了popup.js文件。 此界面是一个空的界面,根据popup.js文件进行判断出现登录界面还是频道信息界面 。在点击进行处理的时间内,给了此界面一个大小,是为了让界面的切换更加顺畅(如果不给会先出现一个小白块,在出现具体的界面(popup会自适应大小))。
> popup.js // 获得背景页对象 var backgroundPage = chrome.extension.getBackgroundPage(); // 将背景页的属性cur_url作为将要展示的界面的值 // location.href 可以改变当前出现的界面 location.href = backgroundPage.cur_url; > backgroud.js 作为一个中间界面存储着当前网页的URL和title // 定义属性 var cur_url; var cur_title; // 接收信息 chrome的API chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){ cur_url = request.cur_url; cur_title = request.cur_title; }); // 判断出现的网页 根据本地对象localStorage.success属性 此属性会在登录成功的时候被设置 var success = localStorage.success; if (success == "true") { cur_url = "main.html"; } else { cur_url = "login.html"; } > contentJS.js 文件是运行在当前网页中的脚本文件 部分代码如下 // 发送信息 chrome.runtime.sendMessage({ cur_url: docment.URL, cur_title: docment.title }); // 获取网页正文 var loc = document.location; var url = { spec: loc.href, host: loc.host, prePath: loc.protocol + "//" + loc.host, scheme: loc.protocol.substr(0, loc.protocol.indexOf(":")), pathBase: loc.protocol + "//" + loc.host + loc.pathname.substr(0, loc.pathname.lastIndexOf("/") + 1) }; // clone一个document对象,因为Readability是通过对当前网页的DOM的修改来进行解析的。这会删除当前网页的一些元素。 // 克隆一个新的对象,就是对新的对象的操作,则不会影响当前网页的正常展示 var documentClone = document.cloneNode(true); var article = new Readability(url, documentClone).parse(); if (article) { // 如果读取到内容,则处理,没有则提示 console.log(article.textContent); } else { // alert("读取失败"); } // clone一个document对象,因为Readability是通过对当前网页的DOM的修改来进行解析的。这会删除当前网页的一些元素。 // 克隆一个新的对象,就是对新的对象的操作,则不会影响当前网页的正常展示 var documentClone = document.cloneNode(true); var article = new Readability(url, documentClone).parse(); if (article) { // 如果读取到内容,则处理,没有则提示 console.log(article.textContent); } else { // alert("读取失败"); } 解释下,正文提取的算法是Readability,其他网页提取正文相关请看下面的资料参考。Readability.js默认是直接在当前网页加载完毕之后注入的。 > login.js 中有两点。 1. 获取当前页面的属性时,曾考虑过contentJS.js中获取,然后发送给background.js。 但是就会局限在,插件的开启在网页打开之前。且如果网页已存在,在打开插件,必须刷新界面才可以获得。因此抛弃了。 正确打开姿势为 chrome.tabs.getSelected(null, function(tab) { qrcode.makeCode(tab.url); }); 2. 生成二维码 导入qrcode.js, qrcode.min.js文件 // 生二维码图片 function makeCode () { // 创建QRCode 对象 var qrcode = new QRCode(document.getElementById("qrcode"), { width : 150, height : 150 }); chrome.tabs.getSelected(null, function(tab) { qrcode.makeCode(tab.url); }); }
**插件通信相关** 这个时候出现一个问题。插件本身与注入到当前网页的js文件的通信问题,以及插件本身各个界面的通信问题。 一般会转化为 1. 插件与注入到网页的js文件的通信 即background.js与contentJS.js直接的通信。 消息传递分为两种,一种是单次的消息请求,另外一种是长连接。 一,单次的消息请求 > contenJS.js // 发送信息 chrome.runtime.sendMessage({ greeting: "您好", cur_url: docment.URL, cur_title: docment.title }); > backgroud.js old method getSelected在chrome.33后弃用 chrome.tabs.getSelected(null, function(tab) { chrome.tabs.sendMessage(tab.id, {greeting: "您好"}, function(response) { console.log(response.farewell); }); }); new method chrome.33后替代getSelected方法 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.sendMessage(tabs[0].id, {greeting: "您好"}, function(response) { console.log(response.farewell); }); }); > contenJS.js 与 background.js 接收信息 // 接收信息 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){ // 可以根据sender.tab存在ze则是background.js 否则contentJS.js console.log(sender.tab ? "来自内容脚本:" + sender.tab.url : "来自扩展程序"); // greenting 是参数字段即上例子的cur_url if (request.greeting == "您好") // 接收到还可以回复一条简单信息 sendResponse({farewell: "再见"}); }); 如果有多个页面同时监听 onMessage 事件,那么只有第一个调用 sendResponse() 的页面可以成功返回响应信息,其它的都会被忽略。 二,长连接 > 与短连接类似 contenJS.js // 创建长连接 var port = chrome.runtime.connect({name: "敲门"}); port.postMessage({joke: "敲门"}); port.onMessage.addListener(function(msg) { if (msg.question == "是谁?") { port.postMessage({answer: "女士"}); } else if (msg.question == "哪位女士?") { port.postMessage({answer: "Bovary 女士"}); } }); > background.js 由于这个文件在插件开启时就会存在,所以他发送信息时,contentJS.js还没有注入到当前网页中。 所以测试方式为: 如果没有popup.html界面,则为点击图标的时候 chrome.browserAction.onClicked.addListener(function(tab) { postMessage(tab.id); }); 如果有的话 在popup.js中调用背景页的这个方法即可 function sendMessage() { chrome.tabs.getSelected(null, function(tab) { postMessage(tab.id); }); } function postMessage(tabID) { // 长连接 var port = chrome.tabs.connect(tabID, {name: "敲门"}); port.postMessage({joke: "敲门"}); port.onMessage.addListener(function(msg) { if (msg.question == "是谁?") { port.postMessage({answer: "女士"}); } else if (msg.question == "哪位女士?") { port.postMessage({answer: "Bovary 女士"}); } }); } 问题是:请求发出了,但是调试时postMessage的信息没有监听到。原因未知 监听的方法为 chrome.runtime.onConnect.addListener(function(port) { if (port.name == "敲门") { port.onMessage.addListener(function(msg) { if (msg.joke == "敲门") { port.postMessage({question: "是谁?"}); } else if (msg.answer == "女士") { port.postMessage({question: "哪位女士?"}); } else if (msg.answer == "Bovary 女士") { port.postMessage({question: "我没听清楚。"}); } }); } }); 三,更多解析请参考 https://crxdoc-zh.appspot.com/extensions/messaging(需翻墙) http://open.chrome.360.cn/extension_dev/messaging.html#simple 2. 插件内部之间的通信 即background.js与其他各个界面的通信。对于 background 和 popup ,其实都是运行在同一个进程中的,所以background 和 popup 之间可以直接相互调用对方的方法,不需要消息传递 > popup.js var test = "textStatus"; // 获得背景页对象 var backgroundPage = chrome.extension.getBackgroundPage(); // 将背景页的属性cur_url作为将要展示的界面的值 // location.href 可以改变当前出现的界面 location.href = backgroundPage.cur_url; // 调用背景页的方法 backgroundPage.getPopupPage(); > backgroud.js 中 // 调用popup.js function getPopupPage() { // 获得popup.html的相关属性及方法 var popupPage = chrome.extension.getViews({type:'popup'}); // 获取popup页面 bgTest = popupPage[0].test; alert(bgTest); } 即可弹出警告框内容是textStatus。 注意一定要指明type,如果没有指定,则获取Background Page之外的所有Extension Page的window对象。 然后就是background是一个运行在扩展进程中的HTML页面。它在你的扩展的整个生命周期都存在, 而popup是在你点击了图标之后才存在,所以,在获取popup变量时,请确认popup已打开。
css相关 @charset "UTF-8"; /* body标签的属性 */ 即html一些元素属性的设置 body { background-color:#2c3335; color:#f5f5f5; /*text-align:center;*/ font-family:"Lucida Grande", "DejaVu Sans", Verdana, sans-serif; width: 320px; height: 400px; } /* id = loginform 的属性 */ 自己设置的元素的id的属性的设置 #loginform { margin-top:20px; margin-left:auto; margin-right:auto; width:300px; } /* class = input 的属性设置 */ 自己设置的元素的class的属性的设置 .input { width:300px; background: #f5f5f5; border:none; border-radius: 4px; color: #333; font-size: 14px; margin-top:10px; }
javaScript相关 只说我遇到的 1. 添加对某个元素的点击事件,添加对这些事件的监听 $(document).ready(function(){ // 对form表单的处理 $("form").submit(function(e){ return false; }); // 退出按钮响应事件 id = mainLogoutClick $("#mainLogoutClick").bind("click", function(){ }); // 对整个div元素的监听点击事件 $(document).on('click', 'div', function () { }); // class = loginbutton 点击事件 $('.loginbutton').on('click', function(){ }); }); 2. 网络请求 我用到的是ajax $.ajax( { url: requestAddArticle, // 请求的网络地址 type: "POST", // 请求的网络类型 data: JSON.stringify({"channelIDs": selectArray, "source": $.base64.encode(locationURL)}), // 请求的参数json序列化 contentType: "application/json", // 设置返回的参数为json对象 dataType: "json", // 设置请求的参数为json对象 beforeSend: function(xhr) { // 请求的header设置 xhr.setRequestHeader("Authorization", "Token " + localStorage.Token); }, success:function(data, textStatus, jqXHR){ // 成功回调 document.getElementById("mainAddBtn").value = "添加成功"; }, error: function(jqXHR, textStatus, errorThrown){ // 失败回调 console.log(JSON.stringify(jqXHR)); } }); 3. 其他 js 是一个很讲究顺序的脚本语言。如果a.js是基于b.js使用的,那么b.js一定要在a.js之前导入。 如果你的js文件将要对html中的某些元素的id或者class进行操作,请在这些元素创建完成之后,导入你的js文件。
插件其他相关 1. chrome extension 应该是谷歌程序扩展,不是插件。但是貌似都是这么叫,所以也默认为插件开发了。 还有一个chrome app。貌似是谷歌扩展应用。 2. 问题。 1)由于界面是两个之间切换,所以点击出现时并不总是很流畅。 2)由于界面的一些元素是根据网络数据动态计算的,所以界面不够稳定。也许可以尝试出现加载等待界面(没尝试)或者固定界面的大小(我的效果,也许我设置错了) 3)由于要加入第三方登录pc端授权。授权回调必须是一个正常的网页,担心插件不具有这个权限(未尝试)。 同时授权回调会跳转到另一界面,成功之后再做一些事,会直接导致本界面的消失。 4)其他 3. 插件开发的可能性选择。 1)有道云笔记的注入iframe,同时具有第三方登录和网页内容提取功能,是一个选择。 2)印象笔记剪藏版插件。一个很棒的插件功能很强大且高效。同样是注入iframe的方式。 3)Asana 插件。一个很简洁的插件。在同一个界面中,写入很多元素布局,根据需要显示隐藏部分元素达到不同的界面显示。登录直接在新窗口进行操作,猜测使用cookies方式达到数据共享。
-
第二阶段,把一个iframe注入到当前的网页中。
看到有道云笔记的插件之后。知道了第三方登录可以融入插件中且界面统一,因此在此改变开发方向,进入iframe注入的研究。 使用iframe后,去掉manifest.json文件中default_popup字段。同时在backgroud.js中实现点击响应事件。 //当点击的时候 注入js文件 chrome.browserAction.onClicked.addListener(function(tab) { chrome.tabs.executeScript(tab.id, {file: "js/jquery.min.js"}); // insert.js文件依赖于jQuery库。所以先导入jquery.min.js文件 chrome.tabs.executeScript(tab.id, {file: "js/insert.js"}); }); > insert.js // 判断是否存在id = TR-IOS 的元素 if (document.getElementById("TR-IOS")) { // 存在则移除 $('#TR-IOS').remove(); } else { // 不存在则添加 var URL = "main.html"; var totalURL = URL + '?URL=' + document.URL; // iframe元素设置 var iframe = '<iframe src="http://kpoints.cn/zsk/chrome-Extension/' + totalURL + '" frameborder="10px" name="TR-IOS" id="TR-IOS" style="border: 10px; border-radius: 5px; border-color: #2c3335; visibility: visible; z-index: 2147483647; position: fixed; width: 340px; right: 20px; top: 20px; height: 520px; display: block!important;"></iframe>'; $("body").append(iframe); //添加iframe document.getElementById('TR-IOS').src = 'http://kpoints.cn/zsk/chrome-Extension/main.html?URL=' + "wwww"; } // 添加监听事件 addEventListener('message', function(ev) { // 当收到此事件时移除此插件 if (ev.data === 'closeIframe') { $('#TR-IOS').remove(); } }); // 通知父窗口 删除本窗口 放到点击按钮的事件中处理 于上面的监听事件正好搭配使用 // parent.postMessage('closeIframe', '*'); > 第三方登录(稍候) 1) QQ 2) sina 3) wxchat 暂未通过审核,所以暂未处理。 插件基本上到此。完毕。 其他为server端处理。例如:html, css, js. 文件存储在服务器端。
第二种方式的问题: 1)无法获取当前网页的URL和title。 解决方式:在注入iframe时直接在url后面拼接上当前网页的URL,title,favIconURL > insert.js // 获得当前页的favIconUrl var getFavicon = function(){ var favicon = undefined; var nodeList = document.getElementsByTagName("link"); for (var i = 0; i < nodeList.length; i++) { if((nodeList[i].getAttribute("rel") == "icon")||(nodeList[i].getAttribute("rel") == "shortcut icon")) { favicon = nodeList[i].getAttribute("href"); } } return favicon; } var favIconUrl = getFavicon(); // 判断是否存在id = TR-IOS 的元素 if (document.getElementById("TR-IOS")) { // 存在则移除 $('#TR-IOS').remove(); } else { // 不存在则添加 var URL = "main.html"; // 拼接上当前网页的URL, title, favIconUrl var totalURL = URL + '?URL=' + document.URL + '&title=' + document.title + '&favIconUrl=' + favIconUrl; // iframe元素设置 var iframe = '<iframe src="http://kpoints.cn/zsk/chrome-Extension/' + totalURL + '" frameborder="10px" name="TR-IOS" id="TR-IOS" style="border: 10px; border-radius: 5px; border-color: #2c3335; visibility: visible; z-index: 2147483647; position: fixed; width: 340px; right: 20px; top: 20px; height: 520px; display: block!important;"></iframe>'; $("body").append(iframe); //添加iframe } 在你输入的那个网页中heade中导入处理的js文件。我的是main.html,我导入的是otherMain.js > otherMain.js // 先存储URL和title的值 var url = getParameterByName('URL'); var title = getParameterByName('title'); var favIconUrl = getParameterByName('favIconUrl'); if (url) { localStorage.baseURL = url; localStorage.baseTitle = title; localStorage.favIconUrl = favIconUrl; } // 处理网络地址后缀 我们人为添加的 function getParameterByName(name, url) { if (!url) { url = window.location.href; } name = name.replace(/[\[\]]/g, "\\$&"); var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, " ")); } 然后在正常有需求的网页使用即可。 favIconUrl的获取函数不太完美,有些网站的写法不是正规写法就没法获得。而网站icon的设置也并不统一。 2)注入iframe这种方式会被拒绝(貌似有反注入)。特别是在https的网站上,直接被阻拦。 有道云笔记的方式貌似是http的网站注入http://youdao...之类,https网站注入https://youdao...之类。暂未测试,只是猜测。
调试与安装相关(稍候)
-
查看源码(学习别人的插件)
- 首先找到对方的扩展ID,浏览器中输入chrome://chrome/extensions/
- 然后在文件路径下找到这个ID文件 /Users/zsk/Library/Application\ Support/Google/Chrome/Default/Extensions
- .crx的扩展文件后缀,直接解压就可以看到里面的文件
extension参考资料
- 中文文档:http://open.chrome.360.cn/extension_dev/overview.html
- 中文文档2:http://open.se.360.cn/open/extension_dev/getstarted.html(这个貌似更好)
- 百度的文档: https://chajian.baidu.com/developer/extensions/getstarted.html
- 英文文档:https://developer.chrome.com/extensions
- 英语文档的翻译:https://crxdoc-zh.appspot.com/extensions/
- 一篇不错的开发文章:https://segmentfault.com/a/1190000006949838?utm_source=tuicool&utm_medium=referral
- 强力推荐的一本书:http://www.ituring.com.cn/minibook/950
- 书中的例子集合:https://github.com/Sneezry/chrome_extensions_and_apps_programming
二维码生成
网页正文提取
- https://github.com/mozilla/readability
- 或者直接搜索关键字:网页正文提取算法,readability。
在百度(中文关键字为主),谷歌或者github(英文关键字为主)上搜索