Chrome 插件开发指南

Chrome 插件开发指南

开发与调试

chrome插件没有严格的项目结构要求,只有保证本目录有一个 manifest.json 即可,从浏览器菜单-更多工具-扩展程序可以进入插件管理页面。或直接输入地址 chrome://extensions访问。

勾选开发者模式可以用文件夹的形式直接加载插件,否则只能安装.crx 格式的文件。
mac 系统下插件安装目录为: ~/Library/Application Support/Google/Chrome/Default/Extensions

核心介绍

  1. manifest.json

    用来配置插件相关的配置信息,必须放在根目录。且以下属性是必不可少的。完整属性可以查看官方文档

    1
    2
    3
    4
    5
    {
    "manifest_version" : 2,
    "name" : "test",
    "version" : "1.0.0"
    }
  2. content-scripts (插件与页面交互)

    是 chrome 插件向页面注入脚本的一种形式,我们可以通过manifest.json配置轻易的向页面注入 js 和 css,最常见的是广告屏蔽,页面样式定制等等.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "content-scripts" : [
    {
    "matches" : ["http://*/*", "<all_urls>"],
    "js": ["js/xxx.js","....js"],
    "css": ["css/xx.css"],
    "run_at": "document_start" // 可选 document_start/end/idle(默认空闲)
    }
    ]
    }

    content-scripts 与原始页面共享DOM,但不共享JS,如果想要访问页面JS某个变量,只能通过 injected js 来实现,content-scripts不能访问绝大部分chrome.xxx.api, 除了以下四种。

    - chrome.extension(getURL,inIncognitoContext, lastError,onRequest,sendRequest)
    - chrome.i18N
    - chrome.runtime(connect,getManifest,getURL,id, onConnect,onMessage,sendMessage)
    - chrome.storage
    
  3. background
    后台是一个常驻的页面,它随着浏览器的开关而开关.通常把需要一直运行的代码放在 background 里面.
    他的权限非常高,可以调用 chrome 的扩展 API(除了 devtools), 而且它可以无限制跨域.配置中,background 可以通过 page 指定一个页面,也可以通过 scripts 指定一个 js,chrome 会自动为这个 js 生成一个默认页面.

    1
    2
    3
    4
    5
    6
    {
    "background":{
    "page": "xxx.html"
    // scripts: ["js/xxx.js"]
    }
    }
  4. event-pages
    鉴于 background 生命周期和浏览器同步,长时间挂载后台影响性能,而 event-pages 与 background 唯一的区别就是多了一个 persistent 参数,它会在需要时被加载,空闲时被关闭.一般 background 用的比较多.

  5. popup
    popup 是点击插件图标时打开的一个窗口网页,焦点离开网页就关闭,一般做一些交互使用.
    popup 可以包含任意你想要的 HTML,并且会自适应大小,可以通过 default_popup 来指定页面,也可以调用 setPopup()方法.

    1
    2
    3
    4
    5
    "browser_action" : {
    "default_icon": "img/xx.png",
    "default_title": "悬停时的标题",
    "default_popup": "xx.html"
    }

    popup 的生命和周期很短,需要长时间运行的代码不要放在 popup 里.popup 中可以通过 chrome.extension.getBackgroundPage() 获取 background 的 window 对象.

  6. injected-script
    content-script 无法访问页面中的 js,虽然它可以操作 dom,但 dom 却不能调用它,也就是在 dom 的事件中无法调用 content-script 中的代码,但是在页面中添加一个按钮并调用插件的 api 是很常见的需求,我们可以再 content-script 中通过 DOM 方式向页面注入 inject-script.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // content-script
    function injectCustomJs(jsPath){
    jsPath = jsPath || 'js/inject.js';
    var temp = document.creatElement("script");
    temp.src = chrome.extension.getURL(jsPath); // 类似于 chrome-extension://xxxx/js/inject.js
    temp.onload = function(){
    this.parentNode.removeChild(this);
    }
    document.head.appendChild(temp);
    }

    代码会报错,因为在 web 中直接访问插件中的资源必须显示声明才行,在配置文件中增加以下配置:

    1
    2
    3
    4
    {
    // 普通页面能够直接访问的插件资源列表,不设置无法直接访问.
    "web_accessible_resources": ["js/inject.js"]
    }

    inject-script 如何调用 content-script 中的代码,要用到消息通信.

  7. homepage_url
    开发者网站

Chrome 插件的 8 种展示形式

  1. browserAction 浏览器右上角图标

    1
    2
    3
    4
    5
    6
    7
    {
    "browser_action" : {
    "default_icon": "img/xx.png", // 19*19
    "default_title": "悬停时的标题",
    "default_popup": "xx.html"
    }
    }

    通过 setIcon 更改 icon, setTitle()更改鼠标 hover 时的标题.setBadgeText()来更改图标上的文本信息.

  2. pageAction 地址栏右侧
    当某些特定页面打开时会在地址栏右边显示的图标.(新版吧位置放到了浏览器右边,可以把它看成置灰的 browserAction.
    例如当打开百度时才显示图标.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // background.js
    chrome.runtime.onInstalled.addListener(function(){
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
    chrome.declarativeContent.onPageChanged.addRules([
    {
    conditions: [
    // 只有打开百度才显示pageAction
    new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'baidu.com'}})
    ],
    actions: [new chrome.declarativeContent.ShowPageAction()]
    }
    ]);
    });
    });
  3. 右键菜单
    通过 chrome.contextMenus API,右键菜单可以出现在不同的上下文.

    1
    2
    3
    4
    5
    6
    7
    // manifest.json
    {"permissions": ["contextMenus"]}
    // background.js
    chrome.contextMenus.create({
    title: "测试右键菜单",
    onclick: function(){alert('您点击了右键菜单!');}
    });

    常见 API 参考

  4. 覆盖特定页面
    使用 override 页可以将 chrome 默认的一些特定页面替换掉.一个插件只能替代一个默认页.

    1
    2
    3
    4
    5
    6
    "chrome_url_overrides":
    {
    "newtab": "newtab.html",
    "history": "history.html",
    "bookmarks": "bookmarks.html"
    }
  5. devtools(开发者工具)
    例如 vue.js devtools , chrome 可以再 devtools 上新增一个面板.
    devtools 页面会随着开发者工具的开关而开关.可以访问 Devtools API ,而其他比如 background 无权访问.

    • chrome.devtools.panels: 面板相关
    • chrome.devtools.inspectedWindow: 获取被审查窗口的信息
    • chrome.devtools.network: 获取有关网络请求的信息

      1
      2
      3
      {
      "devtools_page": "XXX.html"
      }

      这个 html 一般什么都没有,只有一个 script 标签引用 js 文件<script src='js/devtools.js'></script>,
      再看一下 devtools 的代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 创建自定义面板,同一个插件可以创建多个自定义面板
      // 几个参数依次为:panel标题、图标(其实设置了也没地方显示)、要加载的页面、加载成功后的回调
      chrome.devtools.panels.create('MyPanel', 'img/icon.png', 'mypanel.html', function(panel)
      {
      console.log('自定义面板创建成功!'); // 注意这个log一般看不到
      });
      // 创建自定义侧边栏
      chrome.devtools.panels.elements.createSidebarPane("Images", function(sidebar)
      {
      // sidebar.setPage('../sidebar.html'); // 指定加载某个页面
      sidebar.setExpression('document.querySelectorAll("img")', 'All Images'); // 通过表达式来指定
      //sidebar.setObject({aaa: 111, bbb: 'Hello World!'}); // 直接设置显示某个对象
      });
      // 访问被检查的页面DOM需要使用inspectedWindow
      chrome.devtools.inspectedWindow.eval("jQuery.fn.jquery", function(result, isException)
      {
      var html = '';
      if (isException) html = '当前页面没有使用jQuery。';
      else html = '当前页面使用了jQuery,版本为:'+result;
      alert(html);
      });
  6. 选项页 option
    选项页是插件的设置页面,有两个入口,一个是右键图标菜单,一个是插件管理页面.

    1
    2
    3
    4
    5
    6
    7
    8
    {
    // "options_page": "options.html" ,(老版本写法)
    "options_ui": {
    "page": "optionxxx.html",
    "open_in_tab": true, // 在当前 tab 打开
    "chrome_style": true // 添加了一些默认样式
    }
    }

    不能使用 alert, 数据存储建议使用 chrome.storage, 因为会随用户自动同步

  7. omnibox
    注册某个关键字触发插件自己的搜索建议界面.

    1
    2
    3
    4
    {
    // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
    "omnibox": { "keyword" : "go" },
    }

    然后在 background 注册监听事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    chrome.omnibox.onInputChanged.addListener((text, suggest) => {
    console.log('inputChanged: ' + text);
    if(!text) return;
    if(text === 'xxx'){
    suggest([
    {content: '百度搜索 ' + text, description: '百度搜索 ' + text},
    {content: '谷歌搜索 ' + text, description: '谷歌搜索 ' + text},
    ]);
    }
    })
  8. 桌面通知:
    chrome 提供了一个 chrome.notification API 方便插件推送桌面通知.

    1
    2
    3
    4
    5
    6
    chrome.notifications.create(null,{
    type: "basic",
    iconUrl: "img/xx.ping",
    title: "标题",
    message: "内容"
    })

消息通信

  • popup 和 background
    popup 可以直接调用 background 的 js 方法,也可以访问 background 的 DOM.

    1
    2
    3
    4
    5
    6
    7
    8
    // background.js
    function test(){
    alert("background");
    }
    // popup.js
    var bg = chrome.extension.getBackgroundPage();
    bg.test();
    bg.document.body.innerHTML; // dom
  • popup 或 bg 向 content 发消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // bg,popup.js
    function sendMessageToContent(message,callback){
    chrome.tabs.query({active:true,currentWindow: true},function(tabs){
    chrome.tabs.sendMessage(tabs[0].id,message,function(res){
    if(callback) callback(res);
    })
    })
    }
    // content-script.js 接收
    chrome.runtime.onMessage.addListener(function(req,send,res){
    if(req.cmd == 'test') alert(req.value);
    send("我收到了你的消息")
    })
  • content 向 bg/popup 主动发消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // content-script.js
    chrome.runtime.sendMessage({
    greeting: "hello"
    }, function(res){
    console.log("收到回复" + res)
    })
    // bg 或 popup.js
    chrome.runtime.onMessage.addListener(function(req,send,res){
    send("我收到了你的消息")
    })
  • injected script 和 content script
    content-script 和页面内 脚本(injected-script)之间唯一共享的就是页面 DOM 元素.有两种方式实现通信,一是通过 window.postMessage 和 window.addEventListener 实现消息通信(推荐),二是通过 自定义 dom 事件.

    1
    2
    3
    4
    5
    6
    // injected - script
    window.postMessage({"test": 'hello'},'*');
    // content- script
    window.addEventListener("message",function(e){
    console.log(e.data)
    },false)
  • 长连接和短连接
    chrome 插件中有两种通信方式,一种是短连接(chrome.tabs.sendMessage和 chrome.runtime.sendMessage),一个是长连接(chrome.tabs.connect 和 chrome.runtime.connect).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 长连接
    // popup.js
    var port = chrome.tabs.connect(tabId,{name: 'test-connect'});
    port.postMessage({xxx:'xxx'});
    port.onMessage.addListener(function(msg){
    alert('收到消息' + msg.answer)
    // ...
    })
    // content-script
    chrome.runtime.onConnect.addListener(function(port){
    if(port === 'test-connect'){
    port.onMessage.addListener(function(msg){
    alert("收到长连接",msg)
    })
    }
    })

补充

  • 获取当前窗口 id

    1
    2
    3
    chrome.windows.getCurrent(function(cw){
    console.log(cw.id)
    })
  • 获取当前标签页 id

    1
    2
    3
    4
    5
    function getCurrentTabId(callback){
    chrome.tabs.query({active:true,currentWindow:true},function(tabs){
    if(callback) callback(tabs.length ? tabs[0].id : null)
    })
    }
  • 定期执行代码

    1
    2
    3
    4
    5
    // 配置
    "permissions": ["alarms"]
    // 创建方法
    chrome.alarms.create(name,info);// name 为任务名, info 包含以下属性 when 何时,dalayInMinutes 延迟时间, periodInMinutes 非 null表示时间间隔,单位 min
    chrome.alarms.onAlarm.addListener(xxx); // 触发事件
  • 本地存储
    chrome.storage 是针对插件全局的,即使在 background 中保存的数据,在 content-script 也能获取到.chrome.storage.sync 可以跟随当前登录用户自动同步.需要声明 storage 权限,有 sync 和 local 两种方式选择.

    1
    2
    3
    4
    5
    6
    7
    8
    // 读取数据,第一个参数是要读取的 key 以及默认值
    chrome.storage.sync.get({color: 'red',age:18},function(items){
    console.log(items)
    })
    // 保存数据
    chrome.storage.sync({color:'blue'},function(){
    console.log('save success')
    })
  • 快捷键唤醒 popup

    1
    2
    3
    4
    5
    6
    7
    "commands": {
    "_execute_browser_action":{
    "suggested_key":{
    "default": "Alt+Shift+J" // 快捷键唤醒
    }
    }
    }
  • webRequest
    通过 webrequest API 可以对 HTTP 请求进行修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //权限申请
    "permissions": [
    "webRequest", // web请求
    "webRequestBlocking", // 阻塞式 web 请求
    "storage",// 插件本地储存
    "http://*/*" //可以通过 executeScript 或 insertCss 访问的网站
    ]

    // web 请求监听
    chrome.webRequest.onBeforeRequest.addListener(details =>{
    let showImage = false ; // 不展示图片
    if(!showImage && details.type === 'image'){
    return {
    cancel: true
    }
    }
    if(details.type === 'media'){
    //...
    }
    },{urls: ["<all_urls>"]},["blocking"]);
  • 国际化
    插件根目录新建一个_locales 的文件夹,在新建一些语言文件夹如 en,zh_CN,zh_TW,然后在每个语言文件夹放入一个 messages.json,同时在文件中设置 default_locale.测试时,通过给chrome建立一个不同的快捷方式chrome.exe –lang=en来切换语言

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // en/message.json
    {
    "pluginDesc": {"message": "A simple chrome extension demo"},
    "helloWorld": {"message": "Hello World!"}
    }
    // zh_CN/message.json
    {
    "pluginDesc": {"message": "一个简单的Chrome插件demo"},
    "helloWorld": {"message": "你好啊,世界!"}
    }
    // 在 manifest.json 和 css 文件中通过 __MSG_messagename__引入
    {
    "description": "__MSG_pluginDesc__",
    // 默认语言
    "default_locale": "zh_CN",
    }
    // js 中使用
    chrome.i18n.getMessage("helloWorld")