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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
|
---
title: 你的第二个 WebExtension
slug: Mozilla/Add-ons/WebExtensions/Your_second_WebExtension
translation_of: Mozilla/Add-ons/WebExtensions/Your_second_WebExtension
original_slug: Mozilla/Add-ons/WebExtensions/Walkthrough
---
<p>{{AddonSidebar}}</p>
<p>如果你已经阅读了 <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension">你的第一个扩展</a>,那么你现在已经知道如何写一个扩展了。在这篇文章,我们将写一个稍微复杂一点点的扩展来为你展示更多的一些API 。</p>
<p>这个扩展会添加一个新按钮到 Firefox 的工具栏。在用户点击该按钮时,我们会显示一个弹出窗(popup)来让他们选择一种动物。在他们选择之后,我们会将当前网页替换为他所选动物的图片。</p>
<p>要实现这点,我们将:</p>
<ul>
<li><strong>定义一个浏览器动作(browser action),这用来附加一个按钮到 Firefox 的工具栏。</strong><br>
对于该按钮,我们将提供:
<ul>
<li>一个文件名为 "beasts-32.png" 的图标</li>
<li>按钮被按下时要打开的弹出窗。该弹出窗将包含 HTML、CSS 和 JavaScript。</li>
</ul>
</li>
<li><strong>为扩展定义一个图标</strong>,叫做“beasts-48.png”。这个将会在Add-ons管理器中显示。</li>
<li><strong>写一个内容脚本 "beastify.js",用于注入到网页中。</strong><br>
这是用来实际修改页面的代码。</li>
<li><strong>打包一些动物的图像,用以替换网页中的图像。</strong><br>
我们让图像成为 “Web 可访问资源”( web accessible resources ),以便页面可以引用它们。</li>
</ul>
<p>你可以想象这样的扩展的整体结构:</p>
<p><img alt="" src="https://mdn.mozillademos.org/files/13671/Untitled-1.png"></p>
<p>这是一个非常简单的扩展,但也展示了 WebExtensions API 的许多基本概念:</p>
<ul>
<li>添加一个按钮到工具栏</li>
<li>定义一个将使用 HTML、CSS 和 JavaScript 的弹出窗</li>
<li>注入content scripts到网页</li>
<li>content scripts与扩展的其他部分之间的通信</li>
<li>打包你的扩展的资源,使其可被网页所用</li>
</ul>
<p>你可以在 GitHub 找到<a href="https://github.com/mdn/webextensions-examples/tree/master/beastify">该扩展的完整的源代码</a>。</p>
<p>写这个扩展,你需要45或更高版本的firefox。</p>
<h2 id="编写扩展">编写扩展</h2>
<p>创建一个新目录,并切换到该目录:</p>
<ol>
<li>
<pre class="brush: bash">mkdir beastify
cd beastify</pre>
</li>
</ol>
<h3 id="manifest.json">manifest.json</h3>
<p>现在创建一个名为 "manifest.json" 的文件,并对其添加下列内容:</p>
<ol>
<li>
<pre><code>{
"manifest_version": 2,
"name": "Beastify",
"version": "1.0",
"description": "Adds a browser action icon to the toolbar. Click the button to choose a beast. The active tab's body content is then replaced with a picture of the chosen beast. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#beastify",
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/beastify",
"icons": {
"48": "icons/beasts-48.png"
},
"permissions": [
"activeTab"
],
"browser_action": {
"default_icon": "icons/beasts-32.png",
"default_title": "Beastify",
"default_popup": "popup/choose_beast.html"
},
"web_accessible_resources": [
"beasts/frog.jpg",
"beasts/turtle.jpg",
"beasts/snake.jpg"
]
}</code></pre>
</li>
</ol>
<ul>
<li>最开始的三个属性: <strong><code>manifest_version</code></strong>, <strong><code>name</code></strong>, <strong><code>version</code></strong>, 是必须的并且包含了插件最基本的信息。</li>
<li><a href="/zh-CN/docs/Mozilla/Tech/XUL/Attribute/description">description </a>和 <a href="/Add-ons/WebExtensions/manifest.json/homepage_url">homepage_url </a>是可选的,但是推荐填写,因为它们提供关于扩展的有用信息。</li>
<li><a href="/zh-CN/Add-ons/WebExtensions/manifest.json/icons">icons </a>也是可选但推荐的,它决定了插件在附加组件中的图标。</li>
<li><strong><code>permissions</code></strong> 列出了插件所需要的权限。在这里我们仅需要 <a href="/zh-CN/Add-ons/WebExtensions/manifest.json/permissions#activeTab_permission">activeTab permission</a>。</li>
<li><strong><code>browser_action</code></strong> 指定了工具栏按钮。我们在这里提供了三个信息片段:
<ul>
<li><strong><code>default_icon</code></strong> 是必须的,指定了按钮的图标。</li>
<li><strong><code>default_title</code></strong> 是可选的,用于按钮的提示。</li>
<li><strong><code>default_popup</code></strong> 在你想要当用户点击按钮时显示出一个弹出窗时使用。而在这里,我们需要,所以我们列入这个键并将其指向扩展中包括的一个HTML文件。</li>
</ul>
</li>
<li><strong><code>web_accessible_resources</code></strong> 列出了页面可访问的资源。例如由于当前插件使用动物图像替换了页面原有的图像,当前的动物图像要可以被页面访问。</li>
</ul>
<p>需要注意,所有路径是相对于 manifest.json 。</p>
<h3 id="图标">图标</h3>
<p>插件应该有一个图标。这个图标被用于显示在附加组件管理器中(可以通过"about:addons"来访问)。当前插件中manifest.json指定了我们插件的图标位于"icons/beasts-48.png"。</p>
<p>创建“icons”文件夹,并将图标命名为“beasts-48.png”。你可以使用我们例子中的<a href="https://github.com/mdn/webextensions-examples/blob/master/beastify/icons/beasts-48.png">图标</a>,它是从 <a href="https://www.iconfinder.com/iconsets/free-retina-icon-set">Aha-Soft’s Free Retina iconset</a> 截取的,使用需要遵循该网站的<a href="http://www.aha-soft.com/free-icons/free-retina-icon-set//zh-CN/docs/">许可证</a>。</p>
<p>如果你使用自己的图标,它的尺寸应该是48<math><semantics><mo>×</mo><annotation encoding="TeX">\times</annotation></semantics></math>48像素的。同时,对于高分辨率的设备,可以提供96<math><semantics><mo>×</mo><annotation encoding="TeX">\times</annotation></semantics></math>96像素的图片。此时,manifest.json应当这样配置:</p>
<ol>
<li>
<pre class="brush: json line-numbers language-json"><code class="language-json"><span class="key token">"icons":</span> <span class="punctuation token">{</span>
<span class="key token">"48":</span> <span class="string token">"icons/beasts-48.png"</span><span class="punctuation token">,</span>
<span class="key token">"96":</span> <span class="string token">"icons/beasts-96.png"</span>
<span class="punctuation token">}</span></code></pre>
</li>
</ol>
<h3 id="工具栏按钮">工具栏按钮</h3>
<p>工具栏按钮也需要一个图标,并且我们的 manifest.json 承诺我们会为该工具栏在 "icons/beasts-32.png" 提供一个图标。</p>
<p>将一个图标命名为为 "beasts-32.png"并保存到"icons"文件夹。你可以使用例子中的<a href="https://github.com/mdn/webextensions-examples/blob/master/beastify/icons/beasts-32.png">图片</a>,它是取自 <a href="http://www.iconbeast.com/free">IconBeast Lite 图标集</a>并按其<a href="http://www.iconbeast.com/faq/">许可协议</a>授权使用。</p>
<p>如果你没有弹出窗,用户点击的事件会直接分派到你的插件中。<span style="line-height: 1.5;">如果你制作了弹出窗,用户点击会直接打开这个弹出窗,而不会被分派给插件。</span><span style="line-height: 1.5;">本例中我们需要弹出窗,因此我们现在开始写它。</span></p>
<h3 id="弹出窗">弹出窗</h3>
<p>该弹出窗的函数是让用户选择三种动物的其中一种。</p>
<p>在根目录下创建“popup”文件夹,用于存放弹出窗的代码。弹出窗由以下文件组成:</p>
<ul>
<li><strong><code>choose_beast.html</code></strong> 定义了界面的主面板</li>
<li><strong><code>choose_beast.css</code></strong> 美化内容</li>
<li><strong><code>choose_beast.js</code></strong> 通过在当前活跃的标签页中运行内容脚本(content script)处理用户的选择</li>
</ul>
<pre class="brush: bash line-numbers language-bash"><code class="language-bash"><span class="function token">mkdir</span> popup
<span class="function token">cd</span> popup
<span class="function token">touch</span> choose_beast.html choose_beast.css choose_beast.js</code></pre>
<h4 id="choose_beast.html">choose_beast.html</h4>
<p>HTML 文件就像这样:</p>
<ol>
<li>
<pre class="brush: html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="choose_beast.css"/>
</head>
<body>
<div id="popup-content">
<div class="button beast">Frog</div>
<div class="button beast">Turtle</div>
<div class="button beast">Snake</div>
<div class="button reset">Reset</div>
</div>
<div id="error-content" class="hidden">
<p>Can't beastify this web page.</p><p>Try a different page.</p>
</div>
<script src="choose_beast.js"></script>
</body>
</html></pre>
</li>
</ol>
<p>我们有一个ID为 <code>"popup-content"</code> 的<a href="/zh-CN/docs/Web/HTML/Element/div"><div></a>元素包含了每个动物选择。我们还有另外一个<code><div></code> 元素,它的ID为 <code>"error-content"</code> ,class为<code>"hidden"</code>。我们将会使用它以防初始化弹窗的时候出问题。</p>
<p>注意我们引入了CSS和JS文件,就像网页一样。</p>
<h4 id="choose_beast.css">choose_beast.css</h4>
<p>CSS 固定了弹出窗的大小,确保3个选择填充满空间,并给了他们基本点样式。同时隐藏了<code>class="hidden"</code>的元素,这意味着我们的<code>"error-content"</code> <code><div></code> 将会被默认隐藏:</p>
<ol>
<li>
<pre class="brush: css">html, body {
width: 100px;
}
.hidden {
display: none;
}
.button {
margin: 3% auto;
padding: 4px;
text-align: center;
font-size: 1.5em;
cursor: pointer;
}
.beast:hover {
background-color: #CFF2F2;
}
.beast {
background-color: #E5F2F2;
}
.reset {
background-color: #FBFBC9;
}
.reset:hover {
background-color: #EAEA9D;
}</pre>
</li>
</ol>
<h4 id="choose_beast.js">choose_beast.js</h4>
<p>我们在弹出窗的脚本中监听点击事件。 如果用户选择其中一个动物,我们在当前标签页中插入一段内容脚本。一旦内容脚本加载,我们发送一条有关动物选择的信息:</p>
<ol>
<li>
<pre class="brush: js">/**
* CSS to hide everything on the page,
* except for elements that have the "beastify-image" class.
*/
const hidePage = `body > :not(.beastify-image) {
display: none;
}`;
/**
* Listen for clicks on the buttons, and send the appropriate message to
* the content script in the page.
*/
function listenForClicks() {
document.addEventListener("click", (e) => {
/**
* Given the name of a beast, get the URL to the corresponding image.
*/
function beastNameToURL(beastName) {
switch (beastName) {
case "Frog":
return browser.extension.getURL("beasts/frog.jpg");
case "Snake":
return browser.extension.getURL("beasts/snake.jpg");
case "Turtle":
return browser.extension.getURL("beasts/turtle.jpg");
}
}
/**
* Insert the page-hiding CSS into the active tab,
* then get the beast URL and
* send a "beastify" message to the content script in the active tab.
*/
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
});
});
}
/**
* Remove the page-hiding CSS from the active tab,
* send a "reset" message to the content script in the active tab.
*/
function reset(tabs) {
browser.tabs.removeCSS({code: hidePage}).then(() => {
browser.tabs.sendMessage(tabs[0].id, {
command: "reset",
});
});
}
/**
* Just log the error to the console.
*/
function reportError(error) {
console.error(`Could not beastify: ${error}`);
}
/**
* Get the active tab,
* then call "beastify()" or "reset()" as appropriate.
*/
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);
}
});
}
/**
* There was an error executing the script.
* Display the popup's error message, and hide the normal 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}`);
}
/**
* When the popup loads, inject a content script into the active tab,
* and add a click handler.
* If we couldn't inject the script, handle the error.
*/
browser.tabs.executeScript({file: "/content_scripts/beastify.js"})
.then(listenForClicks)
.catch(reportExecuteScriptError);</pre>
</li>
</ol>
<p>从96行开始。只要弹出窗加载完,popup scrpit 就会使用 <code><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/executeScript">browser.tabs.executeScript()</a></code> API在活跃标签页执行 content script。如果执行 content scrpit成功,content script会在页面中一直保持,直到标签被关闭或者用户导航到其他页面。</p>
<p><code>browser.tabs.executeScript()</code>调用失败的常见原因是你不能在所有页面执行content scripts。例如,你不能在特权浏览器页面执行,像about:debugging,你也不能在<a href="https://addons.mozilla.org/">addons.mozilla.org</a>域执行。如果调用失败,<code>reportExecuteScriptError()</code>会隐藏<code>"popup-content"</code> <code><div></code>,并展示<code>"error-content"</code> <code><div></code>, 然后打印一个错误到<a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Debugging">控制台</a>。</p>
<p>如果成功执行 content script ,我们会调用 <code>listenForClicks()</code>。这个监听了弹窗上的点击事件。</p>
<ul>
<li>如果点击有 <code>class="beast"</code>的按钮上,将会调用 <code>beastify()</code>.</li>
<li>如果点击有 <code>class="reset"</code>的按钮上,将会调用 <code>reset()</code>.</li>
</ul>
<p><code>beastify()</code> 函数做了三件事:</p>
<ul>
<li>将被点击的按钮映射到一个指向特定动物图片的URL</li>
<li>通过<code><a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/insertCSS">browser.tabs.insertCSS()</a></code> API 向页面注入一些CSS来隐藏整个页面的内容</li>
<li>通过<code><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/sendMessage">browser.tabs.sendMessage()</a></code> API 向 content script 发送“beastify”信息,要求其 beastify 页面,同时向其传递一个指向动物图片的URL</li>
</ul>
<p><code>reset()</code> 函数实际上就是撤销 beastify :</p>
<ul>
<li>通过 <code><a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/removeCSS">browser.tabs.removeCSS()</a></code> API 移除我们添加的CSS</li>
<li>向 content script 发送“reset”信息要求其重置页面</li>
</ul>
<h3 id="The_content_script">The content script</h3>
<p>在扩展的根目录下创建一个新的文件夹,叫做"content_scripts",然后在里面新建一个新的名为 "beastify.js" 的文件,内容如下:</p>
<ol>
<li>
<pre class="brush: js">(function() {
/**
* Check and set a global guard variable.
* If this content script is injected into the same page again,
* it will do nothing next time.
*/
if (window.hasRun) {
return;
}
window.hasRun = true;
/**
* Given a URL to a beast image, remove all existing beasts, then
* create and style an IMG node pointing to
* that image, then insert the node into the document.
*/
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);
}
/**
* Remove every beast from the page.
*/
function removeExistingBeasts() {
let existingBeasts = document.querySelectorAll(".beastify-image");
for (let beast of existingBeasts) {
beast.remove();
}
}
/**
* Listen for messages from the background script.
* Call "beastify()" or "reset()".
*/
browser.runtime.onMessage.addListener((message) => {
if (message.command === "beastify") {
insertBeast(message.beastURL);
} else if (message.command === "reset") {
removeExistingBeasts();
}
});
})();</pre>
</li>
</ol>
<p>content script做的第一件事是检查全局变量 <code>window.hasRun</code>:如果它被设置了,脚本直接返回,否则设置<code>window.hasRun</code>并继续。原因是每次用户打开弹出窗,弹出窗就会在活跃页面执行一个content script ,所以我们可能会在单个页面运行多个脚本实例。如果是这样的话,我们需要保证只有一个实例在做所有事情。</p>
<p>然后,从第40行开始,content script 监听来自弹出窗的信息,使用<code><a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/onMessage">browser.runtime.onMessage</a></code> API。在上面我们看到弹出窗脚本能够发送两种不同的信息:"beastify" and "reset"。</p>
<ul>
<li>如果信息是 "beastify",我们期待它包含一个指向动物图片的URL。我们移除先前调用添加的动物图片,然后构造并添加一个 src属性被设置动物图片URL 的<code><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img"><img></a></code> 元素。</li>
<li>如果信息是 "reset",我们只需要移除所有被添加的动物片。</li>
</ul>
<h3 id="动物们">动物们</h3>
<p>最后,我们要加入包含动物们的图像。</p>
<p>创建"beasts"文件夹,之后将图片放入并命名。你可以从 <a href="https://github.com/mdn/webextensions-examples/tree/master/beastify/beasts">the GitHub repository</a>,或这里下载图片:</p>
<p><img alt="" src="https://mdn.mozillademos.org/files/11459/frog.jpg" style="display: inline-block; height: 200px; margin: 20px; width: 200px;"><img alt="" src="https://mdn.mozillademos.org/files/11461/snake.jpg" style="display: inline-block; height: 200px; margin: 20px; width: 200px;"><img alt="" src="https://mdn.mozillademos.org/files/11463/turtle.jpg" style="display: inline-block; height: 200px; margin: 20px; width: 200px;"></p>
<h2 id="测试">测试</h2>
<p>请仔细确认项目目录如下所示:</p>
<ol>
<li>
<pre><code>beastify/
beasts/
frog.jpg
snake.jpg
turtle.jpg
content_scripts/
beastify.js
icons/
beasts-32.png
beasts-48.png
popup/
choose_beast.css
choose_beast.html
choose_beast.js
manifest.json</code></pre>
</li>
</ol>
<p>Firefox 45开始,你可以临时从硬盘中安装扩展</p>
<p>在Firefox地址栏中输入:about:debugging,单击“临时载入附加组件”,然后选择你的manifest.json文件。</p>
<p>然后你应该已经看到扩展图标出现在了Firefox的工具条上:</p>
<p>{{EmbedYouTube("sAM78GU4P34")}}</p>
<p>打开一个网页,然后点击图标,选择一个动物,然后观察网页的变化</p>
<p>{{EmbedYouTube("YMQXyAQSiE8")}}</p>
<h2 id="用命令行开发">用命令行开发</h2>
<p>你可以通过使用 <a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext">web-ext</a> 工具来将临时安装的工作自动化,试试这个:</p>
<ol>
<li>
<pre class="brush: bash"><code>cd beastify
web-ext run</code></pre>
</li>
</ol>
|