什么是 Firefox OS 附加组件,为什么我们需要它?
Firefox 的 附加组件生态系统 一直是桌面浏览器领域的 ключевой отличительной чертой。然而,移动空间缺乏强大的附加组件框架。Android 一些解决方案,例如 Xposed,但这些解决方案通常需要已扎根的手机,并且内容通常未经过可信方审核,这使得普通人难以使用。此外,它需要深入了解 Android 平台,这使得 web 开发人员很难参与其中。
在 Firefox OS 2.5 版本中,Mozilla 推出了 新的附加组件模型。这些新的附加组件利用了 WebExtensions API,Chrome 扩展程序就是基于此 API 构建的。由于 Firefox OS 是使用标准 HTML5 技术构建的,因此 web 开发人员可以轻松上手。
Firefox OS 附加组件最实用的功能之一是 "content_scripts"
API。它用于将自定义 JavaScript 和 CSS 注入选定的应用程序。注入的脚本将在应用程序启动时(或附加组件首次启用时)执行,因此我们可以立即更改应用程序或系统本身的行为!
在本文中,我们将向您展示如何使用新的 Firefox OS 附加组件框架开发 iOS 风格的“未读消息”通知。
未读图标附加组件的高级架构
我们的附加组件将在主屏幕上显示应用程序图标的未读消息数(参见下面的屏幕截图)。附加组件需要能够从系统中检索未读通知,将它们存储在持久存储中(这样我们即使在系统重启后也能检索到正确的值),并准确地将它们显示在主屏幕应用程序图标旁边。
注意:您可以在 GitHub 上找到所有 未读图标代码。下面的代码片段已简化以提高可读性,因此请勿直接从本文中的示例代码中复制粘贴。
幸运的是,我们无需入侵每个应用程序并编写自定义逻辑来检索未读消息数。相反,我们可以监听 Firefox OS 的内置通知。大多数核心应用程序(如电话、短信、电子邮件和日历)在收到新未读消息时都会使用此通知系统。我们可以分析这些通知的内容,以检测应用程序何时收到新未读消息。
现在,您可能很想将 JavaScript 直接注入主屏幕应用程序,这样附加组件就可以监听系统通知并同时在应用程序图标上添加红色数字。请记住,注入的 JavaScript 代码将具有与注入它的应用程序相同的权限,而主屏幕应用程序没有权限访问其他应用程序的通知。因此,我们需要将 JavaScript 注入系统应用程序,因为它可以访问所有其他应用程序的通知。
到目前为止,一切都好。但是,系统应用程序无法访问主屏幕的 DOM,因此我们还需要将 JavaScript 注入主屏幕应用程序才能在应用程序图标上添加未读数字。
为了让系统应用程序将未读消息数传达给主屏幕应用程序,我们使用了 Settings API。由于 Settings 是一个键值存储,因此我们可以让系统应用程序将 json 数据写入其中,然后主屏幕可以读取此数据并相应地更新 UI。
附加组件文件夹结构
Firefox OS 附加组件看起来像 基于 WebExtensions 的附加组件 和 开放式 Web 应用程序 的混合体。文件夹通常如下所示
.
├── icons
│ ├── icon-128.png
├── main.js
└── manifest.json
首先,让我们看一下 manifest.json
,它是 WebExtension 清单。在 manifest.json
中,我们明确指定要注入的 JavaScript 和 CSS 文件,以及我们要将它们注入到哪些应用程序和/或网页中。我们通过使用 content_script
字段来实现这一点
{
"content_scripts": [{
"matches": [ // which app we want to inject
"app://system.gaiamobile.org/index.html",
"app://verticalhome.gaiamobile.org/index.html*"
],
"js": ["main.js"] // the JavaScript we want to inject
"css": [],
}],
"name": "Unread Icons",
"description": "Add unread count on icons on the homescreen. Works on: Phone, Messages, Calendar, E-mail, Gallery, BzLite",
"author": "Shing Lyu",
"version": "1.0",
"icons": {
"128": "/icons/icon-128.png"
}
}
为了简化附加组件结构,我们不会将单独的 JavaScript 文件注入到系统和主屏幕中。相反,我们注入一个包含两个应用程序代码的单一 JavaScript 文件(main.js
),并检查 window.location
以确定我们当前所在的应用程序。稍后我们将详细介绍。
如何通过 WebIDE 安装附加组件以及如何在设置中启用它
在我们开始编写 JavaScript 代码之前,让我们先简要介绍一下如何安装和启用附加组件。
虽然您可能希望通过 Firefox Marketplace 安装最终产品,但在开发过程中,通过 WebIDE 安装附加组件更方便。使用最新版本的 Firefox Nightly for desktop,否则 WebIDE 可能无法识别附加组件包。
首先,打开您的 Firefox Nightly,然后打开 **工具 > Web 开发人员 > WebIDE**。然后使用“打开打包的应用程序”按钮打开您的附加组件(选择包含附加组件代码的文件夹,无需压缩)。连接您的 Firefox OS 手机或 模拟器,然后按下安装按钮。
附加组件成功安装后,我们需要在手机上启用它。转到“设置”应用程序,点击“附加组件”部分,您应该在列表末尾看到我们的附加组件。
点击标题 **未读图标**,然后切换开关以启用它。
请务必记住,启用附加组件时,如果您要注入的应用程序已经在运行(例如,系统应用程序),则附加组件代码将立即运行。否则,只有在打开应用程序后才会注入附加组件代码。还要注意,在“设置”中禁用附加组件时,正在运行的附加组件代码不会自动禁用。这意味着,如果您一直切换附加组件,就会注入多个代码副本。强烈建议您在附加组件代码中防止这种情况!有关详细信息,请参见 MDN 上的 防止多次注入。
如何在系统应用程序中收集未读消息
首先,为了在系统应用程序中收集未读通知,JavaScript 代码必须了解系统应用程序中的未读消息
if (window.location.toString() === 'app://system.gaiamobile.org/index.html') {
//Listen for system notification for new unread messages
}
一旦确定我们是在系统应用程序中,我们就会添加一个事件监听器来监听 mozChromeNotificationEvent
,即在通知弹出时触发的系统事件。以下是通知事件的示例
mozChromeNotificationEvent {
target: Window,
detail: Object, //The meat is in here
timeStamp: 1445421387703168,
… //irrelevant details are omitted
}
深入了解 detail
对象会发现
{
"id": "app://system.gaiamobile.org/manifest.webapp#tag:screenshot:1445421387658", //This is what we want
"appIcon": "app://system.gaiamobile.org/style/icons/system_126.png",
"appName": "System",
"data": {
"systemMessageTarget": "screenshot"
},
"manifestURL": "app://system.gaiamobile.org/manifest.webapp",
"text": "screenshots/2015-10-21-17-56-27.png",
"title": "Screenshot saved to Gallery",
… //irrelevant details are omitted
}
我们可以看到,在 event.detail
中,id
字段特别有趣。它包含应用程序清单 URL、事件类型(screenshot
)和一些随机 UUID。我们可以轻松地构建一个白名单,将事件详细信息 ID 模式映射到应用程序。
由于每个应用程序可能生成多种类型的通知,因此我们需要过滤掉仅告诉我们未读消息的通知,并将它们映射到应用程序图标。需要映射是因为有时生成通知的应用程序并不是具有未读消息的应用程序。例如,系统负责生成“已拍摄屏幕截图”通知,但未读通知数字应该显示在图库应用程序中。
收到通知后,我们会通过查看上面的查找表中其事件类型来检查它是否代表新的未读消息。如果通知确实代表特定应用程序的未读消息,则会增加该应用程序的未读计数。可以使用以下代码示例完成此操作
window.addEventListener('mozChromeNotificationEvent', function(evt){
var iconUrl = notificationIdToIconUrl(evt.detail.id); // filtering and mapping is done here
if (typeof iconUrl != "undefined") {
increaseUnreadByOne(iconUrl); //We’ll get to this later
}
})
在 increaseUnreadByOne()
函数内部,我们使用以下 JavaScript 对象来保存每个应用程序的未读消息数
{
"unreads": {
"app://calendar.gaiamobile.org/manifest.webapp": 0,
"app://communications.gaiamobile.org/manifest.webapp-dialer": 0,
"app://gallery.gaiamobile.org/manifest.webapp": 0,
"app://sms.gaiamobile.org/manifest.webapp": 0,
...
}
}
为了让主屏幕访问未读计数,我们需要从系统应用程序将这些值写入 Settings。由于许多应用程序可能希望同时访问设置,因此我们需要在读取/写入数据之前获取锁。以下是 increaseUnreadByOne()
函数的实现方式
function increaseUnreadByOne(appUrl){
var unreads = {}
var lock = navigator.mozSettings.createLock();
var setting_get = lock.get('unreads');
setting_get.onsuccess = function () {
if (typeof setting_get.result.unreads === "undefined"){
unreads[appUrl] = 1
}
else {
unreads = setting_get.result.unreads;
if (appUrl in unreads){
unreads[appUrl] += 1;
}
else {
unreads[appUrl] = 1;
}
}
// Write the unread count to the Settings database.
var lock = navigator.mozSettings.createLock();
var setting_set = lock.set({ 'unreads': unreads });
}
setting_get.onerror = function () {
console.log("[UNREAD] An error occure, the settings remain unchanged");
}
}
如何在主屏幕应用程序图标上绘制未读通知
现在我们已经准备好了未读消息数,我们需要在主屏幕上正确显示它们。如上所述,附加组件 JavaScript 代码(main.js
)的同一副本将注入到系统应用程序和主屏幕应用程序中。因此,我们现在必须检查我们是否是在主屏幕应用程序中,就像我们对系统应用程序所做的那样。
以下 JavaScript 代码从设置数据库中读取未读计数,并尝试将这些计数绘制到应用程序图标上。未读计数图标实际上是一个
<div/>
节点注入到应用程序图标 DOM 节点下,具有红色背景和 100% 的边框半径(使其成为圆形)。应用程序图标 DOM 节点很容易找到,因为它有一个
data-identifier
属性,该属性几乎总是应用程序清单 URL。(唯一的例外是拨号器应用程序,它有一个奇怪的 -dialer
后缀,但为了架构简单起见,我们将它视为特殊情况并对其进行硬编码。)
function drawUnreadIcon(appUrl, number){
var app_selector = '.icon[data-identifier="' + appUrl + '"]'; //for locating the app icon
var unreadIconElement = document.getElementById(app_selector+"-unread");
if (number === 0){
if (unreadIconElement){
unreadIconElement.parentNode.removeChild(unreadIconElement);
}
return
}
else{
if (number > 99) { number = "N" }
if (unreadIconElement){
unreadIconElement.textContent = number;
}
else {
unreadIconElement = document.createElement('div');
unreadIconElement.id = app_selector + "-unread";
unreadIconElement.style.backgroundColor = "red";
unreadIconElement.style.borderRadius = "100%";
//More unreadIconElement.style.foo = "bar" lines are omitted
unreadIconElement.appendChild(document.createTextNode(number));
document.querySelector(app_selector).appendChild(unreadIconElement);
}
}
}
请注意,我们使用 element.style.foo="bar"
语法来分配 CSS,而不是注入单独的 CSS 文件。最初,这是对附加组件框架早期版本的解决方法,该框架在注入 CSS 文件时效果不佳。(您可以在 Bug 1179536 中查看此问题的状态。)现在,随着 CSS 支持变得可用,没有充分的理由改变我们的方法。此附加组件非常简单,因此我们无需将代码拆分为太多文件。但是,如果您想构建更复杂的附加组件,您可能会考虑将 CSS 拆分为不同的文件以提高清晰度。
如何实时更新
我们希望我们的未读计数能够实时更新,但我们不想轮询设置数据库,因为它无法提供及时的信息,并且会消耗电池电量。相反,我们可以订阅设置的更新事件,因此只要设置数据库中的数据发生更改,我们就可以立即触发重新绘制。
window.navigator.mozSettings.addObserver('unreads', function(evt){
var settings = evt.settingValue;
for (appUrl in setting.result.unreads){
drawUnreadIcon(appUrl, setting.result.unreads[appUrl]);
}
})
关于附加组件的应用程序间通信的说明
由于此附加组件是在最近的一次黑客马拉松中构建的,因此我们选择了一种快速且 hacky 的方法来构建使用系统设置存储的应用程序间通信通道。目前似乎还没有针对附加组件的应用程序间通信的公认模式。但您可能需要查看以下选项(这些选项目前仍处于实验阶段,无法保证有效)
调试和测试
尽管我们可以在 WebIDE 中安装扩展,但 JavaScript 控制台和调试器尚未准备好用于调试扩展。目前调试扩展的唯一方法是使用 `console.log()` 将调试消息打印到 `adb logcat` 输出。您可能希望在日志输出前面添加一些前缀,以便您可以轻松地过滤日志输出。此外,您会发现自己经常检查核心应用程序的工作方式(如果您想更改系统行为),而 WebIDE 在这些情况下将非常方便。您可以将您的真实手机连接到 WebIDE,然后使用“运行时信息”>“请求更高权限”来访问系统和内置应用程序。(有关更多信息,请参阅 MDN)。
手机重启后,您将能够在运行的应用程序列表中看到“主进程”选项和其他核心应用程序。
您可以使用 DOM 检查器检查您要调整的 DOM 元素。
并在编写应用程序代码之前,使用 JavaScript 控制台尝试一些代码片段。
如何发布
一旦您创建了一个让您感到自豪的扩展,您一定想与全世界分享。Firefox 市场为您提供了将其发布到全世界的选项。
您需要将您的文件(包括 `manifest.json`)压缩成一个 zip 文件。然后将其上传到新发布的 市场扩展提交页面。
由于扩展非常强大,您的扩展将由 Mozilla 的专家审阅者进行审核,以确保用户安全和隐私不会受到损害。您可以在 此处找到审核标准,请务必在提交扩展之前查看。
总结
我们已经带您逐步了解了构建简单 Firefox OS 扩展的过程。我们向您展示了如何拦截系统通知、读取和写入系统设置,以及在主屏幕 UI 上绘制自定义 UI 元素。我们还向您展示了如何使用 WebIDE 安装和调试扩展。我个人认为,Firefox OS 扩展将成为 Firefox OS 平台的改变者。扩展赋予用户完全控制其移动体验的能力!本文还展示了使用标准 Web 技术构建扩展是多么容易。我们期待着看到人们可以构建多少种巧妙的扩展!
关于 Shing Lyu
我现在是 Firefox OS 的 QA 工程师。我做测试自动化和构建测试工具。我还参与 Firefox OS 应用程序、扩展和各种其他开源项目。
4 条评论