/ 中存储网

使用 Meteor 构建一个响应式的销售图应用程序

2013-09-08 11:44:23 来源:中存储

在云中创建和部署您自己的单页交互式 Meteor 应用程序(实时更新),只需使用浏览器即可完成该操作。在 IBM DevOps Services 上编写代码,使用 Bluemix MongoDB 服务实现服务器端存储,并将应用程序部署到 Bluemix。

Meteor 是一个新的 Web 应用程序开发平台,正在国际上得到广泛采用。Meteor 不仅仅是一个 JavaScript 编码框架,它还提供了一种创新方式来构建可伸缩、交互式的富 Web 应用程序。通过简化编码模型和减少开发人员必须编写的代码量,Meteor 具有加速开发周期的潜力。使用 Meteor,经验丰富的 Web 应用程序架构师和开发人员只需花费几天或几星期的时间,就可以完成从概念到全面部署的整个过程,而不像平常一样需要几个月或更长的时间。

关于安装 Meteor 平台并开始使用它进行开发的分步指南,developerWorks 文章 “使用 Meteor 轻松开发实时网站” 是一个不错的参考资料。在这篇最新的文章中,我将通过两个详细的应用程序示例更深入地剖析 Meteor 开发,并概述 Meteor 的架构。借助这些知识,您能够自行判断 Meteor 上的快速 Web 应用程序创建是否适合您。

从过去的角度看未来

Meteor 提出的方法从某种意义上讲是革命性的,但它也有一些方面是通过进化而来的。它继续采用了与计算机史中的重大成功案例相同的 IT 路径:电子表格软件。图 1 给出了一个典型的电子表格示例 —一个包含饼图的 Sales by Region 电子表格:

图 1. Sales by Regions 电子表格

一个按区域显示销量的典型电子表格和总销量饼图的屏幕截图

试用常见的电子表格

您可以试用图 1 中的电子表格,它是本文的示例代码 下载的一部分。更改任何销售数据,您会看到这个饼图将自动更新。

如果修改 Sales by Region 电子表格中的任何地区销量数据,总销量数据(未显示)也会发生更改,饼图会立即被重绘,以反映各个切片的新的相对比例。

现在,这种电子表格既不新颖也不怎么有趣。但回想 1983 年 Lotus 1-2-3 向早期的 PC 用户揭示这些特性时的情景,您就能体会到它的影响力。在此之前,任何人都无法以如此少的编程完成如此多的工作。尽管电子表格软件最初不是很直观,但大多数用户都在几天之后就可以熟练地操作它。电子表格软件仍然是推动全球的 PC 销售的杀手级应用程序之一。

快进 30 年

在第一款电子表格软件问世 30 年后,您可以看到电子表格在 Meteor 上得到了发展。图 2 显示了使用 Meteor 在 2013 年创建的 Sales Portal Web 应用程序:

图 2. Sales Portal Web 应用程序

Meteor Sales by Region 应用程序的屏幕截图,其中显示了各地区的销量和总销量饼图

Sales Portal 显示了最新的地区销量数据和一个相应的饼图。作为虚构的 CEO,您可以监视销量数据,每个地区销售团队都可以定期更新其销量数据。

如果已经安装 Meteor,那么您可以 下载 Sales Portal 应用程序并动手体验它。更改该下载的 sales_nologin 子目录并键入 meteor run 。在浏览器实例中输入 http://localhost:3000/,地区销量数据和饼图应该就会出现。双击任何销量数据即可更改它。在您确认更改后,饼图会立即更新。如果在多个浏览器实例中打开 Sales Portal,所有这些实例都会更新,以显示最新的销量数据,而且您可以从任一个浏览器实例中修改这些数据。(如果无法安装 Meteor,那么您可以尝试应用程序的托管版本;请参阅 参考资料。)

图 3 显示了 US Central 团队正在选择和更新其销量数据:

图 3. 更新 US Central 销量

一个用户在 Meteor Sales Portal Web 应用程序中选择和更新 US Central 地区的销量数据的屏幕截图

图 4 显示了更新的 US Central 销量数据和最新的饼图。任何在更新的同时访问 Sales Portal 的用户都会立即看到所做的更改。

图 4. 更新的饼图比例反映了新的 US Central 销量数据

反映了新的 US Central 销量数据的已更新饼图的屏幕截图

无需手动更新,因此您还可以想象一个后端,其中的销量数据由更新前自动生成和合并的子集组成。Sales Portal 应用程序的显示效果与电子表格的表示相同,但 Sales Portal 现在还具有:

  • 通过普遍存在的浏览器利用全球互联网进行访问的能力
  • 多个用户同时访问的能力
  • 可选的自动后端数据聚合和合并

如果您打算使用标准的企业技术(比如一个基于 Java™的工具链)设计、编码和部署这样一个系统,则需要大量的工作。Meteor 显著减少了这一工作,您可在后面的练习代码中看到。

反应性思维

Meteor 反应性默认值

Meteor 拥有的一些数据源默认值是反应性默认值。这些数据源目前包括:

  • Meteor 集合,通常是 MongoDB 查询的结果
  • 显式绑定到 Meteor 的 Session 单例的变量
  • Meteor.user Meteor.userId Meteor.logginIn ,它们跟踪当前的用户和登录状态
  • Meteor.status ,跟踪服务器连接状态

电子表格的一个重要特性就是它的反应性。在 Sales by Region 示例中,当一个地区销量数据更新时,依赖于该数据的其他所有数据都会动态地重新计算。如果依赖的组件要呈现图形输出,比如饼图,则会立即使用更新的切片大小重新绘制该图。您无需编写管理依赖关系的代码(这可能很复杂)或更新饼图等组件的代码。您只需声明这些反应性元素(销量数据)和它们的依赖关系(在本例中为总销量和饼图)。电子表格负责处理所有事情。

现在想象使用一个 Web 应用程序执行相同的操作,您就会很好地了解 Meteor 提出的方法如何简化基于 Web 的系统创建。

设计一个 Meteor 应用程序时,您需要确定反应性元素,比如地区销量数据集合。然后,使用标准 HTML、CSS、客户端 JavaScript 库和组件(比如 jQuery、jQuery UI 或 Underscore),以及 Handlebars 等模板技术(在概念上类似于 JavaServer Pages,通常在客户端上运行;请参见 参考资料),布局您的表示层。Meteor 跟踪反应性元素的所有依赖关系,然后重新呈现视觉元素,并重新计算依赖关系,以反映最新更新的值。

此方法大大减少了您需要编写、调试和测试的基础架构代码量。您无需编写自定义的后端 Web 服务来同步更新请求,无需编写代码来更新数据库或数据存储,无需编写代码将更改通知推送到其他连接的客户端,也无需编写代码在收到通知后从后端抓取更新的值。

 

深入剖析 Sales Portal 代码

清单 1 显示了 sales.js 文件,它包含 Sales Portal 应用程序背后的所有服务器端和客户端逻辑。这是我需要为此应用程序编写的惟一的 JavaScript 代码。(可在代码 下载的 sales_nologin 目录中找到 sales.js。)

清单 1. Sales Portal 的客户端和服务器端逻辑:sales.js
 Sales2013 = new Meteor.Collection("regional_sales"); 

 if (Meteor.is_client) { 
  Template.salesdata.dataset = function () { 
    return Sales2013.find({}); 
  }; 

  Template.datapoint.selected = function () { 
    return Session.equals("selected_datapoint", this._id) ? "selected" : ''; 
  }; 

  Template.datapoint.events = { 
    'click': function () { 
      Session.set("selected_datapoint", this._id); 

    } 
  }; 

 Template.salesdata.rendered = function() 
 { 

  $('.editable').editable(function(value, settings) { 
     Sales2013.update(Session.get("selected_datapoint"), 
 {$set: {total: parseInt(value)}}); 
     return(value); 
  }, { 
     type    : 'text', 
     style : 'inherit', 
     width : 100, 
     submit  : 'OK', 
 }); 

     var cur = Sales2013.find(); 
     if (cur.count() === 0)  // do not render pie if no data 
       return; 
     var data = []; 
     cur.forEach( function(sale) { 
       data.push( [sale.region, sale.total]); 
     }); 
  var plot1 = $.jqplot ('chart', [data], 
    { 
      seriesDefaults: { 
 // Make this a pie chart. 
 renderer: $.jqplot.PieRenderer, 
 rendererOptions: { 
   // Put data labels on the pie slices. 
   // By default, labels show the percentage of the slice. 
   showDataLabels: true 
 } 
      }, 
      legend: { show:true, location: 'e' } 
    } 
  ); 
 } 

 } 

 if (Meteor.is_server) { 
  Meteor.startup(function () { 
      Sales2013.remove({}); 
      Sales2013.insert({region:"US East", total: 2032333}); 
      Sales2013.insert({region:"US Central", total: 150332}); 
      Sales2013.insert({region:"US West", total: 1202412}); 
      Sales2013.insert({region:"Asia Pacific", total: 701223}); 
  }); 
 }

观察 清单 1 中围绕 Meteor.is_client 和 Meteor.is_server 变量的条件。这些是 Meteor 核心提供的运行时上下文指标,可用在您代码中的任何地方。在本例中,它们支持将客户端和服务器端代码组合到同一个 source.js 文件中。条件外部的任何代码都同时在客户端和服务器上运行。

您也可以将客户端和服务器源代码完全分开,将客户端代码放在一个名为 client 的子目录中,将服务器端代码放在一个名为 server 的子目录中。在这种情况下,可将客户端和服务器都需要的共有内容放在一个名为 public 的子目录中。Sales Portal 应用程序的一个更加安全的版本(您在本文后面将会看到)使用了这种目录结构。

识别反应性数据

另一个反应性数据源

清单 1 中的 selected_datapoint 会话变量也是反应性的。(请参见 Meteor 反应性默认值 边栏,了解默认情况下属于反应性元素的更多信息。)在本例中,它用于更改销量数据的行突出显示形式。行突出显示通过更改动态的 CSS 样式来执行。selected_datapoint 会话变量会在用户单击一行时更新。因为 Meteor 会在这个变量每次更改时重新呈现依赖关系,所以突出显示形式会相应地进行更新。

Sales Portal 应用程序的一个反应性数据源是对 Sales2013 Meteor 集合的一次查询。可在清单 1 中的这个客户端代码段中看到它的用法:

Template.salesdata.dataset = function () {
   return Sales2013.find({});
};

因为该查询是反应性的,所以它的所有依赖关系都会在查询结果集更改时重新计算或重新呈现。这是在所有浏览器实例中更新销量数据和饼图的方式。清单 2 给出了相关的 HTML 模板代码,可在代码 下载的 sales_nologin 目录中的 sales.html 文件中找到:

清单 2. 客户端 HTML 和模板:sales.html
 <head> 

 <title>Sales by Region</title> 
 </head> 

 <body> 
  <div id="title"> 
    <h1>Global Sales 2013</h1> 
  </div> 
  <div id="container"> 

    <div id="salestable"> 
      {{> salesdata}} 
    </div> 
    <div id="chart"> 
    </div> 
  </div> 

 </body> 

    <template name="salesdata">    <div class="salesdata">      {{#each dataset}}        {{> datapoint}}      {{/each}}    </div>   </template>  

 <template name="datapoint"> 
  <div class="datapoint {{selected}}"> 
    <span class="region">{{region}}</span> 
    <span class="sales editable">{{total}}</span> 
  </div> 
 </template>

清单 2 中的 HTML 文件是一个 Handlebars 模板,Meteor 目前支持该模板。在 {{ }} 中可以看到 Handlebars 表达式。通过它的 Spark 引擎(将在本文的 现代 Web 应用程序的一种架构 一节中描述),Meteor 可处理其他 JavaScript 模板组件。

销售数据行通过 salesdata 模板代码呈现,这些代码已在 清单 2 中以粗体形式显示。此模板依赖于 dataset 帮助函数(如 清单 1 中所示),所以在每次查询发生更改时都会重新呈现。

在服务器上提供抽样数据

Sales Portal 的最初的地区销量数据由清单 3 中所示的服务器端代码(来自 清单 1 )提供:

清单 3. 提供 MongoDB 中的数据的服务器端代码
 if (Meteor.is_server) { 
  Meteor.startup(function () { 
      Sales2013.remove({});        Sales2013.insert({region:"US East", total: 2032333});        Sales2013.insert({region:"US Central", total: 150332});        Sales2013.insert({region:"US West", total: 1202412});        Sales2013.insert({region:"Asia Pacific", total: 701223});
  }); 
 }

Meteor 的延迟补偿

Meteor 有一个称为 延迟补偿(latency compensation)的特性。从根本上讲,延迟补偿是大数据管理领域中的最终一致性概念的一种视觉表现。当通过 Minimongo stub 更新客户端上的数据时,任何更改都会立即在客户端上反映出来,包括反应性重新呈现。这些更改还会被传播到服务器。但是,传播的更改可能会失败,失败的原因有很多,包括拒绝访问。发布 - 订阅机制负责确保客户端最终(通常很快)反映了服务器的实际状态。延迟补偿可实现一种无需等待的、响应非常迅速的 UI,这是现代 Web 2.0 应用程序的一个鲜明特征。而代价可能是出现短暂的视觉数据不一致性。

在 Meteor 服务器上,运行着一个完整的 MongoDB 实例。这个完整实例可接受来自 Meteor 以外的客户端的查询和更新。

在 Meteor 客户端上,可以使用相同的 JavaScript MongoDB API。这统一了客户端和服务器编码,实现了客户端和服务器上的代码重用。客户端 API 由一个称为 Minimongo 的智能 stub 提供。Minimongo 使用 延迟补偿来反映数据库更改。因为 Minimongo 通常处理小型的客户端数据集,所以它不支持索引。

使用一种发布 - 订阅模型来控制 MongoDB 服务器与 Minimongo 客户端之间同步的数据。默认情况下,所有服务器端 Meteor 集合都会被发布。Meteor 使用 DDP(Distributed Data Protocol,分布式数据协议)在客户端与服务器之间移动数据。(可为其他数据库创建 stub 并提供程序形式的 DDP 驱动程序;Meteor 社区的持续工作包括一个即将推出的 MySQL 驱动程序。)

集成 jQuery 插件

Sales Portal 使用 jqPlot jQuery 插件呈现饼图。饼图的呈现和重新呈现是反应性的,由 Sales2013 集合中的数据更改推动。您之前已经看到,每次 Sales2013 集合更改时,都会重新呈现 salesdata 模板。清单 4 显示了在 salesdata 模板的 rendered 事件触发时,重新绘制饼图的客户端函数(来自 清单 1 ):

清单 4. 使用 jqPlot 插件呈现饼图的 jQuery 代码
Template.salesdata.rendered= function() 
 { 

  $('.editable').editable(function(value, settings) { 
     Sales2013.update(Session.get("selected_datapoint"), 
 {$set: {total: parseInt(value)}}); 
     return(value); 
  }, { 
     type    : 'text', 
     style : 'inherit', 
     width : 100, 
     submit  : 'OK', 
 }); 

       var cur = Sales2013.find();        if (cur.count() === 0)  // do not render pie if no data         return;       var data = [];       cur.forEach( function(sale) {         data.push( [sale.region, sale.total]);       });    var plot1 = $.jqplot ('chart', [data],      {        seriesDefaults: {   // Make this a pie chart.   renderer: $.jqplot.PieRenderer,   rendererOptions: {     // Put data labels on the pie slices.     // By default, labels show the percentage of the slice.     showDataLabels: true   }        },        legend: { show:true, location: 'e' }      }    );   
 }

Sales Portal 使用 Jeditable jQuery 插件实现销量数据的就地编辑 (in-place editing)。处理编辑的代码位于 清单 4 中的Template.salesdata.rendered = function() 和 var cur = Sales2013.find(); 行之间。

请参见 参考资料,获取 jQuery、jqPlot 和 Jeditable 插件的更多信息。

理解 Meteor 的电子表格和脚本加载顺序

要成功加载 jQuery 插件,按照正确的顺序加载与它们有关联的 CSS 文件和 JavaScript 代码至关重要。

请注意,在 清单 2 中,sales.html 文件不包含任何 <script> 标记或 <link type="text/css" ... > 样式表。相反,Meteor 通过扫描目录来自动加载客户端脚本和样式表:从最深的目录开始,然后在每个目录中按字母顺序依次扫描。

为了利用这个加载顺序(和脚本或 CSS 文件的名称无关紧要的事实),我重新命名了一些脚本,以确保它们的加载位置不变。例如,jquery.jeditable.mini.js 重命名为 client/js 目录下的 zjquery.jeditable.mini.js,以确保它最后被加载。我还将 jqplot.pieRenderer.min.js 重命名为 yjqplot.pieRenderer.min.js,以确保它在 jquery.jqplot.min.js 之后加载。我将来自插件的 CSS 文件放在 client/css 子目录中,确保它们首先被加载。

 

提高 Sales Portal 的访问安全

目前为止,任何拥有 Sales Portal URL 的人都可看到甚至更改销量数据。尽管这对实际用例而言权限太宽松了,但 Meteor 所支持的默认原型模式是您应用程序最初快速发展的理想选择。在此阶段,您可在快速迭代过程中修改交互、UI 甚至是应用程序逻辑,这通常不会涉及任何敏感数据。在拥有原型设计阶段的开放访问模型下,您能够与协作开发人员和审核用户共享该 URL,从而收集反馈。

下一个自然步骤是打开 Meteor 的安全特性,锁定应用程序。Sales Portal 的一个安全得多(因此更符合实际)的版本的代码可在 下载部分的 sales 子目录中找到。它添加的安全特性包括:

Meteor Smart Packages

Smart Package 是一种功能模块,您可通过命令行在 Meteor 中轻松地添加或删除它们。一个 Smart Package 可包括服务器端和客户端代码、UI、API 等。在本文中可看到 accounts autopublish 和 insecure 的示例。也可通过添加 Smart Package 来添加 CoffeeScript 支持、URI 路由和许多其他特性。要查看针对您使用的 Meteor 版本的最新的 Smart Package 列表,可运行meteor list 命令。

  • 一个用于身份验证系统,仅允许授权的用户访问门户
  • 确保仅一个地区的销量数据的官方所有者可修改该数据的代码
  • 更好的源代码组织,用于确保没有向部署的客户端公开服务器端代码

从现在开始,我所指的 Sales Portal 应用程序都是 sales 目录中的安全版本。

删除客户端修改服务器数据的能力

锁定应用程序的一个不错起点是阻止任何人修改该数据。在这种情况下,您需要做的是使用以下命令删除 insecure Smart Package:

 meteor remove insecure

基本上,insecure Smart Package 会告诉服务器在读取或更改数据之前不要检查访问规则。这个 Smart Package 默认情况下已经安装,它允许进行所有访问。删除它之后,任何客户端都不可以修改任何服务器数据。如果回到某个浏览器实例并尝试修改任何销量数据,您会注意到,虽然应用程序会尝试更改该数据,但它很快会撤销此操作,这反映了来自服务器的拒绝访问。(这是 延迟补偿的一个实际应用示例。该数据会在客户端中更新片刻,但一旦经过授权的服务器副本到达,就会覆盖客户端的数据。)

删除 insecure 包之后,您必须添加访问规则来显式允许(特定用户)访问特定的数据片段。但目前还没有用户。接下来必须添加一个用户数据库和登录授权系统。

确保只有授权的用户才能查看销量数据

添加用户授权系统之前,确保没有人可以看到销量数据。(授权的用户被允许在以后看到它。)现在,即使他们无法修改该数据,仍然可通过访问 Sales Portal URL 来查看它。

删除默认的 autopublish Smart Package,预防任何 Meteor 集合数据从服务器发布到客户端(除了服务器显式发布的数据和客户端显式订阅的数据):

 meteor remove autopublish

如果现在访问 Sales Portal URL,地区销量数据和饼图是不可见的。

通过 accounts Smart Package 添加用户登录名

Meteor 提供了 Smart Package 来简化用户登录和授权系统的添加。accounts Smart Package 涵盖端到端工作流;它包含所需的前端 UI、后端数据库和客户端到服务器 API。您可以使用一个命令将所有这些特性添加到 Sales Portal:

 meteor add accounts-password accounts-ui

account-password Smart Package 支持通过熟悉的电子邮件地址加密码的登录方式来创建用户和登录名。该实现使用了 Secure Remote Password 协议(参阅 参考资料),明文密码绝不会在客户端与服务器之间发送。

除了基于密码的登录,也可让用户通过 Facebook、Twitter、微博、GitHub、Google 和 Meetup 登录,只需向应用程序添加一个或多个 Smart Package 即可。社交网络 OAuth 的登录支持目前在企业内部网环境中可能不是很有用,但这些特性对面向消费者的 Web 或移动应用程序很有价值。

自定义用户登录 UI

如果希望进一步控制 UI 小部件中使用的对话框的样式,可添加 accounts-ui-nostyle 包来代替 accounts-ui 包。如果希望完全接管 UI,请参阅 Meteor 文档中对用于该流程的 API 和数据流的描述(参见 参考资料)。

用于登录的插入式 UI

accounts-ui 包提供了一组预先构建的 CSS 样式的 UI 小部件(和支持性 JavaScript 代码),以处理用户登录、新用户创建和密码丢失恢复。要添加它们,可添加{{loginButton}} Handlebars 帮助器。清单 5 显示了添加到 Sales Portal 应用程序的 sales/sales.html 文件中的登录系统:

清单 5. 添加一个用户登录和授权系统
 <body> 
  <div id="title"> 
   <div class="header"> 
   <div class="span5"> 
     <h1 style="margin-bottom: 0px">Sales Portal</h1> 
   </div> 
   <div class="span5">  <div style="float: right"> 
    {{loginButtons align="right"}} </div> 
   </div> 
   </div> 
  </div>

代码 下载中的 sales 目录包含具有用户访问控制权的已完成的 Sales Portal 应用程序。可运行此版本来尝试登录。启动一个浏览器实例,请注意,现在右上角有一个 Sign in链接。单击它就会看到如图 5 所示的对话框:

图 5. 来自 accounts-ui Smart Package 的登录对话框(在 Firefox 中显示)

登录对话框的浏览器 (Firefox) 的屏幕截图

accounts Smart Package 使用 Meteor 集合和发布 - 订阅(您也可以在子集的代码中手动使用相同的工具)来实现用户数据库。在当前的 Sales Portal 版本中,我在该数据库中创建了两组用户凭据,如表 1 所示:

表 1. 现有的 Sales Portal 用户凭据
电子邮件 密码
joe@dwtestonly.com abc123
sing@dwtestonly.com abc123

打开两个浏览器实例并分别使用一个凭据登录。

可单击 Sign in对话框中的 Create account链接创建更多用户。图 6 显示了 accounts-ui Smart Package 中包含的创建新用户的对话框:

图 6. accounts-ui Smart Package 中创建新用户帐户的对话框(在 Chrome 中显示)

创建新用户的对话框的浏览器屏幕截图(来自 Chrome)

向地区销量数据添加 owner 字段

在当前的 Sales Portal 版本中,最初的数据库内容已被修改。我使用了清单 6 中的服务器端代码来提供该数据:

清单 6. 服务器端数据提供代码
 Sales2013.remove({}); 
 Sales2013.insert({region:"US East", total: 2032333}); 
 Sales2013.insert({region:"US Central", total: 150332, owner: joe._id}); 
 Sales2013.insert({region:"US West", total: 1202412}); 
 Sales2013.insert({region:"Asia Pacific", total: 701223});

将一个新 owner 字段添加到 清单 6 中。在本例中,owner 字段包含拥有 US Central 地区数据的用户的 userId (joe@dwtestonly.com)。这个字段用于将地区销量数据更新仅限制在 joe@dwtestonly.com。可查询 Meteor.users 集合来获取 userId 的值。

细粒度的选择性服务器数据发布

删除 autopublish Smart Package 之后,有必要显式地从服务器发布数据,并显式地从客户端订阅它。

对于 Sales Portal,服务器使用清单 7 中的代码发布 global_sales 集合,该代码包含在 sales/server/sales.js 文件中:

清单 7. 从服务器选择性地发布数据
 Meteor.publish("global_sales", function () { 
      if (this.userId) {  // only visible to logged in users 
      // do not include the owner field for client access 
      return Sales2013.find({}, {fields: {"region": 1, "total":1 }}); 
      } 
 });

请注意,在 清单 7 中,使用了 this.userId 确保有效的用户登录到客户端会话。当 Meteor 代表用户执行服务器代码时,this.userId 始终包含当前已登录用户的惟一 ID。如果当前浏览器实例没有已登录的用户,则 this.userId 是空的,并且没有发布任何数据。而且,在 清单 7 中,不是一个地区销量数据文档(一个 文档实质上是一条包含 MongoDB 实例中可变数量的字段的记录)中的所有字段都会在发送给客户端的集合中返回。具体而言,文档的 owner 字段已对客户端隐藏。这样,在使用一个查询时,您只能发布包含某个字段子集的集合子集,只能将它发布到具有授权的已登录用户的客户端。此技术对确保客户端浏览器无法访问某些文档中的敏感数据字段至关重要。

客户端数据订阅

Sales Portal 客户端代码显式订阅服务器发布的 global_sales 集合,如清单 8 所示:

清单 8. 客户端订阅来自服务器的一个集合
 Meteor.subscribe("global_sales"); 
 Template.salesdata.dataset = function () { 
  return Sales2013.find({}); 
 };

添加访问规则以允许更新地区销量数据

删除 insecure Smart Package 后,实际上会拒绝所有用户更新销量数据。假设不同的地区销量数据归不同的用户所有,可添加一条访问规则来允许 joe@dwtestonly.com 更新 US Central 数据。清单 9 给出了名为 model.js 的服务器端源文件中的这条访问规则:

清单 9. 允许所有者更新销量数据的服务器端访问规则
 Sales2013.allow({ 
  update: function (userId, sales, fields, modifier) { 
    if (userId !== sales.owner) 
      return false; // not the owner 

    var allowed = ["total"]; 
    if (_.difference(fields, allowed).length) 
      return false; // tried to write to forbidden field 

    return true; 
  }, 

 });

如果允许更新,update 操作的访问规则函数会返回 true,否则会返回 false。清单 9 中的代码首先执行检查,确保用户是所有者并且仅修改了 total 字段。

用于迭代式开发的 Meteor 热代码重新加载

要在开发和调试期间节省时间,可让浏览器一直打开 Meteor 应用程序,甚至在更改代码、CSS 或模板时也如此操作。大多数时候,Meteor 的热代码重载特性会检测更改,并将它们推送到客户端浏览器。

打开 Sales Portal 并以 joe@dwtestonly.com 身份登录。尝试修改 US West 数据,您可能注意到它会失败,随后尝试修改 US Central 数据。因为 joe@dwtestonly.com 是此数据的所有者,所以您可修改它。

启动另一个浏览器实例并以 sing@dwtestonly.com 身份登录。尝试修改任何销量数据,您可能注意到它会失败。因为 sing@dwtestonly.com 不是任何销量数据的所有者,所以服务器会拒绝来自该用户的所有修改请求。

如果用户未登录,可使用客户端 currentUser 函数避免呈现模板。将清单 10 中的代码添加到 HTML 文件中:

清单 10. 消除呈现空模板的尝试
 <div id="container"> 
    <div id="salestable"> 
      {{#if currentUser}}        {{> salesdata}}        {{/if}}
    </div> 
    <div id="chart"> 

    </div> 
 </div>

现在,启动一个新的 Sales Portal 浏览器实例。请注意,您无法看到任何数据。以 sing@dwtestonly.com 身份登录,您会注意到,现在您可以看到数据和饼图。再次尝试修改一个字段;您任然无法修改它,因为您不是所有者。

启动另一个浏览器实例,以 joe@dwtestonly.com 身份登录,您可能注意到,现在您可以看到该数据。修改 US East 数据,饼图也会被更新。确认 sing@dwtestonly.com 会话中的饼图已经发生更改。注销两个会话,您可能注意到,该数据现在消失了。

 

应用程序部署:云和私有云

为了简化部署,消除您在创建演示或试验 Meteor 时设置自己的服务器的需求,Meteor 团队设计了单个命令,将应用程序部署到其云托管服务器上。在编写本文时,此服务是免费的。您需要做的是从您的应用程序目录发出此命令:

 meteor deploy applicationname.meteor.com

应用程序名称必须是惟一的,因为它将以 http://applicationname.meteor.com 格式(通过互联网向全球)公开。本文的应用程序部署在 Meteor.com 上;请参见 参考资料,获取它们的链接。

如果希望用户能够通过您自己公司的域名访问您的应用程序,比如通过 http://applicationname.mycompany.com/,那么您需要创建一个 CNAME(别名)DNS 记录,并将它指向 origin.meteor.com。

要在您自己的服务器基础架构上托管应用程序,需要一个预先安装了 node.js 和 MongoDB 工具的服务器。可使用以下命令创建应用程序的一个可部署软件包:

 meteor bundle applicationname.tgz

node.js 上的 fiber

fiber 是 node.js 领域一个相对较新的成员。它在独立的逻辑流(fiber)之间实现了非抢占式多任务。必须在显式地执行了一个 fiber 之后,才能执行另一个 fiber。因为您会准确地控制执行点,所以通常无需保护 fiber 之间的共享状态。fiber 的重要特性在于它提供了标记便捷性,使您能够编写看起来有顺序的代码(比如每个逻辑流一个线程),而不是 node.js 编程另一种典型的深度嵌套的回调。(请参见 参考资料,获取 node.js fiber 的更多信息。)

在编写本文时,Meteor 0.6.3.1 要求在与最终的部署系统相同的操作系统上创建自部署软件包。这个要求源于原生编译代码的依赖性。

Meteor 服务器端代码在 node.js fiber上运行,提供了一个虚拟环境,您可以在该环境进行编码,就像一个线程处理一个传入请求(具有不可共享的状态)那样。此方法可简化服务器端 JavaScript 逻辑的编码。

因为服务器端可部署对象是一个 node.js 应用程序,所以您可以针对自己的具体的互操作或扩展需求来自定义部署拓扑结构。

 

Foto Share:一种移动照片共享服务

现在您已看到使用 Meteor 代码可进行的一些设计和规划,您可能已经在考虑启动一两个项目。下面的示例为您提供了在为移动设备创建 Meteor 应用程序时可采用的更多想法。

Meteor 和移动应用程序

针对创建移动应用程序的 Meteor 特性仍然处于早期开发阶段。整个 Meteor 项目是一项正在快速演化的工作,完整的移动开发支持有望在 1.0 版之后实现。

Foto Share 是一个面向移动电话用户的具有实验性质的照片共享服务,用法既简单又直观:用户可使用他们的电话浏览照片集合,并单击 Share 按钮与其好友分享照片。图 7 显示了在 Apple iPhone 上运行的 Foto Share:

图 7. Apple iPhone 上的 Foto Share

Apple iPhone 上的 Foto Share 的屏幕截图

就像在 Sales Portal 项目中一样,出于同样的安全原因,autopublish 和 insecure Smart Package 已从 Foto Share 中删除。而且为了实现基于密码的登录,Foto Share 添加了 accounts-ui 和 accounts-password Smart Package。代码 下载 中的 Foto Share 应用程序也添加了两个与 Sales Portal 相同的用户。

要试用 Foto Share,首先需要从代码 下载 中的 fotoshare 目录运行该应用程序。如果有两个电话,可将每个电话的浏览器指向 Foto Share。否则,可继续使用 PC 浏览器。以 sing@dwtestonly.com 身份在一个浏览器上登录,以 joe@dwtestonly.com 身份在另一个浏览器上登录(使用用于 Sales Portal 的相同密码)。可以通过滑动 Sing 的照片来浏览它们,或者触摸任意一侧的覆盖图。每张新照片会划入视图中。您会发现,Sing 的所有图片都是来自夏威夷的场景,而 Joe 的照片是阿兹特克和玛雅的。

准备好测试共享后,选择从 Joe 的集合中选择一张图片并触摸电话上的 Share 按钮。在 Sing 的电话上,Meteor 会反应性地更新订阅的集合,Joe 共享的图片现在可在 Sing 的电话上看到。

Foto Share 用户登录 UI

Meteor 目前还没有一个针对移动设备的 accounts-ui Smart Package。像 Meteor Web 应用程序一样,能够 “插入” 一个可自定义的移动登录 UI 会很不错。对于 Foto Share,我使用了 accounts-ui Web UI。图 8 显示了一个移动电话上出现的登录对话框:

图 8. Foto Share 登录屏幕

一个移动电话上的 Foto Share 登录屏幕的屏幕截图

用户通常不会介意每次访问 Web 应用程序都进行登录,但他们期望在首次输入密码后,任何电话应用程序会代表他们自动登录。一个针对移动 Meteor 应用程序的可能解决方案是,将客户端代码包装在一个原生应用程序包装器中,比如流行的 Apache Cordova 平台(参见 参考资料)。然后可以访问电话自己的登录配置文件,避免每次都要求用户登录。此外,您可以直接访问存储在电话的内置图库或相册应用程序中的用户照片集合。

集成 jQuery Mobile 和页码插件

利用移动浏览器与 HTML5 的兼容性越来越高,在 jQuery Mobile 框架上开发的应用程序通常可在最新的主流移动操作系统上运行。Sales Portal 应用程序已展示了 Meteor 如何与 jQuery 和 UI 插件(比如 jqPlot)集成。与之前一样,您需要留意 jQuery Mobile 和分页插件库的 CSS 样式表和 JavaScript 文件的加载顺序。

因为 jQuery Mobile 和 Meteor 的 LiveHTML(反应性重新呈现)需要通过浏览器的文档对象模型 (DOM) 增强和操作 HTML 元素,所以您必须确保它们按照可兼容的顺序进行操作。在 Foto Share 中,jQuery Mobile 执行的页面初始化必须延迟到 Meteor 的操作完成之后。aadelaybind.js 文件(包含以下代码)在 jQuery Mobile 之前加载,以延迟其页面初始化:

 $(document).bind("mobileinit", function(){ 
  $.mobile.autoInitializePage = false; 
 });

在 Meteor 的 LiveHTML 完成它的工作后,pages 模板的 rendered 事件帮助器会触发实际的页面初始化。这个帮助器函数的相关部分如下:

 Template.pages.rendered = function() { 
   ... 
   $.mobile.initializePage();
   ... 
 };

识别 Foto Share 中的反应性数据

在概念上,被设计为最自然的反应性的数据集合是用户的照片集。这么做会使得 Meteor 在每次有人共享其照片时都更新和重新呈现照片列表。这是实验中采用的方法。对于更大的系统,根据图像的后端存储架构,您可能希望仅将照片的元数据而不是图像本身设计为反应性的。

查看服务器端数据提供代码(如清单 11 所示),以了解照片的存储方式:

清单 11. Foto Share 服务器端数据提供和集合发布代码
 Meteor.startup(function () { 
      
      ...     
      
      Fotos.remove({}); 
      Fotos.insert({name:"pic1", img: readPic('pic1.jpg'), 
       owner: sing._id, shared:false}); 
      Fotos.insert({name:"pic2", img: readPic('pic2.jpg'), 
       owner: sing._id, shared:false}); 
      Fotos.insert({name:"pic3", img: readPic('pic3.jpg'), 
       owner: sing._id, shared:false}); 
      Fotos.insert({name:"pic4", img: readPic('pic4.jpg'), 
       owner: joe._id, shared:false}); 
      Fotos.insert({name:"pic5", img: readPic('pic5.jpg'), 
       owner: joe._id, shared:false}); 
      Fotos.insert({name:"pic6", img: readPic('pic6.jpg'), 
       owner: joe._id, shared:false}); 

    Meteor.publish("photos", function () { 
      if (this.userId) {  // only visible to logged in users 
       return Fotos.find( {$or : [{owner: this.userId}, {shared: true}]}, 
  {fields: {"name": 1, "img":1 , "owner": 1}}); 
      } 

  }); 
 });

服务器发布的集合是 FotosFotos 中表示照片的每个文档都拥有 nameimg 和 owner 字段。img 字段是从相应的本地存储的 JPG 文件中读取的。同样请注意,一个订阅客户端收到的数据仅包含他或她自己的照片,加上它的所有者共享的其他任何照片。这里还应用了选择性的字段过滤,从客户端收到的 Foto 集合中删除 shared 字段。owner 字段未过滤掉,因为客户端可能希望显示某个共享图片的所有者名称。清单 12 给出了 readPic() 帮助器函数。它使用同步 fs 来将图像读取到内存中,然后将二进制系统编码为 base64,以便存储在 img 字段中。此格式可方便地用于对从客户端获取照片后显示照片。

清单 12. 读取用于 MongoDB 存储的 JPG 图像的帮助器函数
 function readPic(infile)  { 
    var fs = Npm.require('fs'); 
    var path = Npm.require('path'); 
    var base = path.resolve('.'); 
    var deployLoc = 'public/images/'
    var data = fs.readFileSync(path.join(base, deployLoc, infile)); 
    var tp = data.toString('base64'); 
    return  'data:image/jpeg;base64,' + tp; 
 }

当从数据库再现图像时,模板代码会利用大多数现代浏览器上的 <IMG> 标记中的 数据 URL支持(参见 参考资料)。<IMG> 标记的 SRC 属性上的数据 URL 支持通过一个 base64 编码的字符串实现了图像的二进制位的动态设置。清单 13 显示了 photopage 模板的一部分。在这个模板中,来自照片的 base64 编码的 img 字段用于在一个 jQuery Mobile 页面上呈现该图像。

清单 13. 使用数据 URL 设置一个 IMG 标记的 SRC 属性
 <template name="photopage"> 
  <div data-role="page" id="p{{index}}"> 
    ... 
    <div data-role="content" class="apic"> 

      <img src="{{img}}" />

      <ul data-role="pagination"> 
      {{#if indexIsZero}} 
   <li class="ui-pagination-next"><a href="#p{{indexNext}}">Next</a></li> 
      {{else}} 
   <li class="ui-pagination-prev"><a href="#p{{indexPrev}}">Prev</a></li> 
   <li id="x{{index}}" class="ui-pagination-next"> 
     <a href="#p{{indexNext}}">Next</a></li> 
      {{/if}} 
      </ul> 

    </div> <!-- /content --> 
    ... 
  </div>  <!-- /page --> 
 </template>
 

Meteor Remote Methods:自定义 RPC 非常简单

尽管 insecure Smart Package 已被删除,但仍未在 Foto Share 中创建访问规则。没有访问规则,任务客户端都无法通过 Minimongo 访问数据。但是,当单击 Share 按钮时,照片的 share 字段必须通过某种方式进行更新。如何更新呢?答案是 Meteor Methods。

Meteor Methods 是一种远程过程调用 (RPC) 机制。只需两个简单的步骤,就可以创建从客户端到服务器的 RPC 调用:

  1. 在服务器端定义一个 JavaScript 函数。
  2. 使用 Meteor.call() 远程调用该服务器函数,还可以有选择地传递参数。

Meteor 会负责这期间的所有端点设置、准备和数据编组工作。

单击 Foto Share 中的 Share 按钮时,客户端调用了服务器上一个名为 shareThisPhoto 的 Meteor Remote Method,将照片的 ID 以参数形式传递。在服务器端,代码首先检查调用方是否是照片的所有者,它仅在所有者调用该方法时更新照片的 shared 字段。清单 14 给出了服务器端 shareThisPhoto 代码:

清单 14. 服务器端上更新照片的 shared 字段的 Meteor Remote Method
 Meteor.methods({ 
  shareThisPhoto: function (photoId) { 
    console.log('called shareThisPhoto'); 
    console.log(photoId); 
    var curPhoto = Fotos.findOne({_id: photoId}); 
    if (this.userId !== curPhoto.owner)  { 
      return "Cannot share this photo."; 
    } else { 
      Fotos.update({_id: photoId}, {$set :{shared: true}}); 
      return "Photo shared!"; 
    } 

  }, 
 });

清单 15 给出了在您单击 Share 按钮时,调用该远程方法的客户端代码:

清单 15. 单击 Share 按钮时调用远程方法的客户端代码
 Template.photopage.events({ 
  'click .fs-logoff': function () { 
    Meteor.logout(function() { 
      location.reload(); 
    }); 
  }, 
     'click .fs-share': function() {        Meteor.call('shareThisPhoto', this._id, function (error, retval) {      console.log(retval);        });    }
 });

我选择了 RPC 方法来演示 Meteor Remote Method。也可以定义一条访问规则来允许所有者更新 shared 字段。在这种情况下,当用户单击 Shere 按钮时,您必须随后在本地更新照片的 shared 字段。Meteor 的 Minimongo 将更新推送到服务器,然后将其推送到其他所有已订阅的客户端。

 

现代 Web 应用程序的一种架构

现在您已查看了本文的示例应用程序,您应该已经认识到,Meteor 是为具有图 9 中所表示的特定架构的 Web 应用程序而设计的:

图 9. 富客户端交互式 Web 应用程序架构

富客户端交互式 Web 应用程序架构图

这些类型的应用程序通常包含一个高度交互式的单页 UI。用户通常不会遇到新页面加载;所显示页面的一部分会立即更新来响应用户交互,具有很短的或者甚至没有网络往返延迟。单页界面绝不会限制应用程序,因为页面的各部分能够以多种多样的方式进行更新。这暗指一种独立的桌面应用程序,比如文字处理器或电子表格。

这些类型的应用程序通常会在客户端浏览器上加载 JavaScript 应用程序代码。此代码会管理与用户之间的交互,通过动态操作浏览器的 DOM,修改 CSS 样式,生成新的 HTML 元素 / 代码 / 样式,以及利用其他浏览器提供的 API。与用户的所有交互都由客户端代码控制,除了初始应用程序加载之外,不会通过网络加载额外的 HTML 或样式。相同的代码还会在客户端与服务器之间来回传输数据,以实现应用程序特性。浏览器实质上加载和运行的是一个使用 JavaScript 编写的富客户端(有时称为胖客户端)应用程序。

在服务器端,设置了端点从客户端安全地获取和同步数据。遗留的后端可能拥有 RPC、基于 XML 的 Web 服务、RESTful 服务或其他 JSON 样式的 RPC 调用。现代的后端可能提供了专用的协议,旨在高效地通过网络传输数据,在偶尔断开连接后迅速恢复,支持各种流行的传输方式,灵活地伸缩拓扑结构。

图 10 显示了 Meteor 0.6.3.1 版内部的主要组件:

图 10. Meteor 内部组件

Meteor 内部组件

您已知道,DDP 提供了客户端与服务器实例之间的双向数据流,Minimongo 是一个在客户端上具有本地数据缓存的智能 stub ,向客户端代码提供了熟悉的 MongoDB 查询 API。Spark 是一个流行的 HTML 引擎,它使用 (Handlebars) 模板和依赖关系管理来提供 Meteor 的反应性重新呈现特性。(请参见 参考资料,获取官方 Meteor 文档的链接,您可在其中找到 Meteor 组件的详细说明。)

潜在的大数据应用程序架构

Meteor 的单页、实时、高度交互式的模式几乎是为解决某一类问题而量身定做的。可视化大数据的某一方面离不开交互式仪表板,交互式仪表板在结果可用时立即执行更新。该仪表板也可用于对 MapReduce 作业进行排队并监视它们的实时进度。

一种反应性 Meteor 应用程序可潜在地提供数 TB 或 PB 数据的交互式界面,就像电子表格可提供相对较小的数据集的交互式的合并、摘要、下钻或自定义视图一样。图 11 显示了这样一个系统的架构:

图 11. 一个用于大数据的 Meteor 仪表板应用程序的架构

该图显示了一个用于大数据的 Meteor 仪表板应用程序的架构

在 图 11 中,客户端 Meteor 代码提供了大数据存储库的一个反应性仪表板视图。用户交互会在服务器上生成自定义的 MapReduce 任务,它们可能通过 Meteor Methods 进行排队并在 Hadoop 集群上执行。执行完任务后,结果会通过 Meteor 合并到 MongoDB 实例中。Meteor 服务器的发布 - 订阅组件会检测数据更改,将更新的摘要数据推送到已订阅的客户端。

原文地址:http://www.ibm.com/developerworks/cn/web/wa-meteor-webapps/index.html