Dojo mobile TweetView 系列教程之三——Tweets和Mentions视图
目录(?)[+]
作者:David Walsh
翻译:Siqi([email protected] )
原文:Getting Started with TweetView: Tweets and Mentions
在上一个教程Dojo mobile TweetView 系列教程之二——TweetView启程 中,我们介绍了将要使用dojox.mobile创建的移动应用程序“TweetView”,并为其创建了整体布局模板,现在是时候为TweetView添加更多的代码让它真正工作起来了!本教程将重点介绍“Tweets”和“Mentions”这两个视图的实现。在我们开始码代码前,让我们先整理好TweetView的文件目录结构并回顾几条移动应用程序开发的理念。
版本:1.6
难度:中级
系列:TweetView
打理好你的项目文件结构
TweetView将会使用和大多数基于Dojo的项目一样的文件结构:应用程序的HTML放在根目录下,TweetView的JavaScript文件、图片以及样式表则放在js文件夹中各自命名空间同名的文件夹下。
!TweeView使用很少的自定义图片来减少加载时间。为了使图片资源尽可能小,可以使用诸如Pngcrush 来压缩每一张图片。CSS文件则可以通过Dojo自带的打包工具来进行压缩。
写在开始编码前:移动开发准则
各位看官请先暂且停下!在我们开始码代码前,还有几件关于dojox.mobile和移动web开发的事我们需要了解下:
- 大小很重要: 在开发移动应用程序时,一个字节的大小也很重要,所以尽可能的缩写你的代码,虽然某些缩写可能在你开发标准的Web应用程序时是不可接受的。另外请注意,你添加的每一个依赖项都会增加你应用程序的加载时间。
- 最佳实践:Mobile!=Web: 在使用JavaScript编程时有一些常用的最佳实践,如不要扩展原生对象,不要使用全局变量,保证你创建的对象尽可能的灵活、范型。但这些最佳实践往往会以增加代码量为代价,所以在创建移动应用程序时,你可能需要抛弃这些条条框框来提高你的代码性能。
- 保持简单: 创建一个包含很多自定义样式、控件、布局的过度复杂的移动应用程序会很快让你抓狂。所以创建一个简单的布局,然后往上添你需要的东西,这才是比较好的创建移动应用程序方法。
!在TweetView中我们不会摒弃所有的最佳实践。我们将寻求代码大小与这些最佳实践之间的平衡。
在记住这些之后,还有最后一点:我们将暂时不使用缓存。
Mobile Devices and Caching
大多数移动设备严重依赖缓存来缓解数据传输的重担。这对于产品级的应用程序来说是很好,但我们现在还处于开发初始阶段,所以缓存对我们来说只会在测试这个应用程序的时候让我们沮丧。所以在开发Setting视图前,让我们添加一些防止缓存的META标签:
- <meta http-equiv="cache-control" content="no-cache">
- <meta http-equiv="pragma" content="no-cache">
这些META标签将在开发过程中发挥很大作用,在TweetView真正产品化时,我们将移除这些META标签。
Tweets and Mentions视图
“Tweets”和“Mentions”视图时三个视图中最为复杂的。它们的功能和外观都很相似,所以我们将重点介绍如何创建一个可以同时顾及到这两个视图的类。在看过效果图之后,我们知道我们将需要以下控件:
- dojox.mobile.ScrollableView - 整个视图框架
- dojox.mobile.Heading - “Tweets”标题栏
- dojox.mobile.RoundRectList - 列表容器
- dojox.mobile.ListItem - 列表元素
效果图也清楚的告诉了我们将要通过与Twitter交互来获取用户信息,所以我们将需要一些额外的Dojo资源:
- dojo.io.script - 使用JSONP来从Twitter获取信息
- dojo.DeferredList - 让我们在获取到所有信息之后一次性处理多个Twitter请求。
底部的控制条已经在上一个教程中完成了。(Dojo mobile TweetView 系列教程之二——TweetView启程 )
Developing TweetView
准备好了?现在就让我们一块一块开始做吧!
Adding Resources to the Page
我们需要向app.html页面添加我们自己的样式表"TweetView.css":
- <link href="js/tweetview/resources/TweetView.css" mce_href="js/tweetview/resources/TweetView.css" rel="stylesheet" />
这个样式表将包含整个应用程序所需要的样式,不仅仅是Setting视图。
Tweets和Mentions视图模板
我们在上一个教程中已经创建了Tweets和Mentions视图的模板:
- "tweets" dojoType="dojox.mobile.ScrollableView" selected="true">
"dojox.mobile.Heading"
>"dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" style="float:right;" mce_style="float:right;" icon="images/refresh.png">- Tweets
- "dojox.mobile.RoundRectList"
- "dojox.mobile.ListItem"
>- 这里将是Tweet条目
- "mentions" dojoType="dojox.mobile.ScrollableView">
"dojox.mobile.Heading"
>"dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" style="float:right;" mce_style="float:right;" icon="images/refresh.png">- Mentions
- "dojox.mobile.RoundRectList"
- "dojox.mobile.ListItem"
>- Mention tweet item here
我们在新控件完成时对这些模板进行更新,但现在我们不需要对这些HTML布局进行修改。
新的类:tweetview._ViewMixin
在看过所有的三个视图(Tweets, Mentions和Settings)之后,很明显每一个视图都需要修改dojox.mobile.RoundRectList控件的domNode。为了防止每个视图对象中出现重复的代码,我们创建一个名为_ViewMixin的类来提供一个用来获取列表节点的方法,这样我们就可以根据需要隐藏或显示这些节点。_ViewMixin还提供一个substitue方法,它相当与一个非常原始的模板系统。此外还有一个getElements方法用来根据CSS类获取元素。
- // 提供类
- dojo.provide("tweetview._ViewMixin");
- // 声明类
- dojo.declare("tweetview._ViewMixin", null, {
- // 返回列表节点
- getListNode: function() {
- return this.getElements("tweetviewList",this.domNode)[0];
- },
- // 更新列表控件状态
- showListNode: function(show) {
- dojo[(show ? "remove" : "add") + "Class"](this.listNode, "tweetviewHidden");
- },
- // 将数据写入模板
- substitute: function(template,obj) {
- return template.replace(//$/{([^/s/:/}]+)(?:/:([^/s/:/}]+))?/}/g, function(match,key){
- return obj[key];
- });
- },
- // 根据CSS类获取元素
- getElements: function(cssClass,rootNode) {
- return (rootNode || dojo.body()).getElementsByClassName(cssClass);
- }
- });
你会发现这些方法在整个教程中都很有用。
!花一点时间看看每一个效果图,你会发现共享的列表控件访问方法、模板,是很有必要的。如果你现在还不明白_ViewMixin方法的作用的话,不用担心,我们将在创建每个视图的时候重新讲解一遍。
同时请注意,getElements方法是dojo.query的一个替代品。dojo.query方法包含了很多的功能和代码,而我们仅仅需要根据根据样式表类名来寻找节点,所以为了提高性能,我们不使用dojo.query
新的类:tweetview.TweetView
由于Tweets和Mentions视图需要根据内容定制,我们将创建一个名为tweetview.TweetView的新类。这个类会扩展ScrollableView和_ViewMixin,并监管这个视图内所有的功能和内容:
- // Provide the UI class
- dojo.provide("tweetview.TweetView");
- // Dependencies here
- dojo.require("tweetview._ViewMixin");
- dojo.require("dojox.mobile.ScrollableView");
- dojo.require("dojo.DeferredList");
- dojo.require("dojo.io.script");
- // 导入本地化模块
- dojo.require("dojo.i18n");
- dojo.requireLocalization("dojo.cldr", "gregorian", "", "");
- // 声明类:继承ScrollableView
- dojo.declare("tweetview.TweetView",[dojox.mobile.ScrollableView,tweetview._ViewMixin],{
- // 属性和方法
- });
在Tweets and Mentions视图 这一节中提到的额外的dojo资源也在这里被加载。我们还加载了本地化模块,这样我们的时间戳(比如“小时”,“分钟”,“日期”)就会根据用户语言环境正确显示。
TweetView属性
TweetView类创建完了,它同时用来支撑Tweets和 Mentions视图。现在我们必须思考这两个视图的区别,并为TweetView类创建可配置的属性。首先,我们知道获取tweets和mentions信息的URL格式是不同的,所以我们需要为其创建一个属性。我们使用如下格式的URL来获取tweets信息:
- // 获取tweets的URL
- serviceUrl: "http://twitter.com/statuses/user_timeline/${account}.json?since_id=${since_id}",
!注意serviceUrl中的$(account)和$(since_id)片段。还记得我们创建的_ViewMixin么?每次请求用户信息时,这个URL会被传到substitue方法中。
看一下效果图,你会发现tweet列表中的每一条记录需要经过格式化来使得各个部分信息得以正确显示。所以让我们创建一个属性来提供tweet模板:
- // 为tweets创建模板
- tweetTemplateString: '' +
- '${time}' +
- '' +',
- '
${user}' +- '
${text}' +- '
- '
最后一个需要的自定义属性是表示获取新tweets中的GIF图片的路径:
- // 加载状态图标
- iconLoading: dojo.moduleUrl("tweetview", "resources/images/loading.gif"),
!通过使用dojo.moduleUrl,我们可以避免硬编码路径。这种方式使得我们可以更灵活地引用任何资源时——你不可能永远知道你所需要的模块来自哪里。
现在我们定义好了TweetView类,接下来我们需要更新我们的HTML模板来使用TweetView类为指定的视图提供定制的功能。
实现TweetView
如同使用Dijit控件一样,你必须先在app.html中使用require来导入所需资源:
- // 为"Tweets"和"Mentions"导入资源
- dojo.require("tweetview.TweetView");
在导入了需要的类之后,需要为这两个视图更新HTML模板:
- <div id="tweets" dojoType="tweetview.TweetView" selected="true">
- <h1 dojoType="dojox.mobile.Heading" fixed="top">
- <div dojoType="dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" icon="js/tweetview/resources/images/refresh.png">div>
- Tweets
- h1>
- <ul dojoType="dojox.mobile.RoundRectList" class="tweetviewList">ul>
- div>
- <div id="mentions" dojoType="tweetview.TweetView" serviceUrl="http://search.twitter.com/search.json?q=@${account}&since_id=${since_id}">
- <h1 dojoType="dojox.mobile.Heading" fixed="top">
- <div dojoType="dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" icon="js/tweetview/resources/images/refresh.png">div>
- Mentions
- h1>
- <ul dojoType="dojox.mobile.RoundRectList" class="tweetviewList">ul>
- div>
请注意我们对模板所作的修改:
- 每一个视图由dojox.mobile.ScrollableView变成了tweetview.TweetView。
- id为mentions的控件使用了一个定制的serviceURL 来获取信息
- 标题栏添加了fixed="top"以便它可以固定在每个视图的顶部
- "float:right"被从刷新按钮上移除了——这个样式将被移到一个之后我们将会创建的样式表中。
- RoundRectList控件添加了tweetviewList样式,这样我们就能对之后在其中创建出来的tweet条目节点的样式进行定制。
!注意我们正在使用Tweitter的查询API来获取mentions信息。 Twitter的mentionsAPI需要OAuth验证,而实现OAuth超出了本教程的范围。如果你选择实现一个OAuth,你所需要做的就是把serviceUrl参数换成你自己搭建的服务地址。
处理Twitter帐号
TweetView中每一个视图(Tweets,Mention,和Settings)都依赖一串账户信息。这意味着账户信息对所有的TweetView控件必须是可用的。我们可以创建一个“controller”控件来监督所有的TweetView控件并管理账户状态,不过这也超出了我们现在所需要的功能了。所以,我们还是直接把账户信息放到tweetview的命名空间中去:
- // 在命名空间中直接设置账户信息
- tweetview.ACCOUNTS = {
- dojo: { enabled: true },
- sitepen: { enabled: true }
- };
ACCOUNTS对象包含一系列子对象,这些子对象包含了帐号名以及启用状态。更多属性将被添加到这些子对象中,不过它们都不需要被初始化。
TweetView startup
现在一切就绪,是时候来添加TweetView的startup方法了。让我们一行行来看代码。
保留控件原生startup方法所提供的功能:
- // 当控件启动时
- startup: function() {
- // 保留dojox.mobile.ScrollableView的startup方法所提供的功能
- this.inherited(arguments);
获取控件刷新按钮的引用,并保存原始图片路径(这是为了以后我们要从刷新状态图标切换回来)
- // 获取刷新按钮和图片
- this.refreshButton = dijit.byId(this.getElements("tweetviewRefresh", this.domNode)[0].id);
- this.iconImage = this.refreshButton.iconNode.src;
为刷新按钮绑定onClick事件来刷新tweets:
- // 为刷新按钮绑定onClick事件的处理函数"refresh"
- dojo.connect(this.refreshButton, "onClick", this, "refresh");
马上通过调用refresh方法来加载tweets:
- // 马上加载tweets!
- this.refresh();
!这个refresh调用将在Setting视图完成后被移除,不过在此之前它还是必要的。
为该控件添加tweetviewPane样式:
- // 添加CSS类
- dojo.addClass(this.domNode, "tweetviewPane");
获取RoundRectList控件的引用以便之后使用。现在先将其隐藏,因为它现在没有任何内容。
- // 获取列表控件
- this.listNode = this.getListNode();
- // 隐藏列表,因为它现在还没有填充数据
- this.showListNode(false);
设定一个时间间隔,每过几分钟,刷新tweet时间:
- // 每60秒,更新时间
- setInterval(dojo.hitch(this, function() {
- dojo.forEach(this.getElements("tweetviewTime", this.domNode), function() {
- timeNode.innerHTML = this.formatTime(dojo.attr(timeNode, "data-dojo-time"));
- },this);
- }),60000);
这就是TweetView startup方法,它将推动整个程序。接下来我们需要创建一个refresh方法来更新tweet列表!
TweetView refresh
refresh方法将会从Twitter获取tweets信息。由于我们不能在一次请求中获取多个用户的tweets信息,我们需要为每一个账户发送一个单独的请求,然后统一处理返回的数据。这意味着我们需要创建多个dojo.io.script调用,并在一个dojo.DeferredList回调函数中处理返回的结果。
- // 从twitter获取tweets信息
- refresh: function() {
- // 更新刷新按钮图标
- this.refreshButton.iconNode.src = this.iconLoading;
- // 按钮被"按下"
- this.refreshButton.select();
- // 为每一个帐号向defs添加一个deferred
- var defs = [], accounts = tweetview.ACCOUNTS;
- for(var account in accounts) {
- // 如果账户处于启用状态
- if(accounts[account].enabled) {
- // 获取 tweets!
- defs.push(dojo.io.script.get({
- callbackParamName: "callback",
- preventCache: true,
- timeout: 3000,
- // "substitute" 来自 _ViewMixin
- url: this.substitute(this.serviceUrl, { account: account, since_id: accounts[account].since || 1 })
- }));
- }
- }
- // 创建一个 dojo.Deferredlist 来处理所有返回的tweets
- // 添加 this.onTweetsReceived 作为回调函数
- new dojo.DeferredList(defs).then(dojo.hitch(this, this.onTweetsReceived));
- },
!请确保为dojo.io.script.get方法添加timeout参数,这样你的错误处理回调函数会被正确地触发。
关于refresh方法的一些注释:
- 刷新图标在该方法被调用时切换成旋转的GIF图片。
- 某个账户的tweets信息只有在该账户被启用时才会被获取。
- 注意账户的“since”属性被添加到URL中。该属性在第一次获取数据之后被初始化,以便之后同样的tweets信息不会被重复获取。
TweetView onTweetsReceived 和 sortTweets
onTweetsReceived方法获取所有通过dojo.io.script.calls创建的dojo.Deferreds的返回结果。现在我们已经有了每个账户中的tweets信息,但我们还有几个问题:
- 这些tweets根据用户分类——我们需要将它们加入到同一个数组中。这是一个很容易解决的问题。
- 所有的tweets在融合到一起之后,需要根据日期排序。
在我们将tweets输出到控件中的列表前,我们必须先对其进行排序。其中第一部分是将他们放入同一个数组中:
- // 将tweets放入一个数组,并根据日期排序
- sortTweets: function(deflist) {
- // 为我们的tweets创建一个数组
- var allTweets = [];
- // 为每一个def进行处理
- dojo.forEach(deflist, function(def) {
- // 定义要处理的属性
- // Tweet是“def[l]”, Mentions 是 def[1].results
- var tweets = (def[1].results ? def[1].results : def[1]);
- // 如果我们获取了一些数据
- if(tweets.length) {
- // 获取用户名并更新since属性
- var username = !tweets[0].user ? def[1].query.replace("%40","") : tweets[0].user.screen_name;
- // 更新该用户的since属性
- tweetview.ACCOUNTS[username].since = tweets[0].id_str;
- // 如果这是一个查询,我们需要为tweet添加用户名
- if(def[1].query) {
- dojo.forEach(tweets, function(tweet) { tweet.searchUser = username; });
- }
- // 组成一个大数组
- allTweets = allTweets.concat(tweets);
- }
- },this);
!注意def[1]的处理、用户名的获取以及def.query的检查。查询API返回一个与user timeline API不同的JSON结构。我们可以为处理mentions信息创建一个不同的子类,但是在本应用程序中我们还不需要花时间这么做,我们使用TweetView来处理tweets和mentions。
现在所有的tweets都在一个大数组中了,接下来我们将把他们按时间排序,并将结果返回给onTweetsReceived方法。
- // 按时间排序
- allTweets.sort(function(a, b) {
- var atime = new Date(a.created_at),
- btime = new Date(b.created_at);
- // 普通的排序算法会返回b-a而不是a-b
- // 不过,我们想要把最近的时间放前面
- // 所以我们使用atime-btime
- return atime - btime;
- });
- // 返回tweets
- return allTweets;
所有的tweets排序完后,是时候停止刷新按钮的旋转状态,并将tweets传给updateContent方法来更新页面了:
- // 当所有内容从Twitter加载完毕后触发
- onTweetsReceived: function(rawTweetData) {
- // 将tweets排序
- tweetData = this.sortTweets(rawTweetData);
- // 设置刷新按钮的图标
- this.refreshButton.iconNode.src = this.iconImage;
- this.refreshButton.select(true);
- // tweets不为空
- if(tweetData.length) {
- // 更新内容
- this.updateContent(tweetData);
- }
- },
!Tweets根据时间排序,最新的排在最前面 ——这确保最新的tweets会被放在列表的最顶端。同时请注意,updateContent方法只有在哦我们确实获取了新tweets之后才会被调用。
TweetView updateContent
updateContent方法获取排序过的tweets,并为他们创建dojox.mobile.ListItem控件,然后将它们放到列表的顶端。在我们创建updateContent之前,我们需要一些工具方法来格式化tweet的时间戳和文字内容:
- // 为一个字符串添加恰当的tweet linkification
- formatTweet: function(tweetText) {
- return tweetText.
- replace(/(https?:/S+)/gi,'$1').
- replace(/(^|/s)@(/w+)/g,'$1@$2').
- replace(/(^|/s)#(/w+)/g,'$1#$2');
- },
- // 格式化时间戳
- formatTime: function(date) {
- // 获取当前时间
- var now = new Date();
- // 根据时间戳字符串创建一个日期对象
- var tweetDate = new Date(date);
- // 计算时间: 秒
- var secondsDifferent = Math.floor((now - tweetDate) / 1000);
- if(secondsDifferent < 60) {
- return secondsDifferent + " " + (this.l10n["field-second"]) + (secondsDifferent > 1 ? "s" : "");
- }
- // 计算时间: 分钟
- var minutesDifferent = Math.floor(secondsDifferent / 60);
- if(minutesDifferent < 60) {
- return minutesDifferent + " " + this.l10n["field-minute"] + (minutesDifferent > 1 ? "s" : "");
- }
- // 计算时间: 小时
- var hoursDifferent = Math.floor(minutesDifferent / 60);
- if(hoursDifferent < 24) {
- return hoursDifferent + " " + this.l10n["field-hour"] + (hoursDifferent > 1 ? "s" : "");
- }
- // 计算时间: 天数
- var daysDifferent = Math.floor(hoursDifferent / 24);
- return daysDifferent + " " + this.l10n["field-day"] + (daysDifferent > 1 ? "s" : "");
- },
有了上述方法之后,我们可以创建我们自己的updateContent方法了:
- // 在获取tweets之后触发
- updateContent: function(rawTweetData) {
- // 处理每一条tweet信息
- dojo.forEach(rawTweetData, function(tweet) {
- // Get the user's screen name
- var screenName = tweet.searchUser || tweet.user.screen_name;
- // 创建一个新的列表元素,并添加到列表顶部
- var item = new dojox.mobile.ListItem({
- "class": "tweetviewListItem user-" + screenName
- }).placeAt(this.listNode, "first");
- // 使用我们的tweet模板更新列表元素的内容
- item.containerNode.innerHTML = this.substitute(this.tweetTemplateString, {
- text: this.formatTweet(tweet.text),
- user: tweet.from_user || screenName,
- name: tweet.from_user || tweet.user.name,
- avatar: tweet.profile_image_url || tweet.user.profile_image_url,
- time: this.formatTime(tweet.created_at),
- created_at: tweet.created_at,
- id: tweet.id
- });
- },this);
- // 现在列表内容填充完毕,显示它
- this.showListNode(true);
- },
!注意,user-{screenName} CSS类被添加到每一个列表元素中。该CSS类将在下一个教程中将介绍的Settings视图中启用/禁用用户功能起到重要作用。
我们为每一条获取的tweet创建一个新的列表元素。在新的列表元素创建之后,我们又为其填充格式化过的文字内容和时间戳。最后列表元素被添加到列表的最顶端。
Tweets 和 Mentions 的功能完成了
现在我们已经讲完了TweetView的TweeView类的所有JavaScript内容!Tweets和Mentions视图现在可以读取tweets并进行刷新!让我们回顾下我们是怎么做的吧:
- 创建一个被所有TweetView控件使用的_ViewMixin。
- 创建一个tweetview命名空间下的TweetView类
- 更新Tweets和Mentions的HTML页面来使用新的TweetView控件
- 在tweetview命名控件下设置一个ACCOUNTS对象来使得他们对所有控件是可见的
- 为TweetView创建一系列方法来获取tweets、处理获得的数据、格式化并显示数据
!现在所有的功能已经完成,回过头再来看看startup方法。现在所有的方法都已经实现,应该不难理解整个控件的生命周期中发生了什么。
现在只剩下最后一步了:修改列表的样式。
修改TweetView和其中子控件的样式
为列表修改样式是挺有意思的部分,可能也是最简单的。到目前为止,ListItem控件的内容只能使用单行文字,所以我们需要在我们的TweetView.css样式表中改进这一点。
- .tweetviewHidden { /* 我们使用这个来隐藏节点 */
- display:none;
- }
- .tweetviewList li { /* 最小高度,确保用户头像可以显示 */
- min-height:55px;
- padding-top:8px;
- }
- .tweetviewListItem { /* 内容可以分多行显示 */
- line-height:18px;
- height:auto;
- }
现在ListItem可以显示多行内容了,接下来我们为已经在tweet item模板中定义的CSS类添加具体内容:
- /* 在底部提供适当的留白 */
- .mblScrollableViewContainer {
- padding-bottom:40px !important;
- }
- /* 用户头像 */
- .tweetviewAvatar {
- float:left;
- }
- /* tweets内容区域 */
- .tweetviewContent {
- padding-left:60px;
- }
- /* 用户名样式 */
- .tweetviewUser {
- font-size:80%;
- }
- /* tweet文字样式 */
- .tweetviewText {
- font-size:70%;
- font-weight:normal;
- padding-right:50px;
- padding-bottom:10px;
- }
- /* tweet time */
- .tweetviewTime {
- float:right;
- color:#777;
- font-size:65%;
- font-weight:normal;
- padding-right:10px;
- }
- /* clears floats at the end of the list item */
- .tweetviewClear {
- clear:both;
- }
一切都搞定啦!一个和效果图一样漂亮的tweet列表!
查看示例
最难的部分已经过去!
和本文开始的时候保证的一样,Tweets和Mentions视图是本系列教程中最复杂的。下一篇教程中,我们将着重介绍Setting视图,该视图用来启用/禁用指定账户。到那时,回顾下本教程中的示例和代码,确保你没有错过每一步!