愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Chrome Extension 开发总结(一):基础知识

一、什么是Chrome Extension

Chrome ExtensionGoogle所提供的在Chrome浏览器体系里进行开发,增强浏览器功能的一套技术方案,可以采用Web技术进行开发,主要使用的是HTMLCSSJS。相关的资源最终打成一个.crx包,这个.crx包可以上架Chrome商店,从而可以进行分发和下载。


二、Chrome Extension的能力

Chrome Extension提供了许多的API可供我们使用,通过对这些API的使用,我们可以做到:

  • 控制书签、控制下载、控制窗口、控制标签、控制网络请求
  • 监听各类事件
  • 自定义原声的菜单
  • 完善的通信机制
  • 等等


三、目录结构与开发调试

Chrome Extension项目并没有严格地限制目录结构,唯一的限制是要求根目录下需要有manifest.json文件,这个文件用以申请一些权限、配置一些基础信息。
调试Chrome Extension也很容易,只需要通过地址栏输入:chrome://extensions即可进入插件管理页面,如下:

在这个页面里,我们可以打开右上角的开发者模式,如此便可通过点击左上侧的加载已解压的扩展程序按钮来直接加载项目目录,这对于开发而言是非常方便的。而在最终部署.crx包时,则可以使用打包扩展程序功能。
在开发过程中,当源码有变更时,我们需要手动点击“刷新”图标,来重新加载Chrome Extension,如下:


四、manifest.json

这个文件是一个Chrome Extension中最为关键和不可缺少的文件,所有插件相关的配置、权限的申请都通过这个文件完成。它必须放置于项目文件的根目录中,并且以下字段是必填的:

  • manifest_version,清单文件的版本,固定为2即可
  • name,插件的名称
  • version,插件的版本

除此之外,常见的配置信息如示例:

{
    "manifest_version": 2,
    "name": "demo-plugin",
    "version": "1.0.0",
    "description": "描述信息",
    // 图标,最好提供多个尺寸,但是只提供一个尺寸也是可以的
    "icons": {
        "16": "img/icon.png",
        "48": "img/icon.png",
        "128": "img/icon.png"
    },
    // 常驻后台的脚本
    "background": {
        // 可以用 page 也可以用 scripts 指明
        "page": "background.html",
        // scripts的情况下,会自动生成一个html页引用scripts
        // "scripts": ["js/background.js"]
    },
    // 浏览器右上角图标对应的脚本,有 browser_action,page_action 和 app
    "browser_action": {
        "default_icon": "img/icon.png",
        // 图标悬停标题(可选)
        "default_title": "悬停标题",
        "default_popup": "popup.html"
    },
    // 某些特定页面打开才显示图标(与 browser_action,app 是冲突的,三选一)
    // "page_action": {
    //     "default_icon": "img/icon.png",
    //     // 图标悬停标题(可选)
    //     "default_title": "PageAction悬停标题",
    //     "default_popup": "popup.html"
    // }
    // 需要直接注入页面的JS
    "content_scripts": [
        {
            // 指定在哪些URL里注入,如"http://*/*","https://*/*",“*://*/*.png"等
            // 可以使用"<all_urls>"表明是匹配所有地址
            "matches": ["<all_urls>"],
            // JS将按照指定的顺序注入
            "js": ["js/a.js", "js/b.js", "js/c.js"],
            // 注入CSS
            "css": ["css/custom.css"],
            // 指定注入的时机,可以是 document_start,document_end,document_idle 中的一个
            // 分别是 开始加载时、加载结束时、页面空闲时,默认是 document_idle
            "run_at": "document_start"
        }
    ],
    // 权限申请
    "permissions": [
        "contextMenus", // 右键菜单
        "tabs", // 标签
        "notifications", // 通知
        "webRequest", // web请求
        "webRequestBlocking",
        "storage", // 存储
        "https://*/*", // 可以通过 executeScript 或 insertCSS 访问的网站
        "http://*/*"
    ],
    // 普通页面能够直接访问的资源列表
    "web_accessible_resources": ["js/inject.js"],
    // 插件主页
    "homepage_url": "https://ruphi.cn",
    // 覆盖浏览器的默认页面
    "chrome_url_overrides": {
        // 覆盖默认的新标签页
        "newtab": "newtab.html"
    },
    // 插件配置页
    // 这是 Chrome 40 之前支持的写法
    // "options_page": "options.html"
    // 这是 Chrome 40 之后支持的写法
    "options_ui": {
        "page": "options.html",
        // 是否添加一些默认样式
        "chrome_style": true
    },
    // 向地址栏注册一个关键词,用以提供搜索建议,只能设置一个
    "omnibox": {
        "keyword": "addto"
    },
    // 默认语言
    "default_locale": "zh_CN",
    // devtools 页面的入口,只能指向一个 HTML 文件(不可以是JS)
    "devtools_page": "devtools.html"
}


五、脚本类型

Chrome Extension中,有background scriptcontent scriptpopup script,要掌握好Chrome Extension的开发,分清楚这些脚本之间的差别是很重要的

1、content-scripts

content-scripts可以向页面注入脚本或者CSS,借助它可以实现通过配置的方式向指定页面注入JSCSS,因此可以实现广告屏蔽页面CSS定制等等,在manifest.json里的配置的定义为:

interface Manifest {
    // ...
    content_scripts: Array<ContentScript>
}

interface ContentScript {
    matches: Array<string>,
    js: Array<string>,
    css: Array<string>
    run_at: 'document_start' | 'document_end' | 'document_idle'
}

需要注意的是,如果content_script里的代码需要在页面加载完成后做一些事情,那么需要指定为document_start,如下:

document.addEventListener('DOMContentLoaded', () => {
    console.log(`I've been executed`)
})

需要注意的是:

  • content-script可以跟原始页面共享DOM,但是不共享JS(即原页面的JS,如原页面的某个变量等)
  • content-script只能访问四种Chrome API,即为:
    • chrome.extension
      • chrome.extension.getURL
      • chrome.extension.inIncognitoContext
      • chrome.extension.lastError
      • chrome.extension.onRequest
      • chrome.extension.sendRequest
    • chrome.i18n
    • chrome.runtime
      • chrome.runtime.connect
      • chrome.runtime.getManifest
      • chrome.runtime.getURL
      • chrome.runtime.id
      • chrome.runtime.onConnect
      • chrome.runtime.onMessage
      • chrome.runtime.sendMessage
    • chrome.storage

要突破以上的限制,需要:

  • 共享JS,需要通过injected js实现
  • 访问其他API,需要通过通信机制让background帮忙调用

2、background

background是常驻内存的页面,其生命周期是五种脚本中最长的。它跟随浏览器打开和关闭,因此对于那些需要一直运行、启动就运行的代码,可以放在background里。此外,background具有非常高的权限,几乎可以调用所有的Chrome扩展API(除了devtools),并且可以无限制地跨域,而无需服务器支持CORS

其实,所有通过chrome-extension://id/xxx.html打开的页面,都可以直接跨域

background的配置定义为:

interface Manifest {
    // ...
    background: BackgroundScript
}

interface BackgroundScript {
    page?: string,
    scripts?: Array<string>,
    persistent: boolean
}

特别注意: 由于background的生命周期可能太长,长时间挂载在后台可能会影响性能,所以有了event-pages,即作为background的补充方案而出现,它是一种特殊的background,区别在于persistent参数为falseevent-pages的生命周期则为:在需要时被加载,在空闲时被关闭。被需要的时机,则可以是第一次安装extension、更新extension或者有content-script发过来消息时,等等。

3、popup script

这种脚本类型是在点击浏览器右上角图标(通过browser_action或者page_action指定)时打开的一个小窗口,在失去焦点后,这个窗口就会关闭。因此可以用来做一些临时性的交互。

popup script中,可以包含任意的HTML内容,而且会自适应内容的大小。它的配置定义为:

interface Manifest {
    // ...
    browser_action: BrowserAction
}

interface BrowserAction {
    default_icon?: string,
    default_title?: string,
    default_popup: string
}

在权限上,popup scriptbackground差不多,最大的不同是生命周期的不同,popup script的生命周期其与聚焦,止于失焦。可以通过chrome.extension.getBackgroundPage()获取backgroundwindow对象


六、展示形式

Chrome Extension可展示在浏览器不同的地方,这些地方总共有:

1、browserAction

browserAction展示在浏览器右上角,地址栏右边,示例如图:

配置示例:

{
    // ...
    "browser_action": {
        "default_icon": "img/icon.png",
        "default_title": "标题",
        "default_popup": "popup.html"
    }
}

官方推荐使用19×19px的图片作为图标,也可以使用更大的图片,但是会被自动压缩。除了在manifest.json中制定图标之外,也可以通过调用API交由background设置,如下:

chrome.browserAction.setIcon({
    path: './img/icon2.png'
})

需要注意的是,通过API设置的图片有大小要求,超过190px的图片将会导致设置失败。在browserAction中,我们还可以调用setBadgeText方法,来在图标上展示一段文字(但是有长度限制,中文2个字符,英文4个字符),示例如下:

chrome.browserAction.setBadgeText({ text: 'NEW' })
// 还可以调用以下代码来设置背景颜色
chrome.browserAction.setBadgeBackgroundColor({ color: [255, 0, 0, 255] })

效果如图:

2、pageAction

pageAction的展示位置和browserAction相同,但不同的是pageAction作用于特定页面,而browserAction作用于浏览器全局。在pageAction中,当未激活时,会显示为灰色。并且在灰色状态下,点击后只会弹出选项菜单。如图:

可以通过调用以下两个方法来控制图标的隐藏或者显示:

  • chrome.pageAction.show(tabId),显示图标
  • chrome.pageAction.hide(tabId),隐藏图标

3、右键菜单

通过chrome.contextMenus这个API,我们可以实现为右键菜单添加功能的效果。使用前需要现在manifest.json里申请权限,最简单的示例代码如下:

  • manifest.json
{
    // ...
    "permissions": [
        "contextMenus"
    ]
}
  • background.js
chrome.contextMenus.create({
    title: 'Try to click me',
    onclick() {
        alert('Hey, you clicked me')
    }
})

效果如下:

此外,我们还可以在title中指定%s为选中的文本,并对此进行操作,如下:

chrome.contextMenus.create({
    title: 'Add: %s',
    contexts: ['selection'],
    onclick(params) {
        console.log(params)
        alert(`Added ${params.selectionText}`)
    }
})

如此一来,便只有在选中文本的情况下,会添加菜单项:

关于右键菜单,常用的一些API操作则如下:

chrome.contextMenus.create({
    type: 'normal', // 可以设置类型,可选值有 normal / checkbox / radio / separator
    title: '菜单标题', // 除type为separator外,其他类型中title都是必须设置的,对于 contexts 为 selection 的情况,可以用`%s`拿到选中文本
    contexts: ['page'], // 出现菜单的上下文环境,可选值有 all / page / frame / selection / link / editable / image / video / audio
    onclick() {
        // 定义点击时触发的行为
    },
    parentId: 1, // 可以通过这个设置嵌套菜单,形成父子菜单
    documentUrlPatterns: 'https://*.ruphi.cn/*' // 指定特定页面展示此右键菜单
})

// 可以调用来删除某一个菜单项
chrome.contextMenus.remove(menuItemId)

// 可以调用来删除所有自定义右键菜单
chrome.contextMenus.removeAll()

// 可以调用来更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties)


七、参考资源