我们认为 Firefox OS 是一个绝佳的机会,也是一个挑战,它可以让我们交付一款真正体现开放网络价值和最佳标准的产品。我们很兴奋能够交付一款应用程序,即使在低端设备上也能提供流畅的用户体验。由于许多用户的数据计划非常有限,因此我们决定开发一款合适的阅读应用程序,提供离线享受他们最喜欢的文章的功能,并且没有不必要的干扰,这将是一个关键产品。因此,我们决定构建 Manana。
以下是我们的构建过程。
在技术方面,我们决定使用最基础的框架,辅以 ZeptoJS 来处理 DOM 操作。我们还集成了 Web 活动,以连接到浏览器中的共享选项,并通过电子邮件共享选定的链接。我们还使用 Readability API 来获得链接的格式良好的版本。该应用程序很简单,这也是我们想要的效果。但我们有一个很大的优势:它已被国际化,支持英语、西班牙语、葡萄牙语、波兰语、德语、意大利语。匈牙利语和希腊语即将推出。
你可以从 Firefox 应用商店安装 Manana。
构建布局和管理视图之间的过渡
为了提供品牌化的 UI/UX,我们使用了一个内部构建的小型框架。通过一些巧妙的 CSS 属性和几行 JavaScript 代码(用于处理视图的可见性),整个设置非常容易。基本上,我们拥有每个视图,就像一叠卡片一样排列。每次我们需要其中一个视图时,我们会将其拉出来,而旧的视图则重新放入卡片堆中(变得隐藏)。动画结束后,我们捕获 animationend
事件,因此之后我们删除带来动画属性的类,同时添加 current
类,这使得卡片保持在原位并可见。旧的视图会自动隐藏,因为这是它的默认状态。
下面你将看到一个小例子,说明它如何使用 moveRight
动画工作。
优化 CSS3 过渡
每部智能手机至少都配备了一个小型 GPU,这样 CPU 就不用处理所有图形计算。利用硬件加速是目前很流行的做法,我们也是如此。一些 CSS3 属性可以由 GPU 自动处理,但其他属性则不行,通常是与 3D 相关的属性。这是因为该元素(与动画属性绑定)被提升到一个新层,不会受到其他属性(例如 left
)强加的重绘的影响。但这在未来可能会改变。
tl;dr 在可能的情况下,避免昂贵的 CPU 工作,尤其是在大型或复杂的元素上。以下是我们如何实现这些属性的小例子。
/* moveLeftIn directive */
.moveLeftIn {
animation: 0.5s moveLeftIn cubic-bezier(…) forwards; /* 1 */
backface-visibility: hidden; /* 2 */
transform: translateY(0) translateX(99%); /* 3 */
/* 1. `forwards` let the animation stop at the last keyframe */
/* 2. avoid some possible glitches caused by 3D rendering */
/* 3. take advantage of the GPU with 3D-related properties */
}
/* moveLeftIn animation */
@keyframes moveLeftIn {
0% { transform: translateY(0) translateX(99%); z-index: 999; }
100% { transform: translateY(0) translateX(0); z-index: 999; }
}
连接到 Firefox OS 特定的 Web 活动
我们最喜欢的 WebAPIs 之一是 Web 活动。应用程序会发出一个活动(例如,保存书签),然后会提示用户选择其他处理该活动的应用程序。目前,可以通过应用程序清单(声明注册)将应用程序注册为活动处理程序,但在未来,这可以在应用程序本身中完成(动态注册)。
将链接从浏览器保存到 Manana 是 Web 活动(save-bookmark)的一个例子。注册活动很简单
manifest.webapp
"activities": {
"save-bookmark": {
"disposition": "inline",
"index.html#/savebookmark",
"returnValue": true
}
},
returnValue 至关重要,否则处理程序应用程序(我们的情况下是 Manana)将永远不会返回到调用者(浏览器)。在应用程序中的处理也很简单
navigator.mozSetMessageHandler("activity", function(activity) {
... check if activity is savebookmark
var data = activity.source.data;
... do stuff with activity data
activity.postResult("saved");
});
手机中包含的应用程序是 Gaia 的一部分,Gaia 是 Firefox OS 的 UI,你实际上可以浏览其源代码。它们提供了很多关于如何使用 WebAPIs 的示例。如果你对 save-bookmark 活动感兴趣,请查看 主屏幕应用程序源代码。
Manana 用户还可以直接从应用程序通过电子邮件分享他们的资源。为了实现这一点,我们实现了 email
Web 活动。预先填充电子邮件主题和正文非常简单,只需遵循 mailto
URI 方案即可。
DB.Get(id, function (rs) {
var subject = rs.title,
body = rs.url,
shareActivity = new MozActivity({
name: "new",
data: {
type: "mail",
url : "mailto:?body=" + encodeURIComponent(body) +
"&subject=" + encodeURIComponent(subject)
}
});
});
使用 IndexedDB 检索和存储数据
IndexedDB 是一个客户端 NoSQL 数据库。它有一些很棒的功能,可以简化应用程序更新工作,并且易于集成到应用程序中,即使你使用的是像 AngularJS 这样的框架也是如此。设置模型非常简单,在网上和 Gaia 应用程序中都有广泛的文档记录。
你没有配额限制,尽管 Firefox OS 模拟器在处理超过 5 MB 的数据集时似乎有限制。每个数据库都绑定到一个应用程序和一个版本。一次只能运行一个版本,这使得保持兼容性变得更容易,尤其是在你依赖一些外部 API 的情况下。
由于它是异步的,处理 IndexedDB 操作需要使用回调。下面是一个关于我们如何处理添加链接操作的例子
- 调用 Link.Add 函数,传递一个链接对象和一个成功回调
- Link.Add 通过调用 getDB 来获取数据库连接
- 如果数据库不存在,则执行 onupgradeneeded 回调
- 创建数据库
- 创建集合
- 创建索引
- getDB 检查连接是否已打开,否则打开一个新的连接
- 如果数据库不存在,则执行 onupgradeneeded 回调
- Link.Add 使用默认值填充链接的缺失属性
- 我们调用对象存储的 put 方法将新数据保存/更新到集合中
- 如果我们要保存的链接对象已经包含一个主键属性,则 IndexedDB 会执行更新
- 否则,执行插入
- 如果 put 方法成功,它会调用成功回调,并在 event.target.result 中返回主键
以下是我们在模型中定义的数据库操作示例:(为了简洁起见,我们省略了错误检查代码)
Add: function(data, onsuccess) {
getDB(function(db) {
var transaction = db.transaction(["links"], "readwrite"),
objectStore = transaction.objectStore("links"),
request = objectStore.put(data);
request.onsucces = function(e) {
onsuccess(e.target.result) // e.target.result is the id of the new Link
}
});
},
以下是如何从应用程序的其他部分调用它
var newLink = {url: "http://zombo.com",
title: "Welcome to zombo com"};
Link.Add(newLink, function(linkId) {
window.location.hash = "#/read/:" + linkId;
});
通过 Markdown 支持增强本地化
本地化是应用程序的关键功能。Manana 目前支持 6 种语言(很快将增加到 8 种)。没有官方推荐,但在经过一些研究之后,我们决定使用与 Gaia 使用的相同的 技术。这是使用 Firefox OS 的一大“优势”:整个源代码都在那里,你可以通过阅读它获得灵感。
L10n 用于两种不同的情况:翻译 DOM 元素的 text
内容和翻译内部帮助页面。为了使本地化过程尽可能简单,我们避免在字符串和内部帮助页面中放置 HTML 标签;我们改用 Markdown。根据设计,我们避免在 Javascript 代码中出现字符串,所有消息和警报都包含在 HTML 中。
为了支持 Markdown,我们在 l10n.js 库和 marked.js 之间添加了一些粘合剂。
要加载帮助资源,Manana 首先在当前安装中查找名为 <resourceName>.<locale>.md
的文件。如果查找失败,则回退到资源的英文版本。成功检索到文件后,将内容应用于 marked
函数以生成 HTML,然后将其添加到 DOM。
function resourceInit($oldView, $newView, resource) {
var fallback = "/locales/resources/" + resource + ".en.md",
localized = "/locales/resources/" + resource + "." + getLanguage() + ".md",
renderMd = function (txt) {
$(".js-subpage", $newView).html(marked(txt));
},
renderError = function (txt) {
$(".js-subpage", $newView).text("ouch, cannot find: " + resource);
};
getFile(localized,
renderMd,
function () { getFile(fallback, renderMd, renderError); });
}
关于使用 XMLHttpRequest 检索本地文件的说明
从你的本地应用程序异步检索文件与执行 XMLHttpRequest
一样简单,但有一个情况让我们花了一些时间才弄清楚。如果你要检索的本地文件丢失,该请求不会触发 success
回调(当然),也不会触发 error
回调。它只会永远挂在那里。
经过一些研究,我们注意到 l10n.js
库中有一个 try { ... } catch
块。基本上,“在 Firefox OS 中使用 app://
协议,尝试 XHR 一个不存在的 URL 会引发异常。”
function getFile (url, success, error) {
var xhr = new XMLHttpRequest();
xhr.open("get", url, true);
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0)
success(xhr.responseText);
else
error && error();
}
};
xhr.onerror = error;
xhr.ontimeout = error;
// Here it is!
try {
xhr.send(null);
} catch (e) {
error && error();
}
}
使 UI 与数据库同步
HTML 应用程序最具挑战性的方面之一是数据源(数据库、文件、API 等)和 UI 之间状态的同步。像 Backbone 和 Angular 这样的框架可以简化你的工作,但它们并非易于驾驭的庞然大物。使用基础框架很有趣,主要是因为 JavaScript 在决定如何执行操作时提供了很大的灵活性。
我们真的很喜欢通过消息传递进行的 OO 编程,因此我们想出了一个基于消息的体系结构。body
元素充当我们的全局调度器,而视图根元素是本地调度器。全局事件在应用程序启动时注册,并在应用程序生存期内一直存在。本地事件在进入视图时注册,并在离开该视图之前一直存在。
全局事件的一个例子是我们想对模型执行的 CRUD 操作。
UI 上更改模型状态的每个操作都绑定到一个全局事件,该事件在 document.body
上注册。在进入新视图时,我们调用一个构造函数,该构造函数会执行一些初始化代码。
var events = {
// global events are never unbinded
global: {
"save-link": function() { ... },
"remove-link": function() { ... },
...
"update-link-collecetion": function { ... },
...
},
// view specific events are unbinded everytime we leave that view
home: {
"update-link-collection-complete": function(e) {
var $caller = e.detail.$caller;
...
}
},
...
}
function homeInit($oldView, $newView, dispatch) {
// $oldView is the previous view
// $newView is the current view
// dispatch in an helper which wrap
// document.body.dispatchEvent(new CustomEvent(...))
dispatch("update-link-collection")
}
离开视图会调用该视图上的析构函数,并取消绑定所有本地事件。最终,我们可以打开应用程序源代码,而不用花费数小时试图弄清楚它的工作原理,因为没有框架相关的黑魔法。
与 git 集成
说到 Git,我们心都化了。我们爱 Git。真的。Git 和 Vim 是有史以来编写过的最好的软件。爱。
好了,让我们严肃点。
我们的应用程序由两个组件组成,应用程序本身和一个 API 服务器。API 服务器是 Readability API 的 Golang API 代理。因此,我们的应用程序目录布局如下
manana/ |- distrib.sh |- srv/ |- app/
distrib.sh 是一个简单的 bash 脚本,它执行三个简单的任务
- 安装一个 post-{commit, merge, rebase} 钩子,如果尚未安装。
- 更新 app/js/revision.js,写入当前的 git 修订版本哈希值。
- 创建一个包含应用程序的干净压缩文件,准备发布到应用商店或您的设备。
这使我们的生活更轻松。
结论。下一步是什么?
Manana 依赖 Readability API 来获取网页的精简版本。Readability 正在出色地完成这项工作,但我们想尝试其他解决方案,例如直接在设备中解析页面。这是一项艰巨的任务,但如果能在客户端完成将很酷,并且已经 完成了一些工作。
我们很高兴为 Firefox OS 开发。对我们来说,从技术角度来看,最重要的是我们开发的是一个网络应用程序,而不是一个 特定于操作系统的应用程序。我们可以使用过去几年积累的所有知识,所有我们想要使用的大型开源库,甚至可以使用我们每天使用的浏览器调试工具。
为了帮助新手构建 Firefox OS 应用程序,我们创建了 资源列表,这些资源我们发现必不可少。
关于 Alberto Granzotto
全栈开发人员。开放标准粉丝。
关于 Andrea Di Persio
后端开发人员。他喜欢使用 vim 和嘈杂的机械键盘用 golang 和 python 编写代码。
关于 Riccardo Buzzotta
他是艺术和 HTML+CSS 爱好者。也是数字/模拟艺术家/设计师爱好者。
关于 Mateusz Fafinski
文案撰稿人和内容创作者。编写内容并撰写相关内容。负责内容策展和本地化。
关于 Robert Nyman [荣誉编辑]
技术布道者和 Mozilla Hacks 编辑。发表演讲和博客文章,主题涵盖 HTML5、JavaScript 和开放网络。Robert 坚定地相信 HTML5 和开放网络,自 1999 年以来一直在从事网络前端开发工作,地点包括瑞典和纽约市。他还经常在 http://robertnyman.com 上写博客,并且喜欢旅行和结识新朋友。
8 条评论