Manana 应用的构建过程

我们认为 Firefox OS 是一个绝佳的机会,也是一个挑战,它可以让我们交付一款真正体现开放网络价值和最佳标准的产品。我们很兴奋能够交付一款应用程序,即使在低端设备上也能提供流畅的用户体验。由于许多用户的数据计划非常有限,因此我们决定开发一款合适的阅读应用程序,提供离线享受他们最喜欢的文章的功能,并且没有不必要的干扰,这将是一个关键产品。因此,我们决定构建 Manana。

以下是我们的构建过程。

在技术方面,我们决定使用最基础的框架,辅以 ZeptoJS 来处理 DOM 操作。我们还集成了 Web 活动,以连接到浏览器中的共享选项,并通过电子邮件共享选定的链接。我们还使用 Readability API 来获得链接的格式良好的版本。该应用程序很简单,这也是我们想要的效果。但我们有一个很大的优势:它已被国际化,支持英语、西班牙语、葡萄牙语、波兰语、德语、意大利语。匈牙利语和希腊语即将推出。

你可以从 Firefox 应用商店安装 Manana

构建布局和管理视图之间的过渡

为了提供品牌化的 UI/UX,我们使用了一个内部构建的小型框架。通过一些巧妙的 CSS 属性和几行 JavaScript 代码(用于处理视图的可见性),整个设置非常容易。基本上,我们拥有每个视图,就像一叠卡片一样排列。每次我们需要其中一个视图时,我们会将其拉出来,而旧的视图则重新放入卡片堆中(变得隐藏)。动画结束后,我们捕获 animationend 事件,因此之后我们删除带来动画属性的类,同时添加 current 类,这使得卡片保持在原位并可见。旧的视图会自动隐藏,因为这是它的默认状态。

下面你将看到一个小例子,说明它如何使用 moveRight 动画工作。

Showing the way Views move

优化 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 操作需要使用回调。下面是一个关于我们如何处理添加链接操作的例子

  1. 调用 Link.Add 函数,传递一个链接对象和一个成功回调
  2. Link.Add 通过调用 getDB 来获取数据库连接
    • 如果数据库不存在,则执行 onupgradeneeded 回调
      • 创建数据库
      • 创建集合
      • 创建索引
    • getDB 检查连接是否已打开,否则打开一个新的连接
  3. Link.Add 使用默认值填充链接的缺失属性
  4. 我们调用对象存储的 put 方法将新数据保存/更新到集合中
    • 如果我们要保存的链接对象已经包含一个主键属性,则 IndexedDB 会执行更新
    • 否则,执行插入
  5. 如果 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

全栈开发人员。开放标准粉丝。

Alberto Granzotto 的更多文章…

关于 Andrea Di Persio

后端开发人员。他喜欢使用 vim 和嘈杂的机械键盘用 golang 和 python 编写代码。

Andrea Di Persio 的更多文章…

关于 Riccardo Buzzotta

他是艺术和 HTML+CSS 爱好者。也是数字/模拟艺术家/设计师爱好者。

Riccardo Buzzotta 的更多文章…

关于 Mateusz Fafinski

文案撰稿人和内容创作者。编写内容并撰写相关内容。负责内容策展和本地化。

Mateusz Fafinski 的更多文章…

关于 Robert Nyman [荣誉编辑]

技术布道者和 Mozilla Hacks 编辑。发表演讲和博客文章,主题涵盖 HTML5、JavaScript 和开放网络。Robert 坚定地相信 HTML5 和开放网络,自 1999 年以来一直在从事网络前端开发工作,地点包括瑞典和纽约市。他还经常在 http://robertnyman.com 上写博客,并且喜欢旅行和结识新朋友。

Robert Nyman [荣誉编辑] 的更多文章…


8 条评论

  1. Abhishek Shukla

    对 manana 的精彩概述!

    感谢分享…

    2013 年 12 月 3 日 下午 03:31

    1. Mateusz Fafinski

      谢谢!我们希望您也喜欢这个应用程序!

      2013 年 12 月 3 日 上午 09:48

  2. Andre Jaenisch

    您好,
    我不同意。我在 SimpleRSS 中也遇到了 5 MB 的限制(请参阅应用商店)。因此,当我尝试刷新已订阅的提要时,我的应用程序会中止。

    所以,也许您可以实现一个警告,例如在 4 MB 限制时发出警告。

    此致

    Andre

    2013 年 12 月 5 日 上午 01:42

    1. Andrea Di Persio

      说实话,我对 Firefox OS 上的索引数据库有点困惑。在您发表评论后,我做了一些研究,似乎虽然 API 规范允许应用程序询问用户是否同意超过配额,但在 Firefox OS 上,此行为被阻止,因此您只能使用这个 5 MB 的限制。

      2013 年 12 月 6 日 上午 05:21

      1. Andre Jaenisch

        嗯,很糟糕。

        但是,您可以提供您找到的结果的链接吗?
        这样可以省去我重新搜索的时间,我也可以将它们推荐给开发人员 :-)

        2013 年 12 月 9 日 下午 13:36

        1. Andrea Di Persio

          官方文档 https://mdn.org.cn/en/docs/IndexedDB#Storage_limits
          建议应该有一个提示,询问用户是否同意超过配额限制。
          但是在这里:https://bugzilla.mozilla.org/show_bug.cgi?id=827740 中指出,此提示被抑制了。
          在 Stack Overflow 上,一些用户报告说他们注意到模拟器和真实设备之间的限制不同。
          http://stackoverflow.com/questions/17709960/large-amount-of-data-into-indexeddb-in-firefox-os/17818571#17818571

          2013 年 12 月 16 日 上午 04:47

  3. JulienW

    经过一些研究,我们发现 l10n.js 库中有一个 try { … } catch 块。基本上,“在使用 app:// 协议的 Firefox OS 中,尝试对不存在的 URL 进行 XHR 会引发异常。”

    此问题已在 Firefox OS 1.1 中修复:https://bugzilla.mozilla.org/show_bug.cgi?id=834672

    2013 年 12 月 5 日 上午 04:35

    1. Alberto Granzotto

      很高兴知道这一点,很高兴这个问题已修复。现在轮到 ZTE 发布 FxOS 1.1 的升级了 :)

      2013 年 12 月 5 日 上午 10:25

本文的评论已关闭。