记一次Chrome扩展程序开发

大概是去年这个时候花了几天折腾了一下Chrome扩展程序,当时是使用Vue做了一个书签扩展工具,后来使用了Infinity之后,就不再维护那个项目了。

最近恰好有个任务是为运营开发一款用于提高工作效率的浏览器插件,因此回过头整理相关的开发过程和遇见的一些问题。

<!--more-->

浏览器是日常工作中必不可少的工具,为了提高效率,经常会使用一些扩展工具,比如

  • JSON Formatter,格式化JSON字符串,在浏览器调接口就很直观了
  • Octotree,以目录的方式整理github项目,浏览代码更加方便
  • JetBrains IDE Support,webstrom在浏览器断点调试啥的
  • Vue.js devtools,Vue官方调试工具
  • EditThisCookie,一个方便的cookie管理器

在地址栏输入chrome://extensions/就可以管理浏览器的扩展程序了。

当然,由于应用场景的多样性,难免遇到不存在满足特定功能的插件,因此,了解Chrome扩展程序的开发还是挺有趣的,轮子可以自己造嘛。

参考:

1. 基本概念

扩展程序由HTML、CSS和JavaScript等web技术组成,并由浏览器提供了一系列增强API。

在项目文件夹根目录新建一个manifest.json的文件,用于声明相关配置。这跟Android项目下的manifest.xml的作用基本类似,用于声明程序的基本配置。

{   
    // 区分扩展程序和应用,在扩展程序中只能为2
    "manifest_version": 2,
    "name": "扩展程序测试",
    "version": "1.0",
    "description": "Chrome扩展",
    // 在chrome://extensions/扩展程序列表下的图标
    "icons": {},
    // 当进入指定URL页面时调用对应脚本
    "content_scripts": [
        {
            "matches": ["*://www.baidu.com/*"],
            "js": ["js/content.js"]
        }
    ],
    // 在浏览器右上角的扩展程序的属性
    "browser_action": {},
    // 一些权限设置
    "permissions": [],
    // 一直在后台运行的脚本
    "background": {},
    // 选项页面
    "options_page": "options.html"
}

配置文件主要用来指定文件路径、,配置权限等。

以网络申请权限举例,正常情况下,由于浏览器的同源策略,是禁止JavaScript进行跨域请求的,但是对于扩展程序而言这未免太苛刻了。可以在配置文件中进行声明

"permissions": [
    "http://*/*", "https://*/*"
]

然后就可以使用XHR或者fetch进行跨域网络请求了

2. 页面模块

从配置文件我们可以发现,整个扩展程序可分为下面几个部分

  • content_scripts,进入指定URL页面可注入的脚本
  • background,在后台运行
  • browser_action,右上角弹窗
  • options_page,扩展程序选项页面

2.1. content_scripts

扩展程序一个非常重要的功能就是可以操作用户正在浏览的页面

在配置文件中通过指定对应的match字段,匹配页面url,并在浏览器访问这些页面时,注入对应的脚本文件。

"content_scripts": [
  {
    "matches": [
      "*://local.com/*", // 其中 * 表示通配符
      "*://www.amazon.com/*"
    ],
    "js": [
      "js/content.js"
    ]
  }
],

里面的脚本运行在于网页文档相同的一个沙盒环境下,可以访问和操作页面的DOM和BOM,但是无法访问原始文档里面的JS全局变量等。

2.2. browser_action

有时候需要扩展程序快速展示一些信息功能,右上角的弹窗页面就很有用

对应的配置文件

"browser_action": {
  "default_icon": {
    "19": "images/icon19.png",
    "38": "images/icon38.png"
  },
  "default_popup": "popup.html" // 可以在页面上通过script标签引入对应的页面js文件
},

弹窗页可以看做是扩展程序与用户的交互窗口。

2.3. options_page

如果配置了options_page选项,并制定了对应的页面,则右击右上角图标是,会出现“选项”栏,点击可跳转到对应的选项页面。选项页面一般用于为用户进行一些插件配置操作,诸如偏好设置等。

options_page可以看做是我们为扩展程序提供的一个主页。

2.4. background

上面部分是基于前端进行开发的,包括DOM、html、CSS等,可以发现,他们都需要用户的主动操作,要么访问对应的页面,要么点击右上角的图标弹出页面,这类页面统称为UI页面。

如果希望扩展程序自动运行并常驻后台来实现一些特定的功能,就可以使用background后台页面。

"background": {
    "page": "background.html"
  },

一般情况下,后台页面都是看不见的,但是也可以在后台脚本中操作后台页面的DOM等,有时候需要实现一些特定的功能,可以指定page,否则一般指定scripts即可。

这里有一个利用后台页面实现复制内容到剪切板的功能

function copy(str){
    // 后台页面上提供了一个#sandbox的textarea
   var sandbox = document.getElementById("sandbox");

    sandbox.value = str;
    sandbox.select();
    document.execCommand("copy");
    sandbox.value = "";

    return str;
}

后台脚本可以使用更丰富的chrome接口,去实现特定的功能。

3. 页面交互

一个扩展程序往往不止一个页面,有时候页面之间需要进行数据传递或事件通知,这时候需要考虑页面交互的问题

3.1. 本地存储

一种常见的应用场景是选项页面和其他页面之间的配置选项交互,由于同一个扩展程序的页面(除了注入页面的content_scripts)被认为在同一个域下,因此只需要通过localStorage就可以轻松做到数据的传递。

// options.js
localStorage.setItem(STORAGE_KEY, 1)

// popup.js
let val = localStorage.getItem(STORAGE_KEY)

3.2. UI页面与后台页面的交互

另外一种应用场景时某个页面需要主动通知其他页面,可以通过chrome.runtime接口进行

// content.js 发送信息
chrome.runtime.sendMessage(message);

// background.js 接收消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    // message就是传递的data
});

由于sendMessage无法指定特定的listener,因此onMessage的监听器会接收到所有的消息推送,如果需要区分消息,则需要开发者自己实现相关的逻辑。message可以是字符串也可以是对象,因此可以在参数上携带发送者的消息。

let messageStrategy = {
  // ... 对应的命令逻辑
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    let {command, content} = message
    messageStrategy[command] && messageStrategy[command](content)
});

3.3. 后台页面与标签页交互

试想这样一个需求:自动复制当前标签页文档某个dom内的文字。我们先拆分一下需求

  • 当前页面的数据在content_scripts中获取,然后通过runtime传递到后台页面
  • 复制功能在后台页面实现

上面的逻辑存在一个问题,由于content_scripts是注入到指定标签页执行的,只会执行一次。在标签页来回切换时,则复制到的内容不一定是当前标签页的数据。可以通过chrome.tabs解决这个问题

在content_scripts中执行获取数据的逻辑,通过监听标签页的激活事件来重新执行content_scripts,达到始终获取到当前标签页的目的

chrome.tabs.onActivated.addListener(function (activeInfo) {
    let {tabId, windowId} = activeInfo
    // 重新执行执行脚本,刷新页面逻辑等
    chrome.tabs.executeScript(tabId, {
        file: "/js/content.js",
        runAt: 'document_end'
    });
});

chrome.tabs还有很多其他的功能,比如查询某个标签页、向标签页注入js和css等。需要注意的是,使用tabs时记得先在权限中声明

"permissions": [
    "tabs"
  ]

4. 遇见的一些问题

在开发时遇见了下面一些问题,这里一并记录下来

脚本形式

脚本只能通过script标签引入,使用行内脚本会提示

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:"...

不过行内样式是可以正常使用的。

api类别

Chrome提供的大部分API是不支持在content_scripts中运行的,在background中执行没有问题,此外background内的代码貌似静默执行,不会报错

match通用匹配

注意配置的match,如果需要在全站页面使用,后面的路径记得配置*,例如*://www.amazon.com/*,否则只有首页可以注入脚本...

5. 小结

开发扩展程序是一件很有趣的事,这里简单整理了Chrome扩展程序的开发心得,主要包括主要的几个模块,开发过程主要是围绕着这几个模块页面进行的,并在不同的页面上调用浏览器接口,实现对应的功能需求。

而至于浏览器提供的相关接口,按需查阅文档就可以了,不必过于看重,我整理了一下后面可能会用到的特性

  • webRequest,用于分析、阻断及更改网络请求的方法,监听整个网络请求的声明的声明周期
  • fileSystem,文件系统,读入文件或者写出文件

接下来打算写一个构建mock数据的扩展程序,主要为了解决jsonp无法被mockjs拦截的问题...