Meteor 是一个构建在 Node.js 之上的平台,用来开发实时网页程序,这一篇用 Meteor 搭建 App_Recommender_System 的 front-end.
基础
Meteor 的创新
Meteor 的关键性创新在于 Rails 程序只跑在服务器上,而一个 Meteor App 还包括在客户端(浏览器)上运行的客户端组件。这就相当于书店的伙计不仅仅在书店里帮你找书,还跟你回家,每天晚上读给你听(这听起来怪怪的)。
这种架构让 Meteor 变得数据库无处不在。简单说,Meteor 把你的数据拿出一部分子集复制到客户端。这样后两个主要结果:第一,服务器不再发送 HTML 代码到客户端,而是发送真实的原始数据,让客户端决定如何处理线传数据。第二,你可以不必等待服务器传回数据,而是立即访问甚至修改数据(延迟补偿 latency compensation)。
Meteor 特点
Meteor 位于程序数据库和用户界面之间,保持二者之间的数据同步更新。除此之外,Meteor 还有以下特点:
- 纯数据对话。服务器与客户端初始化后只传输数据,由客户端决定如何渲染。
- 一种语言。前后端统一使用 JavaScript 进行开发。
- 无处不在的数据库。浏览器端使用与服务器端一致的 API 访问本地数据库。
- 延迟补偿。在客户端使用预取和数据模型模拟技术,提供接近零延迟的数据库连接体验。
- 全栈响应式。实时作为默认模式,从数据库到模版的所有层面上,都应当具备可用的事件驱动接口。
- 社区生态友好。Meteor 开放源代码并能与现有的开源工具和框架整合,而非取代它们
- 简单即生产力。让事情看起来简单的最佳方式就是让它真正变得简单,通过干净且具古典美的 API 来实现。
Meteor 结构
一般来说需要新建四个子文件夹:/client,/server,/public 和 /lib。然后在 /client 文件夹中新建 main.html 和 main.js 文件。这些文件夹中有一些拥有特别的作用。
Meteor 文件规则
- 在 /server 文件夹中的代码只会在服务器端运行。
- 在 /client 文件夹中的代码只会在客户端运行。
- 其它代码则将同时运行于服务器端和客户端上。
- 请将所有的静态文件(字体,图片等)放置在 /public 文件夹中。
Meteor 加载文件顺序
- 在 /lib 文件夹中的文件将被优先载入。
- 所有以 main.* 命名的文件将在其他文件载入后载入。
- 其他文件以文件名的字母顺序载入。
Meteor 核心概念
模板系统 Spacebars
Spacebar 就是简单的 HTML 加上三件事情:Inclusion (有时也称作 “partial”)、Expression 和 Block Helper。
模板的作用局限于显示或循环变量,而 helper 则扮演着一个相当重要的角色:把值分配给每个变量。
collection
meteor collection 是 MongoDB Collections 的扩展,在 server 下插入 collection 的数据将会在 MongoDB 自动更新。任何在 client 下 publish 的数据将会在客户端页面实时显示。
All collections are reactively updated on both the client and the server. If a new app were published to our store, and thousands of clients were connected, all of their app stores would update immediately without any extra work on our end!
服务器端
在服务器,collection 有一个任务就是和 Mongo 数据库联络,读取任何数据变化。 在这种情况下,它可以比对标准的数据库。
collection 还可以像 API 一样操作 Mongo 数据库。在服务器端的代码,你可以写像 Posts.insert() 或 Posts.update() 这样的 Mongo 命令,来对 Mongo 数据库中的 posts 集合进行操作。
客户端
在客户端,collection 是一个安全拷贝来自于实时一致的数据子集。客户端的 collection 总是(通常)透明地实时更新数据子集。当你在客户端申明 Posts = new Mongo.Collection(‘posts’); 你实际上是创建了一个本地的,在浏览器缓存中的真实的 Mongo 集合。 当我们说客户端 collection 被”缓存”是指它保存了你数据的一个子集,而且对这些数据提供了十分快速的访问。
有一点我们必须要明白,因为这是 Meteor 工作的一个基础: 通常说来,客户端的集合的数据是你 Mongo 数据库的所有数据的一个子集(毕竟我们不会想把整个数据库的数据全传到客户端来)。
另外,那些数据是被存储在浏览器内存中的,也就是说访问这些数据几乎不需要时间,不像去服务器访问 Posts.find() 那样需要等待,因为数据事实上已经载入了。
客户端-服务器通讯
实际情况是服务器端的 collection 被客户端的 collection 通知说有一个新 item,然后执行了一个任务把这个 item 放入 Mongo 数据库,进而送到所有连接着的客户端。
在浏览器的控制台取出所有的 item 没什么用处。需要把这些数据显示在模板中,并把这个简单的 HTML 原型变成一个有用的实时 Web 应用。
发布(Publication)和订阅(Subscription)
collection 通过发布(publications)和订阅(subscriptions)机制把数据实时同步上行或者下行到连接着的各个用户的浏览器或者Mongo数据库中。Meteor App 保证只发布你让这个当前用户看到的数据。autopublish 的目的是让 Meteor 应用有个简单的起步阶段,它简单地直接把服务器上的全部数据镜像到客户端,因此你就不用管发布和订阅了。然而在实际工程中,我们需要删除它。
meteor remove autopublish
发布
一个 App 的数据库可能用上万条数据,其中一些还可能是私用和保密敏感数据。显而易见我们不能简单地把数据库镜像到客户端去,无论是安全原因还是扩展性原因。发布 就是告诉 Meteor 哪些数据子集是需要送到客户端。
为达到这个目的,我们建立一个简单的 Publish() 函数,只发布没有打标记的帖子
// 在服务器端
Meteor.publish('posts', function() {
return Posts.find({flagged: false});
});
订阅
订阅 就是让客户端来确定哪些子集是他们在某个特别时候特别需要的。
在客户端我们需要订阅这个发布。我们仅仅需要增加这样一行到 main.js 文件中
Meteor.subscribe('posts');
Meteor 程序在客户端能够具有可伸缩性:不去订阅全部数据,而是指选择你现在需要的数据去订阅。这样的话,你就可以避免消耗大量的客户端内存,无论服务器端的总数据量有多大。
DDP
基本上我们可以把发布/订阅模式想象成为一个漏斗,从服务器端(数据源)过滤数据传送到客户端(目标)。
这个漏斗的专属协议叫做 DDP(分布式数据协议 Distributed Data Protocol 的缩写)。如果想了解 DDP 的更多细节,可以通过看 Matt DeBergalis(Meteor 创始人之一)在 Real-time 大会上的讲演视频,或者来自 Chris Mather 的这个截屏视频,来学习关于这个概念更多的细节。
DDP distributed data protocol. the stateful websocket protocol.(under "Publish and subscribe", "Methods", and "Server connections") `ddp` can be configured to use a randomly generated subdomain for each long polling connection(Web browsers put a limit on the total number of HTTP connections that can be open to a particular domain at any one time, across all browser tabs.) 所谓 wire up(Mongo driver will automatically register with `ddp` to receive incoming data for `mycollection` and use it to keep `MyCollection` up to date.) 特性: database driver integration automatic latency compensation(client's screen update instantly when they make changes 不用 wait for server round trip. 和 full-stack db drivers to snapshot and restore records??) transparent reconnect authentication (authentication hooks work great with Meteor Account.) input sanitization(audit-argument-checks, match's check) tracker-aware(connection status, subscription readiness, currently logged-in user 都是 reactive variables) default connect(meteor tools 构建会自动set up server, 这样就可以直接 meteor.subscribe,而不用 myconn = DPP.connect(url), myconn.subscribe 了) connection lifecycle hooks (当connection建立或关闭时,实现用户在线统计功能) CRUD bilerplate and quickstart packages (The `insecure` package turns off `allow`/`deny` rule checking for the generic `create`, `update`, and `delete` methods. The `autopublish` package automatically subscribes every connected client to the full contents of every database collection.)
Router
假设有一个帖子列表页面,我们还希望可以通过固定链接访问到每个单独的帖子页面,URL 形式是 http://myapp.com/posts/xyz(这里的 xyz 是 MongoDB 的 _id 标识符),对于每个帖子来说是唯一的。这意味着我们需要某些路由来看看浏览器的地址栏里面的路径是什么,并相应地显示正确的内容。这就是 router 的作用。
添加 Iron Router 包
meteor add iron:router
基本概念
- 路由规则(Route):路由规则是路由的基本元素。它的工作就是当用户访问 App 的某个 URL 的时候,告诉 App 应该做什么,返回什么东西。
- 路径(Path):路径是访问 App 的 URL。它可以是静态的(/terms_of_service)或者动态的(/posts/xyz),甚至还可以包含查询参数(- /search?keyword=meteor)。
- 目录(Segment):路径的一部分,使用正斜杠(/)进行分隔。
- Hooks:Hooks 是可以执行在路由之前,之后,甚至是路由正在进行的时候。一个典型的例子是,在显示一个页面之前检测用户是否拥有这个权限。
- 过滤器(Filter):过滤器类似于 Hooks ,为一个或者多个路由规则定义的全局过滤器。
- 路由模板(Route Template):每个路由规则指向的 Meteor 模板。如果你不指定,路由器将会默认去寻找一个具有相同名称的模板。
- 布局(Layout):你可以想象成一个数码相框的布局。它们包含所有的 HTML 代码放置在当前的模板中,即使模板发生改变它们也不会变。
- 控制器(Controller):有时候,你会发现很多你的模板都在重复使用一些参数。为了不重复你的代码,你可以让这些路由规则继承一个路由控制器(Routing Controller)去包含所有的路由逻辑。
路由:把 URL 映射到模板
默认情况下,Iron Router 会为路由规则,指定相同名字的模板。而如果路径(path 参数)没有指定,它也会根据路由规则的名字,去指定同样名字的路径。
你可能想知道为什么我们需要在一开始去制定路由规则。这是因为 Iron Router 的部分功能需要使用路由规则去生成 App 的链接信息。其中最常见的一个是 的 Spacebars helper,它需要返回路由规则的 URL 路径。
除了指定静态的 / URL ,我们还可以使用 Spacebars helper。虽然它们的效果是一样的,不过这给了我们更多的灵活性,如果我们更改了路由规则的映射路径,helper 仍然可以输出正确的 URL 。
等待数据
如果你要部署当前版本的 App(或启动起来去使用上面的链接),你会注意到在所有帖子完全出现之前,列表里面会空了一段时间。这是因为在第一次加载页面的时候,要等到 posts 订阅完成后,即从服务器抓取完帖子的数据,才能有帖子显示在页面上。
这应该要有一个更好的用户体验,比如提供一些视觉上的反馈让用户知道正在读取数据,这样用户才会去继续等待。Iron Router 给了我们一个简单的方法去实现它。我们把订阅放到 waitOn 的返回上。
Meteor 部署
meteor deploy myapp.meteor.com
当然,你要把“myapp”替换成你想要的名称,最好是命名一个没有被使用的。如果你的名称已经被使用,Meteor 会提示你去输入密码。如果发生这样的情况,只需通过 ctrl+c 来取消当前操作,然后用另一个不同的名称再试一次。
如果顺利地部署成功了,几秒钟后你就能够在 http://myapp.meteor.com 上访问到你的应用了。
实例
安装 meteorjs
$ curl https://install.meteor.com/ | sh
创建项目
$ meteor create app_store
$ cd App-Recommender-System/ $ meteor
浏览器打开 http://localhost:3000/,就可以看到 Meteor App 的状态。
第三方 package
Add three packages:
- twbs:bootstrap - Twitter Bootstrap packaged for Meteor
- iron:router – A Meteor package that handles routing between pages
- barbatus:stars-rating – A small library to give us nice rating stars for the app store.
meteor add iron:router twbs:bootstrap barbatus:stars-rating
Remove two default packages:
- autopublish – a development package that publishes all of our MongoDB data to the client. Great for prototyping, insecure for production!
- insecure – The package name says it all. This package allows the user client to create, update, read and delete any data in our database. It’s another package meant to make development easier, but we won’t have a need for this in our project.
meteor remove autopublish insecure
server
Create app collection
新建 lib 文件夹,创建 apps.js 文件,添加如下代码
Apps = new Meteor.Collection('apps');
Put json files
这里衔接上一个部分 crawler 的工作,将我们保存在 MongoDB 的数据导出来放在 server 文件夹下。
mongoexport -d appstore -c app_info -o ./app_info_new.json
Populate app collection
server 文件夹下新建 fixures.js 来 load data.
// checks if app collection is empty so we don't call this code on every run
if(Apps.find({}).count() < 1){
// read in json file using Npm filesystem package
var fs = Npm.require('fs');
fs.readFile('../../../../../server/app_info.json', 'utf8', Meteor.bindEnvironment(function(err, data) {
if (err) throw err;
var appData = data.split("\n");
for (var i = 0; i < appData.length - 1; i++) {
var rawAppData = JSON.parse(appData[i]);
var app = {};
app.name = rawAppData.title;
app.app_id = rawAppData.app_id;
app.developer = rawAppData.developer;
app.description = rawAppData.intro;
app.avgRating = parseInt(rawAppData.score) / 2;
app.iconUrl = rawAppData.thumbnail_url;
app.recommendedApps = rawAppData.top_5_app;
app.numberOfRecommendations = 0;
// insert app into collection
Apps.insert(app);
}
}, function(err){
throw err;
}));
}
Publish app collection
server 文件夹下新建 publications.js 文件 publish data
client
新建 index.html 文件
在 client 目录下新建 index.html 文件。添加 head,meteor 的 Blaze template engine 会产生 body。
添加 css 文件
这里就不贴代码了
Tell iron:router where to rend templates
在 layouts 里新建 master_layout.html 文件
topChart template
新建 views 文件夹,新建 topChart.html 文件
为 topChart template 填充数据
之前我们加了 “iron:router” 包,用这个包,我们可以提供一个得到 topChart template 的路径,为这个 template 设置数据
在 lib 下新建 routing 文件夹,添加 router.js 文件
更新 topChart.html 文件
|
|
appPreview template
views 里添加 appPreview.html 文件
appPage template
和上面是同样的道理,在 views 里添加 appPage.html 文件,然后为 appPage template 填充数据,不同的是这里多了中间一步,通过遍历 recommendedApps,我们只能从数据库里得到 recommended app 的 appid, 然而并没有 data context,也就是说,我们还需要有一个 template helper 来设置 recommended apps 的 data context,所以需要新建一个 appPage.js 文件来完成这个工作。
之后再修改 routers.js 文件,加上
运行效果


参考链接
DISCOVER METEOR