Time Out Firefox OS 应用程序的制作

冒险的开始

我们告诉我们的客户,当然可以,我们会为他们做 Firefox OS 应用。当时我们对 FFOS 并不了解。但是,嘿,我们刚刚完成了他们原生 iOS 和 Android 应用的重构。网页应用程序一直都是我们的核心业务。那么有什么好担心的呢?

事实证明,比我们想象的要多。我们在途中遇到的部分挑战,我们自己战胜了。有时我们担心我们无法及时拯救公主(即在 MWC 2013 之前)。但每当我们迷失在细节的森林里时,来自 Mozilla 的勇敢骑士都会来拯救我们。最终,一切都好转,团队从此过上了幸福的生活。

但这是完整的故事

任务与挑战

就像他们的 iOS 和 Android 应用一样,Time Out 的新 Firefox OS 应用应该允许用户按类别、区域、距离或关键字搜索浏览其关于酒吧、餐馆、活动等内容的丰富信息,目标城市是巴塞罗那。我们需要以插图列表的形式展示结果,以及在地图上直观地展示结果,并有一个合适的详细信息视图,包括评分、访问详细信息、电话按钮和社交工具。

但最重要的是,除了原生应用的功能外,这个应用还应该能够在离线状态下完成所有这些操作。

哦,还需要在四周内有一个可呈现、可运行的原型。

代码的跨平台可重用性,如移动网站或其他移动平台上 HTML5 应用的底层,显然是优先级 2,但仍然需要牢记。

公主显然处于危险之中。所以我们抓住了所有可能提供帮助的人,把他们锁在一个房间里,以便解决一些基本问题。很快发现,主要的架构挑战在于

  • 我们需要在手机上存储很多东西,包括应用本身、巴塞罗那的完整街道地图,以及 Time Out 关于城里每个场馆的信息(文本、图像、位置和元数据),
  • 至少其中一部分需要从应用内部加载;第一次加载,之后可以同步,
  • 应用需要在这些可能很长的下载过程中保持交互性,因此需要异步,
  • 每当浏览器位置发生变化时,下载就会被中断。

实际上,所有不同的功能必须在一个 HTML 文档中实现。

一个文档加哈希标签

对于在一个页面完成所有操作的场景中,动态呈现、更改和移动内容,仅仅依靠 JavaScript 似乎不是一个明智的选择。我们被告知 Firefox OS 将在各种设备上发布,包括价格非常低廉的设备,因此很明显,如果要实现流畅的全屏内容过渡,就不能通过 JS 循环来实现。

另一方面,不需要基于 JS 的演示机制。由于 Firefox OS 没有遗留的半死不活的版本,我们可以(终于!)完全依靠 HTML5 和 CSS3,而且没有回退。即使是在 FFOS 之外,移动环境中快速的更新周期似乎也没有阻碍我们将来在更多平台上采用纯 CSS3 方法。

既然如此,哪里可以找到最好的实践示例呢?当然是 Mozilla Hacks!经过一番挖掘,Thomas 找到了 Hacking Firefox OS,其中 Luca Greco 描述了使用附加到 URL 的片段标识符(也称为哈希标签)来通过 CSS 切换和转换内容,我们欣然采用了这种方法。

另一个有价值的想法来源是 Mozilla 网站上列出的 GAIA 构建块,该网站已被更加实用的 Building Firefox OS 网站取代。

实际上,我们最终开始考虑使用屏幕。每个屏幕实际上都是一个 <div>,它的可见性和过渡由 :target CSS 选择器控制,这些选择器依赖于浏览器位置的哈希标签。幸运的是,还有 <a href="https://mdn.org.cn/en-US/docs/Web/API/window.onhashchange">onHashChange</a> 事件,我们可以在 JavaScript 中监听这个事件,以处理这些屏幕更改的应用级别方面。

因此,我们的主要 HTML 和 CSS 结构如下所示

以及菜单

我们以类似的方式对抽屉菜单进行建模,只是它位于与包含所有屏幕的 <section> 容器相同的级别上的 <nav> 元素中。它的激活和停用是通过捕获菜单图标的点击来完成的,然后从 JS 中主动更改屏幕容器的 data-state 属性,从而触发相应的 CSS3 渐入/渐出过渡(屏幕容器的过渡,显示出下面的菜单)。

这充当了我们对低端设备上基于 CSS3 的 UI 性能的“Hello, World!”测试,以及将演示级别的 CSS3 自动化与应用级别的显式状态处理相结合的测试用例。我们对两者都得到了肯定的答案。

UI

当我们围绕这些概念构建了一个虚拟模型时,Time Out 的第一个设计模型也出来了,因此我们可以开始实现前端并考虑如何将其连接到数据源。

对于演示,我们尽力将 HTML 和 CSS 限制在绝对最小限度。Mozilla 的 GAIA 示例又一次成为有价值的想法来源。

同样,仅针对 Firefox OS 的目标,使我们能够摆脱我们仍然存在的桌面级向后兼容性困境。没有人会问我们它在 IE8 中显示效果好吗?或者更糟糕的问题。我们终于可以使用真正的 <section><nav><header><menu> 标签,而不是一大堆不同的 <div> 类。真是太解脱了!

我们从 Time Out 获得的清晰、矩形、扁平化和极简的设计也为保持 UI HTML 的简洁和干净做出了贡献。在我们完成 15 个屏幕的 UI 创建和样式设置后,我们的 HTML 只有大约 250 行。我们后来在扩展功能的同时将其改进到了 150 行,但那是另一个故事。

说到样式,并非所有在桌面 Firefox 上看起来不错的效果,即使是在其 响应式设计视图 中,在实际的移动设备上也表现得一样好。我们克服的一些问题

比例:应用在参考设备(Mozilla 发送给我们的用于测试的 TurkCell 品牌 ZTE 设备)和我们全新的 Nexus 4s 上的显示效果大不相同。

经过大量的试验、撕扯头发和四处查看其他人如何解决跨分辨率优雅的比例缩放问题以保持一致的外观和感觉后,我们偶然发现了这个神奇的咒语

<meta name="viewport" content="user-scalable=no, initial-scale=1,
maximum-scale=1, width=device-width" />

正如 Opera 文章中所述,它的作用是告诉浏览器“不需要缩放,非常感谢。只需将视窗的宽度设置为设备屏幕的宽度”。它还能防止在地图缩放时意外缩放。MDN 上有关于该主题的更多信息.

然后有一些东西在放大到高分辨率时会不可避免地变得像素化,例如基于 API 的场馆图像。我们对此无能为力。但至少我们可以通过将应用 chrome 中的图标和徽标转换为 SVG,使它们在任何分辨率下都显得漂亮。

移动设备上的另一个问题是,用户必须触摸内容才能滚动内容,因此我们希望阻止随之而来的自动突出显示

li, a, span, button, div
{
    outline:none;
    -moz-tap-highlight-color: transparent;
    -moz-user-select: none;
    -moz-user-focus:ignore
}

我们后来被警告说,抑制默认突出显示可能会造成可访问性问题,因此您可能需要仔细考虑这一点。

连接到实时数据源

现在我们有了应用的演示基础结构和 UI HTML/CSS。它看起来很不错,使用了虚拟数据,但它仍然是死的。

让它起死回生遇到了麻烦,因为 Time Out 正处于一个大型项目之中,该项目旨在用现代的 Graffiti 服务替换其遗留的 API,因此他们没有太多带宽来满足我们项目的特定需求。新方案仍然是原型,并且在不断快速发展,所以我们无法针对它进行构建。

遗留的架构已经包含一个代理,将原始 API 包装成更适合其 iOS 和 Android 应用使用的形式,但经过仔细检查后,我们发现最好在 PHP 中对它进行重新包装,以实现几个目的

  • 添加 CORS 支持以避免 XSS 问题,因为 API 和应用位于 timeout.com 的不同子域中,
  • 将 API 输出剥离到 FFOS 应用真正需要的部分,我们可以看到这将显着减少带宽并提高速度,
  • 为离线使用 API 基于数据奠定基础,我们已经知道以后会需要这样做。

作为服务器端 CORS 支持的替代方案,也可以考虑使用 SystemXHR API。然而,它是一个强大而可能很危险的工具。我们还希望避免对 FFOS 专有 API 的任何不必要的依赖。

因此,虽然这种方法并不完全是面向未来的,但它帮助我们快速获得了结果,因为应用调用的端点完全是我们自己选择的,因此我们可以根据需要调整它们,而不会在沟通上浪费时间。

填充内容元素

对于所有动态和 API 驱动的元素,我们使用相同的 подход,使它在应用中可见

  • 有一个简单、极简、空、隐藏、单例的 HTML 模板,
  • 克隆该模板(N 倍,用于重复的元素),
  • 用 API 基于的内容为克隆(复数)添加 ID 并填充。
  • 对于非常简单的元素,如 <li>,请保存克隆,并在填充时动态创建 HTML。

例如,让我们考虑查找场地的过滤器。菜系 是适合餐厅的过滤器,但肯定不适合博物馆。过滤器值也是如此。巴塞罗那有素食餐厅,但肯定没有素食酒吧。因此,在选择场地类型后,需要向 API 询问过滤器名称和可能值的列表。

在 UI 中,酒吧和酒吧的可折叠类别过滤器如下所示

一个过滤器的模板是唯一一个的直接子级

<div id="templateContainer">

它充当我们所有在运行时克隆和填充的内容的中央模板存储库,并且其唯一有趣的属性是不可见。在它里面,搜索过滤器的模板是

<div id="filterBoxTemplate">
  <span></span>
  <ul></ul>
</div>

因此,对于我们为任何给定类别获取的每个过滤器,我们所要做的就是克隆、标记,然后填充此模板

$('#filterBoxTemplate').clone().attr('id', filterItem.id).appendTo(
'#categoryResultScreen .filter-container');
...
$("#" + filterItem.id).children('.filter-button').html(
filterItem.name);

正如您一定猜到的,我们随后必须再次调用 API 来了解每个过滤器的可能值,这些值随后被渲染成 <li> 元素,位于过滤器内部的 <ul> 中。

$("#" + filterId).children('.filter_options').html(
'<li><span>Loading ...</span></li>');

apiClient.call(filterItem.api_method, function (filterOptions)
{
  ...
  $.each(filterOptions, function(key, option)
  {
    var entry = $('<li filterId="' + option.id + '"><span>'
      + option.name + '</span></li>');

    if (selectedOptionId && selectedOptionId == filterOptionId)
    {
      entry.addClass('filter-selected');
    }

    $("#" + filterId).children('.filter_options').append(entry);
  });
...
});

基于 DOM 的缓存

为了节省带宽并提高在线使用的响应速度,我们进一步采取了这种简单的方法,并有意识地在 DOM 中存储了比当前显示所需的更多应用程序级别信息,如果这些信息可能在下一步中需要。这样,我们就可以轻松快速地本地访问它,而无需再次调用 API 并等待 API 的响应。

我们这样做的技术方法是一个有趣的技巧。让我们看一下从搜索结果列表到场地详细信息视图的转换,以说明这一点

对于上面的过滤器,detailView 的屏幕类有一个 init() 方法,该方法根据 API 输入填充 DOM 结构,这些输入在应用程序级别封装。现在的技巧是,在渲染搜索结果列表时,为其每一行注册匿名点击处理程序,这些处理程序(JavaScript 传递魔法)包含渲染行本身的场地对象的副本,而不是对它们的引用。

renderItems: function (itemArray)
{
  ...

  $.each(itemArray, function(key, itemData)
  {
    var item = screen.dom.resultRowTemplate.clone().attr('id',
      itemData.uid).addClass('venueinfo').click(function()
    {
      $('#mapScreen').hide();
      screen.showDetails(itemData);
    });

    $('.result-name', item).text(itemData.name);
    $('.result-type-label', item).text(itemData.section);
    $('.result-type', item).text(itemData.subSection);

    ...

    listContainer.append(item);
  });
},

...

showDetails: function (venue)
{
  require(['screen/detailView'], function (detailView)
  {
    detailView.init(venue);
  });
},

实际上,渲染每个场地详细信息视图的数据副本存储在 DOM 中。但既不在隐藏元素中,也不在节点对象的自定义属性中,而是在结果列表行的每个匿名按值传递的点击事件处理程序中,这样做的额外好处是,它们不需要显式读取,而是可以主动将自己馈送到场地详细信息屏幕,只要一行收到触摸事件。

和虚拟馈送

在 MWC 2013 之前完成应用程序几乎是一场争分夺秒的比赛,对我们和 Time Out 的 API 人员来说都是如此,他们有完全不同且同样(如果不是更多的话)有运动感的任务要做。因此,他们为我们构建的(遗留)API 添加功能的时间非常有限。对于一个数据馈送,这意味着我们必须诉诸于将静态 JSON 文件包含到应用程序的清单和分发中;然后使用相对的、自我引用的 URL 作为伪 API 端点。应用程序主屏幕上显示的顶级场地列表就是以这种方式驱动的。

虽然并不完全完美,但这比将静态内容扔到 HTML 中要好得多!此外,它使显示代码已适合切换到最终在以后实现的动态数据源,并且与我们的离线数据缓存策略兼容。

由于顶级场地的实时数据缺失一直延伸到他们的预告图像,我们使后者在物理上成为 JSON 虚拟馈送的一部分。在 Base64 :) 但是即使是低端参考设备也能很好地处理大量 ASCII 垃圾。

状态保存

我们有高达 5M 的本地存储空间可以滥用,并且已经有了不同的计划(以及更高的需求)用于存储地图和应用程序数据以供离线使用。那么如何利用这个轻松访问的存储位置呢?我们认为,至少可以在这里保留当前的应用程序状态,这样您返回应用程序时,它将与您离开时完全一样。

地图

城市指南是一款不仅具有地理感知能力,而且还以地理为中心的应用程序的典范。地图适合在线和离线使用中的快速渲染和交互,这自然是一个首要的要求。

在查看了可用的内容后,我们决定使用 Leaflet,这是一个免费的、易于集成的、移动友好的 JavaScript 库。事实证明,它在行为和地图源方面都非常灵活。

凭借对捏合、平移和优雅触摸处理的支持,以及简洁易用的 API,Leaflet 使我们能够以适度的努力和很少的痛苦构建一个易于使用、外观不错的 地图。

对于不同的项目,我们后来使用按需云计算能力将欧洲大部分地区的 OSM 矢量数据渲染成云存储中数 TB 的 PNG 瓦片。如果您有充分的理由不依赖第三方托管应用程序,我们建议您采用这种方法,只要您不要在家尝试;移动瓦片可能比生成瓦片更慢、更昂贵。

但是由于在该应用程序首次发布之前时间紧迫,我们只是合法且谨慎地(!) 从 MapQuest.com 提取了现成的 OSM 瓦片。

为离线使用打包瓦片对于巴塞罗那来说非常容易,因为大约 1000 个地图瓦片足以覆盖整个城市区域,直到街道级别(缩放级别 16)。因此,我们可以将每个瓦片作为单行添加到 manifest.appache 文件中。由此产生的、完全自动的、基于浏览器的首次使用下载仅为 10M。

这使我们留下了许多类似于

/mobile/maps/barcelona/15/16575/12234.png
/mobile/maps/barcelona/15/16575/12235.png
...

的清单行,并希望有一个与 DNS 区域文件类似的 $GENERATE 子句。

将所有离线依赖项的位置扔到一个文件中,并期望它们因此可用,这看起来可能很方便,但这种方法有很大的弊端。Jake Archibald 的文章 Application Cache is a Douchebag 对其进行了总结,并提供了一些帮助 在 Html5Rocks 上由 Eric Bidleman 提供.

我们当时发现,对当前下载状态的控制程度,以及在最初时间用户在我们的应用程序中花费的时间不足以完成下载的情况下恢复应用程序缓存加载的过程相当繁琐。

对于巴塞罗那,我们诉诸于在本地存储中将缓存状态标记为脏,并且仅在我们收到 window.applicationCache 对象的 updateready 事件后才清除该标志,但在之后推广到更多城市时,我们将地图完全移出应用程序缓存。

离线存储

迈向离线就绪的第一步显然是了解设备是联机还是脱机,这样我们才能在实时和本地数据源之间切换。

这听起来比实际操作起来容易。即使不考虑跨平台因素,联机状态属性 (window.navigator.onLine)、在 <body> 元素上为状态更改触发的事件(“联机”和“脱机”,同样在 <body> 上),以及应该具有联机/脱机状态以及带宽等的 navigator.connection 对象,实际上并没有真正可靠到足以信任。

所有这些方面的标准化仍在进行中,并且一些实现 被标记为实验性是有充分理由的 :)

我们最终编写了一个 NetworkStateService 类,它使用上述所有内容作为提示,但最终,而且非常务实地,它会通过对已知实时 URL 的定期 HEAD 请求来确信自己,没有事件丢失,并且状态是正确的。

确定了这一点后,我们仍然需要使应用程序在离线模式下工作。在存储机会方面,我们正在考虑

存储 容量 更新 访问 典型用途
应用程序/应用程序缓存,即应用程序的 webapp.manifest 中 appcache_path 值指向的文件中列出的所有内容,这些内容将在安装应用程序时下载到设备上。 <= 50M。在其他平台(例如 iOS/Safari)上,需要用户交互,从 10M+ 开始。Moziila 的建议是保持 <2M。 困难。需要用户交互/同意,并且只能对整个应用程序进行整体更新。 通过(相对)路径 HTML、JS、CSS、静态资产(例如 UI 图标)
LocalStorage 在 FFOS 等 UTF8 平台上为 5M,在 Chrome 等 UTF16 平台上为 2.5M。 详细信息在此处 随时从应用程序 通过名称 应用程序状态、用户输入或中等规模应用程序的全部数据的键值存储
设备存储(通常是 SD 卡) 仅受硬件限制 随时从应用程序(除非在连接到台式计算机时被安装为 UDB 驱动器) 通过路径,通过 设备存储 API 大事物
文件系统 API 糟糕的想法
数据库 在 FFOS 上无限制。其他平台上的里程数各不相同 随时从应用程序 快速且通过任意属性 数据库 :)

关于在何处存储离线操作的数据的某些方面很容易决定,而另一些方面则不是那么容易

  • 应用程序,即 HTML、JS、CSS 和 UI 图像将进入应用程序缓存
  • 状态将保存在本地存储中
  • 地图瓦片再次进入应用程序缓存。后来我们发现这是一个相当愚蠢的决定。巴塞罗那的缩放级别 16 为 10M,但后来的城市则不同。伦敦 > 200M,即使缩减到最大缩放级别 15 仍然达到 61M。因此,我们将它移到设备存储中,并在后来的版本中添加了一个主动管理的下载过程。
  • 场地信息,即 Time Out 在巴塞罗那显示的所有场地的名称、位置、图片、评论、详细信息、放映时间等。考虑到我们需要大量空间、高效的任意访问以及动态更新,这必须进入数据库。但是如何做呢?

不同移动 HTML5 平台上的现状充其量是令人困惑的,Firefox OS 已经支持 IndexedDB,但 Safari 和 Chrome(考虑到早期的版本,直到 Android 2.x)仍然依赖于大量类似但不同的 sqlite/WebSQL 变体。

因此,我们求助于帮助,并且如往常一样,当我们联系 Mozilla 团队时,我们收到了帮助。这次是关于 pouchDB 的一个指针,这是一个基于 JS 的数据库层,它同时隐藏了不同的本地数据库存储引擎,并提供了一个类似于 CouchDB 的接口,并且为远程 CouchDB 托管的主数据库添加了超级简单的按需同步功能。

去年,该应用还处于预发布阶段,但已经非常实用。它也存在一些缺点,例如需要为基于 WebSql 的平台添加 shim。这反过来又意味着我们无法依赖存储为 8 位干净,因此我们必须对二进制文件进行 base64 编码,尤其是场地图片。这不能完全归咎于 pouchDB,但它确实增加了文件大小。

数据采集

在选择数据库平台之后,我们接下来需要考虑如何从 Time Out 的 API 中采集所有场地数据到数据库中。我们有几个可用端点。最适合此任务的是无类别或其他限制的邻近搜索,因为我们认为它可以让我们方格状地采集特定城市区域。

然而,距离度量的问题在于它生成的是圆形而不是方形。因此,我们思路中的第一步会错过理论网格角上的场地

而将半径扩展到网格对角线的(一半),则会产生冗余命中,需要进行去重。

最终,我们简单地根据与城市中心位置的邻近性进行搜索,无限期地对结果进行分页,以便确保能够遇到每个场地,并且只遇到一次

从技术上讲,我们将采集器构建为 PHP,作为已有的 CORS 启用、结果缩减的 API 代理的扩展,用于实时操作。它将场地信息馈送到那里共同托管的主 CouchDB。

距离 MWC 2013 的时间越来越紧迫,我们没有在复杂的数据组织上花费太多时间,只是将场地信息以每个类别一张表、每个场地一行的方式推送到数据库中,并按位置索引。

这使我们能够支持基于类别的和基于区域/邻近性的(地图和列表)浏览。我们开发了如何实现离线关键词搜索的想法,但最终没有实现。因此,该应用在离线时会简单地移除搜索图标,并在恢复网络连接时重新添加。

总的来说,该应用现在

  • 支持开箱即用的实时操作,
  • 在启动时检查与远程主数据库的同步状态,
  • 在需要时请求进行大型(初始或更新)下载的权限,
  • 支持所有用例,但在离线时不支持关键词搜索。

该图总结了相关组件及其交互关系

代码组织与优化

为了开发该应用,我们维护了一个结构良好且扩展的源代码树,例如每个 JavaScript 类都驻留在一个单独的文件中。下面展示了源代码树的一部分

然而,这对于应用的部署来说并不理想,尤其是在作为托管的 Firefox OS 应用或移动网站进行部署时,下载文件越少、文件越小,速度就越快。

此时,Require.js 拯救了我们。

它提供了一种非常优雅的方式来智能地进行异步需求处理 (AMD),但更重要的是,它还附带了一个优化器,可以将 JS 和 CSS 源代码分别合并成一个文件

为了启用异步依赖管理,必须通过声明将模块及其需求告知 AMD API,本质上是一个返回要定义的类的构造函数的函数。

应用于我们应用的搜索结果屏幕,它看起来像这样

define
(
  // new class being definied
  'screensSearchResultScreen',

  // its dependencies
  ['screens/abstractResultScreen', 'app/applicationController'],

  // its anonymous constructor
  function (AbstractResultScreen, ApplicationController)
  {
    var SearchResultScreen = $.extend(true, {}, AbstractResultScreen,
    {
      // properties and methods
      dom:
      {
        resultRowTemplate: $('#searchResultRowTemplate'),
        list: $('#search-result-screen-inner-list'),
        ...
      }
      ...
    }
    ...

    return SearchResultScreen;
  }
);

在构建和部署过程中执行优化步骤,我们使用了 Rhino,Mozilla 的基于 Java 的 JavaScript 引擎

java -classpath ./lib/js.jar:./lib/compiler.jar
  org.mozilla.javascript.tools.shell.Main ./lib/r.js -o /tmp/timeout-webapp/
  $1_config.js

CSS 捆绑和缩小也得到支持,只需使用不同的配置进行另一次调用即可。

结果

一开始,四个星期的时间非常紧张,我们完全低估了将 HTML5 带到移动和离线环境,并将其包装成一个可用于市场发布的 Firefox OS 应用的复杂性。

Firefox OS 中的调试功能,尤其是在设备本身上,还处于早期阶段(与今天的 about:app-manager 相比)。因此,我们科隆办公室的灯光一直亮到很晚。

用清晰的功能和表现分离的方式构建应用,在距离 T0 一周时,我们收到了大部分前端的新样稿,这一点也证明了这是一个明智的选择 :)

但这很有趣,而且很令人兴奋,我们在过程中学到了很多,并最终在我们的工具箱中获得了许多非常有用的闪亮新工具。这些工具通常来自 Mozilla 超级有帮助的团队的建议。

说实话,我们最初对这个项目抱有不同的期望,我们想知道我们能离原生应用体验有多近。我们回来后,我们完全确信,并且渴望更多。

最终,我们赶上了截止日期,作为一个 fellow hacker,你可能可以想象我们松了一口气。这款应用最终甚至获得了 70 秒的知名度,当 Jay Sullivan 在 Mozilla 的 MWC 2013 媒体发布会上简短演示它 时,它被展示为 HTML5 和 Firefox OS 离线功能的典范(Time Out 部分在 7:50)。我们非常自豪!

如果你想玩一下,你可以在 市场上找到这款应用,或者直接在 网上尝试(但没有离线模式)。

从那时起,Time Out Firefox OS 应用一直在不断发展,我们团队也利用了这个机会继续开发和构建 FFOS 应用。在某种程度上,这部分可重用内容已经成为一个框架,但这将是另一个故事……

我们要感谢所有一路帮助我们的人,特别是来自 Time Out 的 Taylor Wescoatt、Sophie Lewis 和 Dave Cook,来自 Mozilla 的 Desigan Chinniah 和 Harald Kirschner,他们总是在我们需要帮助时出现,当然还有 Robert Nyman,他耐心地指导我们完成了这篇写作。

关于 Andreas Oesterhelt

在 InTradeSys 工作,该公司位于科隆,是一家专门从事敏捷、增量开发的开发公司,通常开发原型 Web 应用,但仍需扩展到大型网站(如 eBay 或 Groupon)的生产使用。他负责一些工作,包括咖啡供应、团队开发和软件架构方面的帮助。

更多 Andreas Oesterhelt 的文章…

关于 Thomas Biniasch

Thomas 是 InTradeSys 的团队负责人,他在那里帮助开发 Web 应用和移动应用。在技术方面,他还是一名特技演员,大胆地探索无人涉足的领域。

更多 Thomas Biniasch 的文章…

关于 Robert Nyman [名誉编辑]

Mozilla Hacks 的技术布道者和编辑。关于 HTML5、JavaScript 和开放网络发表演讲和博客文章。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直在瑞典和纽约市从事 Web 前端开发工作。他经常在 http://robertnyman.com 上发布博客文章,并且喜欢旅行和结识新朋友。

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


2 条评论

  1. taylor wescoatt

    再次与你们合作这个项目真的很愉快。你们展现出所有我在你们项目中熟悉的主动性、创造力和灵活性。你们真的付出了最后努力,制作出了让 Jay 在大型观众面前演示时令人兴奋的作品。感谢你们!

    2014 年 2 月 28 日 下午 03:51

  2. Thomas

    做得很好,伙计们,期待与你们合作。再次感谢。

    2014 年 3 月 9 日 下午 18:53

本文的评论已关闭。