Chrome扩展程序开发

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

chrome插件源码css

插件对应的先后端源码html

概述

chrome扩展程序

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

开发原因

按照惯例,开发前多问问本身 why? how?java

why:jquery

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

how:es6

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

  • 若是没有选择文本,则保存网页连接web

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

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

先上个成果图:

chrome extension - login

chrome extension - info

chrome extension - frontend

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

消息传递

普通的消息传递

经过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.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商店了。

最后终于搞定,线上可见:cliper extension

学习资源

下一步?

  • 插件功能丰富化

  • 插件可在网页上高亮展现标记的文本

  • es6 + babel重构

  • 须要使用框架吗?


注:本文源码位于github仓库:cliper-chrome,线上产品见:clipercliper extension