美文网首页
谷歌插件

谷歌插件

作者: 小朴同学 | 来源:发表于2019-02-17 17:19 被阅读0次

    谷歌插件开发笔记

    插件开发相关及问题记录。

    开发目标:具有登录功能及可以展示频道列表信息。
    延伸问题:可以生成二维码及提取当前网页的主要内容。
    
    1. 第一阶段,原生chrome extension阶段,调用chorme的API

      谷歌程序插件的本质就是一个网页展示的小窗口。html搭建界面,css完成布局,js完成交互操作。外加一些chrome的API,去特定的实现一些功能。

      1. 插件的配置文件。
      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文件 设置。完成了最基本的设置。可以自己根据需求添加或者减少部分字段。

      1. 配置文件可以根据实际需求不断修改。根据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方式达到数据共享。
      
    2. 第二阶段,把一个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...之类。暂未测试,只是猜测。
      
    3. 调试与安装相关(稍候)

    4. 查看源码(学习别人的插件)

      image
      • 然后在文件路径下找到这个ID文件 /Users/zsk/Library/Application\ Support/Google/Chrome/Default/Extensions
      • .crx的扩展文件后缀,直接解压就可以看到里面的文件

    extension参考资料


    二维码生成


    网页正文提取

    • https://github.com/mozilla/readability
    • 或者直接搜索关键字:网页正文提取算法,readability。
      在百度(中文关键字为主),谷歌或者github(英文关键字为主)上搜索

    相关文章

      网友评论

          本文标题:谷歌插件

          本文链接:https://www.haomeiwen.com/subject/moogeqtx.html