[TOC]

0x01 前言简述

题外话: 由于公司内部系统不只支持将后台网页中的数据进行导出为docx或者html而只能打印成PDF,所以为了丰衣足食选择了自己进行开发一个浏览器插件来提取网页中的代码片段与提前设置好的html模板将获取的html进行插入,但是我们需要从基础学习开始一步一步的接触Firefox扩展软件的开发;

此时可能您会问我为何不选择使用Chriome进行扩展开发?
答:要科学上网找官网资料(受限于学习环境),同时Firefox 与 Google Chrome、Opera 和 W3C 草案社区组织 所支持的 扩展(Extensions) API 在很大程度上兼容。大多数情况下为这些浏览器编写的扩展只需少许修改即可在 Firefox 或 Microsoft Edge 中运行,并且这种 API 与也完全兼容 多进程 Firefox。


1.Firefox扩展开发简述

描述:Firefox 开发者工具可以帮助我们在 PC 和移动设备上检查,编辑,调试 HTML、CSS 及 JavaScript。

官方开发参考地址:

说明:以下都是引用官方文档中一些介绍与关键点;

Q:扩展是什么?
A:扩展为浏览器添加特性与功能它通过熟悉的 web 技术——HTML,CSS 还有 JavaScript 来创建,利用网页上的 JavaScript 使用同一批 API,但扩展也可以访问扩展自己专用的 JavaScript API,所以进行插件开发您需要对Javascript有一定的了解;

Q:扩展有什么用处?

  • 为浏览器添加特性与功能,和在网页里编码相比他能帮助您处理页面上的数据按照开发者的流程进行,实际上扩展是用来提升或补充网站功能;
  • 让用户展现他们的个性:浏览器扩展可以操控网页的内容;
  • 从网页中添加或删除内容:你可能想要帮助用户从网页中阻止一些侵扰的广告;
  • 添加工具和新的浏览特性:给任务面板添加新特性,或者从URL地址,超链接,或者页面文字生成二维码。
  • 游戏开发:通过线下游戏的特性,或者探索新游戏的可能性来提供传统计算机游戏功能;
  • 添加开发工具:你可以提供网站开发工具给你的公司或者开发一个有用的技术或者你想分享的网站开发技术。


2.扩展关键字解释

描述:扩展是指一个包含若干文件的安装包,可直接分发至用户,根据下部分的第一个实例来做为参考,一个插件基本的框架如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> ToolsDir
> popup
- index.html
- index.js
- style.css
> images
- demo.png
> context_script
- Tools_demo1.js
- Tools_demo2.js
- style.css
> icons
- logo-16.png
- manifest.json
- main.js

manifest.json
该文件是每个 WebExtension 里面必须存在的文件,它包含了关于这个扩展插件基本的元数据(metadata),比如它的名字、版本和所需扩展API权限和资源路径。并且它也对 WebExtension 中其他资源文件进行了链接。

WeiyiGeek.manifest.json

WeiyiGeek.manifest.json

基础示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
{
//(1) 版本 名称 软件版本号 必不可少,author可选
"manifest_version": 2,
"name": "WeiyiGeekTools",
"short_name": "WTools",
"version": "1.0",
"author": "WeiyiGeek",

//(2) Firefox 扩展插件描述以及插件说明连接设置
"description": "Adds Borwser plug-in with WeiyiGeek.",
"homepage_url": "https://blog.weiyigeek.top",

//(3) 扩展图标 (16~96)像素,将显示在附加组件管理器上.
"icons":{
"16": "icons/logo-16.png",
"32": "icons/logo-32.png",
"48": "icons/logo-48.png"
},

//(4) 特定于浏览器的设置,指定一个附加组件 ID ,浏览器最小版本以及更新的manifest.json地址
"browser_specific_settings": {
"gecko": {
"id": "[email protected]",
"strict_min_version": "42.0",
"update_url": "https://example.com/updates.json"
}
},

//(5) URL 匹配特定模式的网页才进行加载脚本,比如放访问weiyigeek.top域名时候执行main.js中的操作;
// 匹配和排除匹配
"content_scripts": [
{
"exclude_matches": ["*://developer.mozilla.org/*"],
"matches": ["*://*.weiyigeek.top/*"],
"js": ["main.js"]
}
],

//(6) 后台脚本(background scripts)的职责,由于扩展常常需要独立于任何浏览器窗口或特定网页来维持一种长期的状态或者执行长期的操作,
// 可以添加多个后台脚本或者你也可以先引入一个后台页面,再在后台页面中引入脚本优势是ES 6 模块支持;
"background": {
"scripts": ["background-script.js"],
"page": "background-page.html"
},

//(7) 使用指定WebExtension API名称权限才能调用
"permissions": [
"activeTab"
],

//(8) 在工具栏中添加按钮
"browser_action": {
"default_icon": "icons/logo-48.png",
"default_title": "Tools",
"default_popup": "popup/index.html"
},

//(9) 地址栏添加按钮与工具栏按钮(或 browser action)非常相似。
// 仅default_icon是强制(必需)的
"page_action": {
"browser_style": true,
"default_icon": {
"16": "icons/logo-16.png",
"32": "icons/logo-32.png"
},
"default_title": "WeiyiGeekTools",
"default_popup": "popup/index.html",
"show_matches": ["*://*/*"]
},

//(10) 使打包好的内容可用于网页与目录脚本
"web_accessible_resources": [
"images/test.jpg"
],

//(11) 国际化设置如果扩展名包含_locales目录,则该key必须存在,否则不得存在
"default_locale": "zh",


//(12) 快捷键触发事件
"commands": {
"toggle-feature": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"linux": "Ctrl+Shift+U"
},
"description": "Send a 'toggle-feature' event"
}
},

//(13) 内容安全策略
"content_security_policy": "script-src 'self' https://example.com; object-src 'self'",

//(14) 用户脚本
"user_scripts": {
"api_script": "apiscript.js",
}
}

在运行时访问 manifest.json 键:

1
2
#通过 runtime.getManifest() 函数访问拓展的 manifest 数据:
browser.runtime.getManifest().version;

关键说明:

  • 1.background pages:后台脚本(background scripts)在拓展加载完毕后开始运行(执行一个长时间运行的逻辑),直到拓展被禁用或卸载。并且可以添加多份脚本就像同一个网页中的多个脚本一样,它们将会运行在同一上下文环境中。
    • 后台脚本的运行环境:DOM API,WebExtension API, 跨域访问, 网页内容(通过 message-passing API 与内容脚本通信),内容安全策略(Content Security Policy)。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      //#支持多个脚本和引入一个后台页面,再在后台页面中引入脚本这样做的优势是ES6 模块的支持;
      // manifest.json
      "background": {
      "page": "background-page.html"
      }

      // background-page.html
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="utf-8">
      <script type="module" src="background-script.js"></script>
      </head>
      </html>
  • 2.content scripts:与网页进行交互注意它与JavaScript在页面中的 <script>元素不一样。
  • 3.browser action files: 在工具栏中添加按钮。
  • 4.page action files: 添加到浏览器地址栏中的按钮,用户通过点击这个按钮与你的扩展进行交互。参考
  • 5.options pages: 为用户定义一个可浏览的UI界面,可以改变插件的设置。
  • 6.web-accessible resources: 使打包好的内容可用于网页与目录脚本。

补充工具:


0x02 扩展编写

1.第一个扩展实例

目录结构:

1
2
3
4
5
6
7
8
/mnt/f/WeiyiGeekTools
❯ tree
|____icons
| |____logo-16.png
| |____logo-32.png
| |____logo-48.png
|____main.js
|____manifest.json

扩展描述文件:manifest.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"manifest_version": 2,
"name": "WeiyiGeekTools",
"version": "1.0",
"description": "Adds Borwser plug-in with WeiyiGeek.",
"icons":{
"16": "icons/logo-16.png",
"32": "icons/logo-32.png",
"48": "icons/logo-48.png"
},
"content_scripts": [
{
"matches": ["*://*.WeiyiGeek.top/*"],
"js": ["main.js"]
}
],
"browser_specific_settings": {
"gecko": {
"id": "[email protected]"
}
}

}

JS操作地址:main.js

1
document.body.style.border = "5px solid blue";


安装与测试

  • 1.打开 Firefox 的 about:debugging 页面,点击”This Firefox” (在新版本的Firefox里),点击 “临时加载附加组件(Load Temporary Add-on)” 按钮,并选择你的附加组件目录(附加组件将会被安装,直到下次重启浏览器失效。):
WeiyiGeek.临时加载

WeiyiGeek.临时加载

  • 2.现在尝试访问访问,你将会在页面上看到有个红色的边框,与此同时修改main之后需要重新点击临时插件中的加载页面马上就会有变化
WeiyiGeek.执行效果

WeiyiGeek.执行效果


2.第二个扩展实例

描述:实现将扩展添加一个新按钮到 Firefox 的工具栏,并在用户点击该按钮时,我们会显示一个弹出窗(popup)来让他们选择操作;

实现要点:

  • 1.定义Browser Action设置相应的图标, 将我们的插件附加到Firefix工具栏之中;
  • 2.绑定一个popup弹出页面设置相应的操作按钮;
  • 3.建立一个main.js内容脚本实现,修改页面的代码;
  • 4.向页面插入图片和还原网页显示;

基础架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/mnt/f/WeiyiGeekTools
❯ tree
|____content_script
| |____backgroud.js
|____icons # Icons
| |____logo-16.png
| |____logo-32.png
| |____logo-48.png
|____images # Web Accessible Resources
| |____test.jpg
|____main.js # content script
|____manifest.json
|____popup # Browser Action 引用了 icons 下的图标
| |____index.html #界面的主面板
| |____index.js #通过在当前活跃的标签页中运行内容脚本(content script)处理用户的选择
| |____style.css #美化内容

主要代码:

  • manifest.json : 以下关键字不多介绍,不懂的看上文;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    {
    "manifest_version": 2,
    "name": "WeiyiGeekTools",
    "version": "1.0",

    "description": "Adds Borwser plug-in with WeiyiGeek.",
    "homepage_url": "https://blog.weiyigeek.top",

    "icons":{
    "16": "icons/logo-16.png",
    "32": "icons/logo-32.png",
    "48": "icons/logo-48.png"
    },

    "content_scripts": [
    {
    "matches": ["*://*.weiyigeek.top/*"],
    "js": ["main.js"]
    }
    ],

    "browser_specific_settings": {
    "gecko": {
    "id": "[email protected]"
    }
    },

    "permissions": [
    "activeTab",
    "alarms"
    ],

    "browser_action": {
    "default_icon": "icons/logo-48.png",
    "default_title": "Tools",
    "default_popup": "popup/index.html"
    },

    "page_action": {
    "browser_style": true,
    "default_icon": {
    "16": "icons/logo-16.png",
    "32": "icons/logo-32.png"
    },
    "default_title": "WeiyiGeekTools",
    "default_popup": "popup/index.html",
    "show_matches": ["*://*/*"]
    },

    "web_accessible_resources": [
    "images/test.jpg"
    ],

    "background": {
    "scripts": ["context_script/background.js"]
    }
    }
  • popup/index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./style.css">
    </head>
    <body>
    <div id="popup-content">
    <div class="button beast">Frog</div>
    <div class="button reset">Reset</div>
    <div class="button reget">网页内容更改</div>
    </div>
    <input type="text" name="title" id="title" placeholder="请输入标题">
    <div id="error-content" class="hidden">
    <p>Can't beastify this web page.</p><p>Try a different page.</p>
    </div>
    <script src="./index.js"></script>
    </body>
    </html>
  • popup/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    /**
    用CSS隐藏页面上的所有内容,拥有“beastify-image”类的元素除外。
    */
    const hidePage = `body > :not(.beastify-image) {
    display: none;
    }`;

    const hidePage1 = `div#container > :not(.main) {
    display: none;
    };`;

    /**
    浏览器插件监听单击按钮,并将相应的消息发送到脚本的页面内容。
    */
    function listenForClicks() {
    //监听器是重点(JS 精髓)
    document.addEventListener("click", (e) => {
    /**
    * 给定一个的名字,获取对应图像的URL.
    */
    function beastNameToURL(beastName) {
    switch (beastName) {
    case "Frog":
    return browser.extension.getURL("../images/test.jpg");
    }
    }

    /**
    浏览器插件监听单击按钮,并将相应的消息发送到脚本的页面内容。
    *插入隐藏页面的CSS到活动标签,然后获得野兽的URL和发送“beastify”消息到活动标签的内容脚本。
    */
    function beastify(tabs) {
    browser.tabs.insertCSS({code: hidePage}).then(() => {
    let url = beastNameToURL(e.target.textContent);
    browser.tabs.sendMessage(tabs[0].id, {
    command: "beastify",
    beastURL: url
    });
    });
    }

    /**
    发送一个“重置”消息到活动标签的内容脚本,将删除页面隐藏CSS从活动标签,
    */
    function reset(tabs) {
    browser.tabs.removeCSS({code: hidePage}).then(() => {
    browser.tabs.sendMessage(tabs[0].id, {
    command: "reset",
    });
    });
    }

    /* 自定义get方法设置网站标题 */
    function reget(tabs){
    browser.tabs.removeCSS({code: hidePage}).then(() => {
    let context = "测试测试";
    browser.tabs.sendMessage(tabs[0].id, {
    command: "reget",
    title: context
    });
    });
    }

    /**
    * 只需将错误记录到控制台。
    */
    function reportError(error) {
    console.error(`[Error]: ${error}`);
    }


    /**
    *获取活动标签,然后调用“beastify()”或“reset()”或者reget()方法。
    */
    if (e.target.classList.contains("beast")) {
    browser.tabs.query({active: true, currentWindow: true})
    .then(beastify)
    .catch(reportError);
    } else if (e.target.classList.contains("reset")) {
    browser.tabs.query({active: true, currentWindow: true})
    .then(reset)
    .catch(reportError);
    } else if (e.target.classList.contains("reget")) {
    browser.tabs.query({active: true, currentWindow: true})
    .then(reget)
    .catch(reportError);
    }
    });
    }

    /**
    执行脚本时出错。
    *显示弹出窗口的错误信息,隐藏正常UI。
    */
    function reportExecuteScriptError(error) {
    document.querySelector("#popup-content").classList.add("hidden");
    document.querySelector("#error-content").classList.remove("hidden");
    console.error(`Failed to execute beastify content script: ${error.message}`);
    }

    /**
    *当弹出窗口加载时,将内容脚本注入活动标签页,
    *并添加一个单击处理程序。
    *如果我们不能注入脚本,处理错误。
    */
    browser.tabs.executeScript({file: "/main.js"})
    .then(listenForClicks)
    .catch(reportExecuteScriptError);
  • main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"use strict";

(function() {
/**
*检查并设置一个全局保护变量。
*如果该内容脚本再次注入到同一页面,(下次它什么也做不了。)
*/
if (window.hasRun) {
return;
}
window.hasRun = true;

/**
*给一个野兽图片的URL,删除所有现有的野兽,然后
创建和样式的IMG节点指向
*该图像,然后插入节点到文档中。
*/
function insertBeast(beastURL) {
removeExistingBeasts();
let beastImage = document.createElement("img");
beastImage.setAttribute("src", beastURL);
beastImage.style.height = "100vh";
beastImage.className = "beastify-image";
document.body.appendChild(beastImage);
}

/**
*从页面上删除所有野兽。
*/
function removeExistingBeasts() {
let existingBeasts = document.querySelectorAll(".beastify-image");
for (let beast of existingBeasts) {
beast.remove();
}
}

/**
* 自定义方法
*/
function settingTitle(title){
document.getElementsByTagName("title")[0].innerText="网页修改测试";
}

/**
* 监听来自后台脚本的消息,调用“beastify()”或“reset()”
*/
browser.runtime.onMessage.addListener((message) => {
if (message.command === "beastify") {
insertBeast(message.beastURL);
} else if (message.command === "reset") {
removeExistingBeasts();
} else if (message.command === "reget") {
settingTitle(message.title)
}
});
})();

0x03 扩展API记录

extension
1
2
# 1.返回资源给对象
browser.extension.getURL("beasts/frog.jpg");


runtime

Function

  • 1.browser.runtime.onMessage: 监听监听来自弹出窗的信息即接收Tabs发送的Message信息并根据所发送的信息进行判断;
    1
    2
    3
    4
    5
    6
    7
    8
    //addListener() 监听
    browser.runtime.onMessage.addListener((message) => {
    if (message.command === "beastify") {
    insertBeast(message.beastURL);
    } else if (message.command === "reset") {
    removeExistingBeasts();
    }
    });
tabs

描述:该Webextend API与浏览器标签系统进行交互。你可以使用该API获取一个已打开标签的列表并且使用各种标准过滤标签,并进行 打开, 刷新,移动,重载,移除操作;

该API不能直接访问标签中的主机内容,但是你可以使用 tabs.executeScript() 或者 tabs.insertCSS() APIs,来插入javascript和CSS。

参考地址:https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/API/tabs

基础引入:

1
2
3
// # manifest.json
"permissions": ["tabs"]
"permissions": ["*://developer.mozilla.org/*", "tabs"]

Enums Value:

  • tabs.Tab : 该值包含了一个标签的信息 如 Tab.url, Tab.title, Tab.favIconUrl;
    1
    2
    3
    4
    5
    6
    7
    function logTabs(tabs) {
    for (let tab of tabs) {
    console.log(tab.url); // tab.url requires the `tabs` permission
    }
    }
    let querying = browser.tabs.query({});
    querying.then(logTabs, onError);


Function:

  • 1.browser.tabs.executeScript():要弹出窗加载完 popup scrpit 就会使用该API在活跃标签页执行 content script,如果执行 content scrpit成功,content script会在页面中一直保持,直到标签被关闭或者用户导航到其他页面;
    1
    2
    3
    browser.tabs.executeScript({file: "/content_scripts/beastify.js"}) // 调用执行内容脚本
    .then(listenForClicks) //#如果成功执行就会调用 listenForClicks()。
    .catch(reportExecuteScriptError); //#如果调用失败就会调用 reportExecuteScriptError()方法,然后隐藏"popup-content" <div>,并展示"error-content" <div>, 然后打印一个错误到控制台。
  • 2.browser.tabs.insertCSS() : 向页面插入CSS样式
    1
    2
    3
    4
    5
    6
    7
    8
    // # CSS 过滤器
    const hidePage = `body > :not(.beastify-image) {
    display: none;
    }`;
    browser.tabs.insertCSS(
    {code: hidePage}).then(() =>
    ..... //# js ES6
    }
  • 3.browser.tabs.removeCSS(): API 向页面移除某CSS样式
  • 4.browser.tabs.sendMessage(): API 向 content script 发送“retrunValue”信息,这个消息将被content scripts中 runtime.onMessage 事件的所有监听者收到,然后它们可以选择通过使用 sendResponse 这个方法发送一个response到background scripts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    browser.tabs.removeCSS({code: hidePage}).then(() => {
    browser.tabs.sendMessage(tabs[0].id, {
    command: "reset",
    retrunValue: "返回值给内容脚本", //可以返回文本或者json
    {greeting: "Hi from background script"}
    }.then(response => {
    console.log("Message from the content script:");
    console.log(response.response); //来之内容脚本:Promise.resolve({response: "Hi from content script"});
    }).catch(onError); //异常捕捉
    });
    }
    1. browser.tabs.query():获取所有包含指定属性的标签,如果没有属性则获取所有标签。
      js // # 选项卡在其窗口中是否处于活动状态。 // # 选项卡是否在当前窗口中 // # 父窗口或窗口的id。当前窗口的WINDOW_ID_CURRENT。 browser.tabs.query({active: true, currentWindow: true}) .then(beastify) .catch(reportError);https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/API/tabs/query