SD2C的第一天晚上,我在「开放平台」和「PPT制作秘诀」两者之间犹豫,最后选择了去听蔡学镛的「尼古丁+咖啡因...不瞌睡的PPT制作秘诀」,原因是我觉得如果蔡学镛能教别人写不瞌睡PPT,那么他自己的PPT至少也应该是不瞌睡的。事后证明我的选择是没错的——别人告诉我「开放平台」论坛成了平台和开发人员互相责骂对方的战场。
蔡学镛把制作PPT比喻为制作生鱼片,要经过选材、处理、装饰这三步。在选材方面,一个PPT讲的内容最好是相当于一章书的内容,时间不能超过90分钟,而讲述的事情不能超过7件。接下来的处理,要注意做好破题,引起受众的兴趣,并且每一小节的结束时都要有总结。一页PPT所表达的意思,有可能相当于一个词、一句话、一段话,我们应该多用表达一句话的PPT页面。最后的装饰会需要用到不少图片,通过使用正确的关键字联想策略,往往都能搜索到你想要的图片。
SD2C的第二天,我原计划要听听周爱民的「实践者思想」和蔡学镛的「DSL设计与实践」的,结果上午听完Gary Bennett的「iPhone SDK简介」之后就被吸引住了,下午的时间都放在iPhone相关课程上了。这说明了,课程的首要条件还是要能把人吸引住,不会让人想要睡觉,也不会让人想要换个课室看看。在满足这个前提条件后,才有资格来讨论内容的知识性如何。我在听了几节iPhone开发的课程后,又开始对iPhone开发充满兴趣了。
晚上的「程序员软技能」论坛是我最期待的,因为这个话题可以讲得非常有震撼性。讲得好的话,这可以让很多程序员发现自身的unknown unknown(不知道自己不知道的事情)来,把这变为known unknown(知道自己不知道的事情)之后,只要努力了就能获得改善。结果主持人刘江自己戴上了嘉宾的帽子就开始不停地说,搞到台下有人举手说「在刚才的30分钟里面您已经讲了20分钟喇」。同时,估计因为刘江是做图书的,所以花了不少时间在读书的话题上,此外再提到了一些企业文化和沟通技能之类的东西,顺便还鄙视了一轮内地的高等教育连幼儿教育都不如(来自台湾清华的蔡学镛很无辜地无法参与到这个话题中来)。可能是我原本对这个话题的期望太高,所以我对这个论坛挺失望的。将来有空我会专门写文章讨论一下程序员常见的unknown unknown。
最后一天的SD2C我没有参加,因为已经没什么让我想要听的了,所以我选择了去参加Windows 7 Community Launch。有微软社区的人说SD2C是「傻蛋2.0大会」,选择了Visual Studio 2010 Beta 2 Launch、Windows 7 Launch、Windows 7 Community Launch这三天来举行,分明就是跟微软过不去。说实在的,我也觉得今年的SD2C日期选择失策了,明知道Windows 7对Microsoft有多么重要,也明知道发布日期是哪一天,还偏要选择一个冲突的日期。希望明年的SD2C能够选择一个更好的时间吧,相信会务今年已经通过举手投票搞清楚大家的喜好了——最好是在星期二三四开会,因为星期一和星期五工作压力小,同时星期六日是用来休息的。
2009年10月25日星期日
2009年10月22日星期四
SD2C 2009 (Part 1 - Keynote & Meal)
一年一度的SD2C又来了,今年的时间由去年的两天半扩充到三天,第一天只设主会场,全部都是keynote。
跟去年的情况类似的是,keynote环节基本上就是赞助商专场,每个赞助商都来从技术的角度说一下自己当前最重视的市场。例如说Microsoft开始重视Team Foundation Server的市场占有率了,于是就在Visual Studio 2010的keynote上介绍TFS的特性,所占比例比VS2010自身还要多。当然,keynote里面也有一些是有启发性的,例如IBM讲的大型企业中的敏捷实践,这就属于我认为比较好的keynote。
由于今年的话题集中在云和移动上面,所以keynote大多与这两个领域相关。能够上去讲keynote的人,当然是要有一定水平的,不能讲一些很肤浅的内容,但如果因此就把keynote弄得很高深或者很抽象又弄巧成拙了。高深的内容固然能够表现出演讲者很有水平,问题是这样一个面向上千受众的活动,有多少人能够对如此多个keynote的话题都有深入了解呢?就算你对其中一个话题很有了解,觉得讲得如此有高度很对你胃口,但你也总有n个其它不了解的话题吧,那些话题你听起来也会觉得是云里雾里的。
在我看来,优秀的keynote应该是这样子的——它能够阐述一个很新颖的观点,或者提供一种全新的视觉,使得无论你原本对这个话题了解多少,你都会被震撼到——这个事情我有所了解,但为什么我之前就没有尝试过从这个角度来思考呢?这其实也就是Punch Party的优势,无论受众具备怎么样的背景,都能感受到全新观点带来的冲击力。如果能够打到这样的效果,我并不在意一个keynote是否顺便在推销某个企业或者产品。
接下来要说说吃饭的问题,但在此之前让我先说说我对MVP Program的一些观点,这纯粹是个人观点,也不一定符合事实。我觉得,就拿MVP Summit这样一个全球峰会来说,每年要接待来自全球各地的几千人,为他们提供四天的食宿,平均分摊到每个与会者身上的费用肯定是不少的。如果MVP Summit的作用仅仅在于让大家聊聊技术,我想Microsoft作为一家商业公司是不会愿意花这笔钱的,就算Microsoft想要这样做,股东们也不会同意。因此,MVP Program必然是能够产生价值的,并且分摊下来每位MVP为Microsoft创造的价值应该大于Microsoft为他所作的花费。这些价值可能包括,MVP为用户解答问题,为客户带来技术培训,也就相当于节省了Microsoft的服务成本和市场费用。同时,MVP也会积极反馈关于产品的各种问题,这也属于创造价值的部分。
回到SD2C的事情上来,CSDN其实对热心贡献的用户非常好,例如说提供SD2C赠票。但是CSDN从来都对此事保持非常低调的姿态,生怕激怒那些付钱买票的人,同时也怕更多人去找他们要赠票。我的疑问是,为什么这些对热心用户的正面激励不能好像MVP Program那样光明正大的做呢?如果一位热心用户为CSDN创造的价值能够大于SD2C门票的成本价,那送他一张赠票肯定还是赚了的。
为什么我要说这件事?因为今年的赠票不包括背包和晚餐。不包括背包我觉得可以理解,金融危机嘛,能够让我免费来参加SD2C我已经非常满足了,没有背包也没所谓。但是不包括晚餐就非常不人性化了——这不是逼拿赠票的人不参加晚上的活动提前离场吗?况且,班车还要等晚上活动结束后才有,傍晚离开还是挺不方便的。于是我就在会场的做啥大屏幕发起讨论,问问没晚餐票的人准备如何吃晚餐——我的本意只是想组织落单的赠票使用者一起吃饭,反正拿赠票的大多是CSDN活跃用户,大家也都是认识的。同时我考虑到CSDN不希望公开有赠票这件事,我一开始也刻意回避了这个话题,后来有人公开讨论了我也就跟着讨论了。
结果是什么呢?CSDN会务组单独给我一张晚餐票,并告诉我原因是蒋涛看到做啥大屏幕的内容了。这就让我感觉到很奇怪了,两天的晚餐票值多少钱呢?值得把事情搞到这样子吗?CSDN自己是做媒体的,应该十分清楚媒体的影响力。赠票邀请来的人,都是技术社区上最活跃的人,他们的博客都是具有相当影响力的媒体,给这些人留下这样一个负面印象,CSDN损失的是多少价值呢?这是否已经超过了两张晚餐票的价钱呢?我相信以蒋涛在CSDN这么多年来做媒体的经验,应该在这件事被搬上大屏幕讨论之前就把帐算清楚了吧。
最后一点,也就是这样做是否厚道。如果CSDN以媒体身份受邀参加这样一场大会,本来是希望借此机会向自己的读者传递更多更好的技术资讯的,然而却受到大会主办方不公平的对待,搞到连饭都没得吃,你们会喜欢被这样对待吗?如果你们自己也不喜欢被这样对待,那就在如此待人之前先考虑清楚。
跟去年的情况类似的是,keynote环节基本上就是赞助商专场,每个赞助商都来从技术的角度说一下自己当前最重视的市场。例如说Microsoft开始重视Team Foundation Server的市场占有率了,于是就在Visual Studio 2010的keynote上介绍TFS的特性,所占比例比VS2010自身还要多。当然,keynote里面也有一些是有启发性的,例如IBM讲的大型企业中的敏捷实践,这就属于我认为比较好的keynote。
由于今年的话题集中在云和移动上面,所以keynote大多与这两个领域相关。能够上去讲keynote的人,当然是要有一定水平的,不能讲一些很肤浅的内容,但如果因此就把keynote弄得很高深或者很抽象又弄巧成拙了。高深的内容固然能够表现出演讲者很有水平,问题是这样一个面向上千受众的活动,有多少人能够对如此多个keynote的话题都有深入了解呢?就算你对其中一个话题很有了解,觉得讲得如此有高度很对你胃口,但你也总有n个其它不了解的话题吧,那些话题你听起来也会觉得是云里雾里的。
在我看来,优秀的keynote应该是这样子的——它能够阐述一个很新颖的观点,或者提供一种全新的视觉,使得无论你原本对这个话题了解多少,你都会被震撼到——这个事情我有所了解,但为什么我之前就没有尝试过从这个角度来思考呢?这其实也就是Punch Party的优势,无论受众具备怎么样的背景,都能感受到全新观点带来的冲击力。如果能够打到这样的效果,我并不在意一个keynote是否顺便在推销某个企业或者产品。
接下来要说说吃饭的问题,但在此之前让我先说说我对MVP Program的一些观点,这纯粹是个人观点,也不一定符合事实。我觉得,就拿MVP Summit这样一个全球峰会来说,每年要接待来自全球各地的几千人,为他们提供四天的食宿,平均分摊到每个与会者身上的费用肯定是不少的。如果MVP Summit的作用仅仅在于让大家聊聊技术,我想Microsoft作为一家商业公司是不会愿意花这笔钱的,就算Microsoft想要这样做,股东们也不会同意。因此,MVP Program必然是能够产生价值的,并且分摊下来每位MVP为Microsoft创造的价值应该大于Microsoft为他所作的花费。这些价值可能包括,MVP为用户解答问题,为客户带来技术培训,也就相当于节省了Microsoft的服务成本和市场费用。同时,MVP也会积极反馈关于产品的各种问题,这也属于创造价值的部分。
回到SD2C的事情上来,CSDN其实对热心贡献的用户非常好,例如说提供SD2C赠票。但是CSDN从来都对此事保持非常低调的姿态,生怕激怒那些付钱买票的人,同时也怕更多人去找他们要赠票。我的疑问是,为什么这些对热心用户的正面激励不能好像MVP Program那样光明正大的做呢?如果一位热心用户为CSDN创造的价值能够大于SD2C门票的成本价,那送他一张赠票肯定还是赚了的。
为什么我要说这件事?因为今年的赠票不包括背包和晚餐。不包括背包我觉得可以理解,金融危机嘛,能够让我免费来参加SD2C我已经非常满足了,没有背包也没所谓。但是不包括晚餐就非常不人性化了——这不是逼拿赠票的人不参加晚上的活动提前离场吗?况且,班车还要等晚上活动结束后才有,傍晚离开还是挺不方便的。于是我就在会场的做啥大屏幕发起讨论,问问没晚餐票的人准备如何吃晚餐——我的本意只是想组织落单的赠票使用者一起吃饭,反正拿赠票的大多是CSDN活跃用户,大家也都是认识的。同时我考虑到CSDN不希望公开有赠票这件事,我一开始也刻意回避了这个话题,后来有人公开讨论了我也就跟着讨论了。
结果是什么呢?CSDN会务组单独给我一张晚餐票,并告诉我原因是蒋涛看到做啥大屏幕的内容了。这就让我感觉到很奇怪了,两天的晚餐票值多少钱呢?值得把事情搞到这样子吗?CSDN自己是做媒体的,应该十分清楚媒体的影响力。赠票邀请来的人,都是技术社区上最活跃的人,他们的博客都是具有相当影响力的媒体,给这些人留下这样一个负面印象,CSDN损失的是多少价值呢?这是否已经超过了两张晚餐票的价钱呢?我相信以蒋涛在CSDN这么多年来做媒体的经验,应该在这件事被搬上大屏幕讨论之前就把帐算清楚了吧。
最后一点,也就是这样做是否厚道。如果CSDN以媒体身份受邀参加这样一场大会,本来是希望借此机会向自己的读者传递更多更好的技术资讯的,然而却受到大会主办方不公平的对待,搞到连饭都没得吃,你们会喜欢被这样对待吗?如果你们自己也不喜欢被这样对待,那就在如此待人之前先考虑清楚。
2009年10月16日星期五
国际长途比国内市话便宜
众所周知,在国外买国际长途电话卡打到中国,比国内市话还要便宜。这说明了国内话费之高完全是垄断造成的,其实根本就不值这样的价格,因此我们能够打国际长途就打国际长途吧,尽量别再为国内垄断行业支付额外的话费了。
在国内要通过国际长途拨打国内电话,通过VoIP就行了,Skype和Google Voice都可以,它们的价格都很便宜。就拿移动话费来说,非漫游非长途要¥0.29,漫游或/且长途要¥0.39,但是Skype只要$0.021(每次接通需要$0.039连接费),而Google Voice只要$0.02(无需连接费)。
如何使用Skype拨打国内电话呢?你可以用信用卡通过PayPal购买Skype credit,然后就可以用任何Skype客户端拨打国内号码了。对我来说最方便的是,iPhone上面有Skype客户端,只要在有Wi-Fi的地方我都可以用它来打电话。当然,Skype通话质量会受带宽影响,如果你所在的区域信号不好或者带宽有限,通话质量就会下降。
在没有Wi-Fi的时候怎么办呢?通过Google Voice的回拨服务可以解决问题,当然前提是要通过网络(GPRS/EDGE或3G)访问Google Voice。Google Voice拨号可以拨通对方的国内电话,但回拨只能拨美国电话,怎么办呢?我们可以通过购买Skype Online Number解决这个问题。这时候Google Voice和Skype会同时拨通双方电话,然后建立双方通话,话费也就是$0.041而已,还是比移动话费稍微便宜一点。
可能很多人会怀疑,花那么多美元来拨打国际长途,真的比国内话费要便宜吗?这就要看你的具体使用方式和套餐组合方式喇。我曾经因为每天漫游加长途贡献了不少人民币给中国移动,换成Skype后至少省了一半的钱,所以从此我就决定改为贡献美元给Skype好了。如果你并不介意花$10购买Skype credit试试,可以实际使用一段时间后对比一下烧钱速度后再决定。
在国内要通过国际长途拨打国内电话,通过VoIP就行了,Skype和Google Voice都可以,它们的价格都很便宜。就拿移动话费来说,非漫游非长途要¥0.29,漫游或/且长途要¥0.39,但是Skype只要$0.021(每次接通需要$0.039连接费),而Google Voice只要$0.02(无需连接费)。
如何使用Skype拨打国内电话呢?你可以用信用卡通过PayPal购买Skype credit,然后就可以用任何Skype客户端拨打国内号码了。对我来说最方便的是,iPhone上面有Skype客户端,只要在有Wi-Fi的地方我都可以用它来打电话。当然,Skype通话质量会受带宽影响,如果你所在的区域信号不好或者带宽有限,通话质量就会下降。
在没有Wi-Fi的时候怎么办呢?通过Google Voice的回拨服务可以解决问题,当然前提是要通过网络(GPRS/EDGE或3G)访问Google Voice。Google Voice拨号可以拨通对方的国内电话,但回拨只能拨美国电话,怎么办呢?我们可以通过购买Skype Online Number解决这个问题。这时候Google Voice和Skype会同时拨通双方电话,然后建立双方通话,话费也就是$0.041而已,还是比移动话费稍微便宜一点。
可能很多人会怀疑,花那么多美元来拨打国际长途,真的比国内话费要便宜吗?这就要看你的具体使用方式和套餐组合方式喇。我曾经因为每天漫游加长途贡献了不少人民币给中国移动,换成Skype后至少省了一半的钱,所以从此我就决定改为贡献美元给Skype好了。如果你并不介意花$10购买Skype credit试试,可以实际使用一段时间后对比一下烧钱速度后再决定。
2009年9月17日星期四
你的网站「被兼容」了吗?
一般情况下,我们只会讨论我们的网站如何主动兼容某某浏览器,被动地等待浏览器来兼容我们的网站是不切实际的幻想——哪个浏览器会那么伟大,原意主动为一个不兼容的网站而作出改变呢?IE8就是这样一个伟大的浏览器,Microsoft就是一家这样伟大的企业。
故事是这样的,我们有一小段JavaScript依赖于userAgent属性,同样是用IE8进行浏览,在测试环境上userAgent显示为MSIE 7.0,而在生产环境上userAgent显示为MSIE 8.0。为什么会这样呢?打开Developer Toolbar后,发现原来是Browser Mode这个开关在搞鬼——当Browser Mode是Internet Explorer 8的时候,userAgent就是MSIE 8.0;当Browser Mode是Internet Explorer 8 Compatibility View(兼容性视图)或Internet Explorer 7的时候,userAgent就是MSIE 7.0了。
接下来的问题是,我们并没有刻意去拨动这个开关啊,两个相同的页面怎么在不同的环境中默认显示为不同的Browser Mode呢?我的猜想是,这是由于域名不同而引起的——Microsoft自己维护着一个Compatibility View List,当访问该List中的站点时,IE8会自动启用Compatibility View,也就是将Browser Mode切换到Internet Explorer 8 Compatibility View。接着我在地址栏输入以下地址,检查了一下我本地最近更新的List:
事实表明,我们测试用的baidu.com域名确实在上述List中,但部署到baidu.jp后也就脱离了该List。这就很好地解释了我们遇到问题,同时也提醒我们域名已经成为了IE8测试中不可避免的一个紧耦合因素。在过去,我们可以简单地认为,部署在不同URL的相同页面在同一款浏览器中显示出来总是一样的。但现在我们必须修正这句话了,仅当不同URL都基于同一个域名时上述命题仍然成立。
通过这个案例,希望能让大家了解到在开发与测试过程中保持域名一致的重要性。如果你开发的页面要部署到example.com,你最好在develop.example.com上开发,在test.example.com上测试,然后再部署。如果你需要在本机进行开发测试,也要通过改hosts模拟一个localhost.example.com来进行测试与调试,以确保代码在最终部署后能执行在相同的环境下。
故事是这样的,我们有一小段JavaScript依赖于userAgent属性,同样是用IE8进行浏览,在测试环境上userAgent显示为MSIE 7.0,而在生产环境上userAgent显示为MSIE 8.0。为什么会这样呢?打开Developer Toolbar后,发现原来是Browser Mode这个开关在搞鬼——当Browser Mode是Internet Explorer 8的时候,userAgent就是MSIE 8.0;当Browser Mode是Internet Explorer 8 Compatibility View(兼容性视图)或Internet Explorer 7的时候,userAgent就是MSIE 7.0了。
接下来的问题是,我们并没有刻意去拨动这个开关啊,两个相同的页面怎么在不同的环境中默认显示为不同的Browser Mode呢?我的猜想是,这是由于域名不同而引起的——Microsoft自己维护着一个Compatibility View List,当访问该List中的站点时,IE8会自动启用Compatibility View,也就是将Browser Mode切换到Internet Explorer 8 Compatibility View。接着我在地址栏输入以下地址,检查了一下我本地最近更新的List:
res://iecompat.dll/iecompatdata.xml
事实表明,我们测试用的baidu.com域名确实在上述List中,但部署到baidu.jp后也就脱离了该List。这就很好地解释了我们遇到问题,同时也提醒我们域名已经成为了IE8测试中不可避免的一个紧耦合因素。在过去,我们可以简单地认为,部署在不同URL的相同页面在同一款浏览器中显示出来总是一样的。但现在我们必须修正这句话了,仅当不同URL都基于同一个域名时上述命题仍然成立。
通过这个案例,希望能让大家了解到在开发与测试过程中保持域名一致的重要性。如果你开发的页面要部署到example.com,你最好在develop.example.com上开发,在test.example.com上测试,然后再部署。如果你需要在本机进行开发测试,也要通过改hosts模拟一个localhost.example.com来进行测试与调试,以确保代码在最终部署后能执行在相同的环境下。
2009年8月23日星期日
Twitter Suspension
我的Twitter帐号,也就是@CatChen,经历了一次长达半个月的suspension。

8月5日早上醒来,发现我的Tweetie不能再更新,打开Web看看,发现如下提示:
这时候我觉得很无辜,竟然被误判为发垃圾信息的用户了。当然,这可能和最近Twitter系统不稳定有关,因为有些人的following被清空了,自然会导致另外一些人的followers数量暴跌,由此触发垃圾用户判别标准也是有可能的。无论如何,我只能按照它给出的Suspended Account帮助链接寻求帮助了。
打开帮助页后,发现了一条令人极度无奈的信息——“帐号有可能要被封禁至少30天以备研究之用”。你Twitter要研究和改进反垃圾信息策略可以啊,但你不应该为了自己的研究就封禁我的帐号30天吧!不过我还是先提交请求好了,反正现在也没什么更好的解决方案了。
第一次在Web上提交请求,系统发了一封自动回复给我,我大概看了一下,就存档了。结果请求第二天就关闭了,问题没有得到解决。第二次提交请求,质询为什么请求关闭了但封禁没结束,系统再发了一封同样的自动回复给我,我仔细阅读后才发现,信的末端说明要回复此信解释为什么我觉得这个封禁是错误的。回信后,系统指派了人负责跟踪这个问题,但一直没有任何进展。
后来我在Twitter上看到有人说,要联系suspended@twitter.com,我就再发信去质询进度,这相当于创建了第三个请求。系统继续发回自动回复,我又继续回信解释,接着问题第二天就解决了。虽然我并不能确定,是不是联系suspended@twitter.com确实帮助我快速解决了这个问题,但我建议不幸被封禁的人都应该试一试这个方法,然后告诉我这是不是一个有效的方法。
最后,这次事件引起了我的一个想法——Twitter如此错误封禁我的帐号半个月,和GFW随意封禁一些无关紧要的网站随后又解封,有什么本质上的区别吗?对我造成的影响其实是没有区别的,这两者都只会提高我日常生活的成本。
一直以来,我们依赖于网络从不同的网站获取信息,而GFW有能力随时终止网络服务的能力,所以GFW让人觉得讨厌。最近,Twitter也成为了我们获取信息必不可缺的一个途径,它自身同样具备随时终止服务的能力,因此Twitter并不可爱。与Twitter类似的其它核心服务也如此,如果你的Gmail帐号有一天被封禁了,你使用的所有Google服务随之而终止,Google无论如何都坚持它封禁你的帐号是没有违反服务条款的,这时候Google本质上和GFW还有任何区别吗?
8月5日早上醒来,发现我的Tweetie不能再更新,打开Web看看,发现如下提示:
This account is currently suspended and is being investigated due to strange activity. If we have suspended your account mistakenly, please let us know. See Suspended Accounts for more information.
这时候我觉得很无辜,竟然被误判为发垃圾信息的用户了。当然,这可能和最近Twitter系统不稳定有关,因为有些人的following被清空了,自然会导致另外一些人的followers数量暴跌,由此触发垃圾用户判别标准也是有可能的。无论如何,我只能按照它给出的Suspended Account帮助链接寻求帮助了。
打开帮助页后,发现了一条令人极度无奈的信息——“帐号有可能要被封禁至少30天以备研究之用”。你Twitter要研究和改进反垃圾信息策略可以啊,但你不应该为了自己的研究就封禁我的帐号30天吧!不过我还是先提交请求好了,反正现在也没什么更好的解决方案了。
第一次在Web上提交请求,系统发了一封自动回复给我,我大概看了一下,就存档了。结果请求第二天就关闭了,问题没有得到解决。第二次提交请求,质询为什么请求关闭了但封禁没结束,系统再发了一封同样的自动回复给我,我仔细阅读后才发现,信的末端说明要回复此信解释为什么我觉得这个封禁是错误的。回信后,系统指派了人负责跟踪这个问题,但一直没有任何进展。
后来我在Twitter上看到有人说,要联系suspended@twitter.com,我就再发信去质询进度,这相当于创建了第三个请求。系统继续发回自动回复,我又继续回信解释,接着问题第二天就解决了。虽然我并不能确定,是不是联系suspended@twitter.com确实帮助我快速解决了这个问题,但我建议不幸被封禁的人都应该试一试这个方法,然后告诉我这是不是一个有效的方法。
最后,这次事件引起了我的一个想法——Twitter如此错误封禁我的帐号半个月,和GFW随意封禁一些无关紧要的网站随后又解封,有什么本质上的区别吗?对我造成的影响其实是没有区别的,这两者都只会提高我日常生活的成本。
一直以来,我们依赖于网络从不同的网站获取信息,而GFW有能力随时终止网络服务的能力,所以GFW让人觉得讨厌。最近,Twitter也成为了我们获取信息必不可缺的一个途径,它自身同样具备随时终止服务的能力,因此Twitter并不可爱。与Twitter类似的其它核心服务也如此,如果你的Gmail帐号有一天被封禁了,你使用的所有Google服务随之而终止,Google无论如何都坚持它封禁你的帐号是没有违反服务条款的,这时候Google本质上和GFW还有任何区别吗?
2009年8月16日星期日
jQuery is DSL (Part 2 - jQuery)
jQuery的Internal DSL形式
在上一篇文章里面,我们了解到了Internal DSL的具体形式,形如:/* Method Chaining */
computer()
.processor()
.cores(2)
.i386()
.disk()
.size(150)
.disk()
.size(75)
.speed(7200)
.sata()
.end();
然后我们在看看一段典型的jQuery代码:
$("ul#contacts li.item")
.find("span.name")
.click(function(e) { $(e.target).siblings(".more").toggle(); })
.end()
.find("input.delete")
.click(function(e) { $(e.target).parents(".item").remove(); })
.end()
.find("div.more")
.hide()
.end();
从结构上来说,是不是跟上面那一段Internal DSL的例子很相似?就算我们不看对应的HTML,我们也能猜到这段jQuery代码的含义:
- 遍历
<ul id="contacts">
中的每一个<li class="item">
(这看起来是个联系人列表)- 对于里面的
<span class="name">
- 绑定
click
事件,操作是显示/隐藏class="more"
兄弟节点
(这是估计联系人姓名,点击后切换详细信息的显示/隐藏)
- 绑定
- 对于里面的
<input class="delete">
- 绑定
click
事件,操作是把class="item"
父节点删除
(这应该是用来删除联系人的)
- 绑定
- 对于里面的
<div class="more">
- 隐藏这个
div
(默认隐藏详细信息?)
- 隐藏这个
- 对于里面的
$$("ul#contacts li.item span.name")
.invoke("observe", "click",
function(e) { $(e.target).next(".more").toggle(); });
$$("ul#contacts li.item input.delete")
.invoke("observe", "click",
function(e) { $(e.target).up(".item").remove(); });
$$("ul#contacts li.item div.more")
.invoke("hide");
这是我用Prototype所能写出的最贴近Internal DSL的形式了。(如果你能够写出一个更自然的版本,欢迎分享。)在Prototype里面,能够返回一组元素的操作就只有
$$()
,并且它只能作用于全局,缺乏jQuery中find()
或者filter()
的功能,所以这一组描述联系人列表行为的语句无法组合在一起,必须逐一定义每类元素的行为。此外,此例子中每类元素都仅仅指定了一个行为,因此Prototype的invoke()
写法看起来还是和jQuery的click()
写法很相近的。但如果一类元素拥有多个行为,Prototype的invoke()
就不能好像jQuery那样链式调用下去了,必须每一个行为重头写一个$$()
,或者把invoke()
改成each()
加匿名函数。无论是那种做法,都只会降低代码的可读性。jQuery的语法分析器
我们都知道,Internal DSL的实现依赖于对语法分析器的封装,对Internal DSL的调用其实都是对语法分析器的调用,经过语法分析后再构造出对底层API的调用。例如jQuery当中的click()
,它依赖于当前的状态,也就是前面$()
筛选出来的节点集合,把click()
解释为要为这一组节点绑定DOM的click事件,最后再调用DOM API完成任务。在这个例子当中,DOM API相对jQuery API而言就是底层API了。jQuery可以说是挑了一个最容易实现的语法模型来做,永远只有一种token,因此永远也只有一种状态,这种状态当然也是永远有效的,你根本不可能给jQuery输入一个当前状态无效的token。jQuery的唯一状态就是一个jQuery对象实例,其本质就是一个元素集合。读入的token可能是各种针对这个元素集合的操作,但它的返回一定还是一个元素集合。这使得jQuery的语法分析器不会进入无效状态,也就无需判断无效状态,因此大大简化了Internal DSL实现中常见的一个难题。
小结
通过拿jQuery和Prototype做对比,我们可以发现jQuery用非常低的成本实现了Internal DSL,同时带来了Prototype所没有的明显好处。这可以看作是一个很好的范例——如果你需要描述的业务逻辑能够归纳为简单的语言模式,为此实现一门Internal DSL的性价比将会是很高的。你需要做的仅仅是为这个简单的语言模型实现一个简单的解释器,接着你就可以享受贴近人类思维模式的接口了。最后,如果你喜欢我的文章,可以考虑订阅我的博客:
2009年8月10日星期一
jQuery is DSL (Part 1 - DSL)
jQuery刚刚出来的时候,我没有太多关注它,觉得这不过是Yet Another JavaScript Library。早期的jQuery专注于DOM节点的筛选与操作,不提供众多的基础类扩展,更不提供UI组件,因此体积能够做到很小。然而,我实在看不出它和我熟悉的Prototype比有什么明显的优势——jQuery能做的各项独立的操作,Prototype都能做。
后来用jQuery的人越来越多,并且大家都爱用它的链式方法调用,甚至还把这种写法推广到其它语言中去。例如ASP.NET MVP Omar AL Zabir就把他的服务器端C#组件设计为支持链式方法调用的。这时候我才开始关注jQuery,并且逐渐喜欢上了链式方法调用的写法,也在我自己的JavaScript组件中实现类似的API(参考Async和Overload)。最后,我突然明白到,这其实就是一种Internal DSL嘛!
在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——
当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:
第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。
第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。
无论是哪一种写法,中间都必须写一个分析器层。就如同语法分析器需要使用状态机一样,Internal DSL的实现也必须内置一个状态机,以记录当前执行到什么状态了,并且接下来可以转移到哪些有效状态。
由于这不是一篇专门讲语法分析器和状态机实现的文章,所以我们把关注点保持在API层面就可以了,不深入讨论其实现细节和成本。我们知道链式方法调用能够实现Internal DSL就够了,至于jQuery是如何利用好这一点的,我们在下一篇文章里再作讨论。
如果你不希望错过下一篇文章,你可以考虑订阅我的博客:
后来用jQuery的人越来越多,并且大家都爱用它的链式方法调用,甚至还把这种写法推广到其它语言中去。例如ASP.NET MVP Omar AL Zabir就把他的服务器端C#组件设计为支持链式方法调用的。这时候我才开始关注jQuery,并且逐渐喜欢上了链式方法调用的写法,也在我自己的JavaScript组件中实现类似的API(参考Async和Overload)。最后,我突然明白到,这其实就是一种Internal DSL嘛!
在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——
什么是Internal DSL?
DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。例如说,我们有专门描述字符串特征的正则表达式,有专门描述数据库查询的SQL,有专门描述XML结构的DTD和XSD,甚至有专门描述XML变换的XSLT,这些都是DSL。当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:
- 使用通用语言描述该领域的问题(non-DSL)
- 发明一门全新的语言描述该领域的问题(External DSL)
- 在一门现成语言内实现针对领域问题的描述(Internal DSL)
I.DepositTo(new USD(200), CitiBank); /* C# */
I deposit 200USD to CitiBank /* E-DSL */
I.deposit(200.USD()).to(CitiBank); /* I-DSL */
deposit [something] to [somewhere]
?)。第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。
第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。
如何构造Internal DSL?
常见的两种Internal DSL实现方法是Method Chaining和Function Sequence。如果我们需要描述一台机器的硬件组成,两种实现方式的代码分别如下:/* Method Chaining */
computer()
.processor()
.cores(2)
.i386()
.disk()
.size(150)
.disk()
.size(75)
.speed(7200)
.sata()
.end();
/* Function Sequence */
computer();
processor();
cores(2);
processorType(i386);
disk();
diskSize(150);
disk();
diskSize(75);
diskSpeed(7200);
diskInterface(SATA);
无论是哪一种写法,中间都必须写一个分析器层。就如同语法分析器需要使用状态机一样,Internal DSL的实现也必须内置一个状态机,以记录当前执行到什么状态了,并且接下来可以转移到哪些有效状态。
由于这不是一篇专门讲语法分析器和状态机实现的文章,所以我们把关注点保持在API层面就可以了,不深入讨论其实现细节和成本。我们知道链式方法调用能够实现Internal DSL就够了,至于jQuery是如何利用好这一点的,我们在下一篇文章里再作讨论。
小结
在这篇文章里,我们了解了Internal DSL与External DSL之间的区别,同时还了解到实现Internal DSL的具体方式,这为我们接下来讨论jQuery的Internal DSL式接口做好了铺垫。在下一篇文章里,我们将深入地来看看为什么jQuery的接口要如此设计,它能为用户带来了怎样的便利,同时它自身的实现上又有什么优势。如果你不希望错过下一篇文章,你可以考虑订阅我的博客:
2009年7月2日星期四
让 JavaScript 轻松支持函数重载 (Part 2 - 实现)
在上一篇文章里,我们设计了一套能在JavaScript中描述函数重载的方法,这套方法依赖于一个叫做Overload的静态类,现在我们就来看看如何实现这个静态类。
我们允许用户输入一个字符串,表示某一个重载的签名。在用户调用函数时,我们需要拿着用户输入的参数实例去跟签名上的每一个参数类型作比较,因此我们需要先把这个字符串转换为类型数组。也就是说,字符串"Boolean, Number, Array"应该转换为数组[Boolean, Number, Array]。
在进行转换之前,我们先要考虑处理两个特殊类型,就是代表任意类型的"*",和代表任意数量的"..."。我们可以为它们定义两个专有的类型,以便在Overload内对它们做出特殊的兼容性处理:
在有了这两个类型之后,字符串"Boolean, *, ..."就会被正确转换为数组[Boolean, Overload.Any, Overload.More]。由于Overload.Any和Overload.More都是函数,自然也都可以看做类型。
在这两个类型得到正确处理后,我们就可以开始编写识别文本签名的转换函数了:
我想这段代码相当容易理解,因此就不再解释了。我第一次写这段代码时忘记写上面的第一个if了,导致空白签名字符串""无法被正确识别为空白签名数组[],幸好我的unit test代码第一时间发现了这个缺陷。看来编写unit test代码还是十分重要的。
为了作长度对比,我们需要在这个函数外对表示任何参数个数的"..."作一下特殊处理:
这一段代码将会整合到第一节的转换函数末端,以便matchSignature函数能够轻易判断出参数与签名是否匹配。在最理想的情况下,我们对输入参数类型匹配到0个或1个重载,这样我们很容易就判断出命中哪个重载了。但如果有2个或以上的重载匹配,那么我们就要从中挑选一个最优的了,这正是下一节要讨论的内容。
由于0和1这两个参数会被编译器理解为int类型,对于第1个重载它们都不用进行类型转换,都与第2个重载它们都要进行类型转换,因此第1个重载较优。
在第1个参数上,第1个重载较优;在第2个参数上,第2个重载较优。在这种情况下,任何一个重载都不优于另一个,找不到较优重载编译器就报错。
在第1个参数上,第1个重载优于第3个重载,于第2个重载无异;在第2个参数上,第1个重载优于第2个重载,于第3个重载无异。尽管第2个重载于第3个重载分不出个优劣来,但我们可以确定第1个重载比它们都要好,因此编译器选择了第1个重载。
假设我们有一个overloadComparator的比较函数,可以比较任意两个签名之间的优劣,我们需要对签名仅仅两两比较,以找出最优重载吗?事实上是不需要的,我们可以利用Array的sort方法,让它调用overloadComparator进行排序,排序后再验证前两名的关系就可以了——如果并列,则不命中任何一个;如果有先后之分,则命中第一个。
具体的overloadComparator代码就不在这里给出了,它依赖于另一个名为inheritanceComparator的比较函数来对比两个签名的参数类型哪一个更贴实际传入的参数类型,里面用到了一种比较巧妙的方法来判断两个类型是否为继承关系,以及是谁继承自谁。
识别文本签名
我们先来回顾一下上一篇文章中提到的Overload用例: var extend = Overload
.add("*, ...",
function(target) { })
.add("Boolean, *, ...",
function(deep, target) { });
我们允许用户输入一个字符串,表示某一个重载的签名。在用户调用函数时,我们需要拿着用户输入的参数实例去跟签名上的每一个参数类型作比较,因此我们需要先把这个字符串转换为类型数组。也就是说,字符串"Boolean, Number, Array"应该转换为数组[Boolean, Number, Array]。
在进行转换之前,我们先要考虑处理两个特殊类型,就是代表任意类型的"*",和代表任意数量的"..."。我们可以为它们定义两个专有的类型,以便在Overload内对它们做出特殊的兼容性处理:
Overload.Any = function() {};
Overload.More = function() {};
在有了这两个类型之后,字符串"Boolean, *, ..."就会被正确转换为数组[Boolean, Overload.Any, Overload.More]。由于Overload.Any和Overload.More都是函数,自然也都可以看做类型。
在这两个类型得到正确处理后,我们就可以开始编写识别文本签名的转换函数了:
if (signature.replace(/(^\s+|\s+$)/ig, "") == "") {
signature = [];
} else {
signature = signature.split(",");
for (var i = 0; i < signature.length; i++) {
var typeExpression =
signature[i].replace(/(^\s+|\s+$)/ig, "");
var type = null;
if (typeExpression == "*") {
type = Overload.Any;
} else if (typeExpression == "...") {
type = Overload.More;
} else {
type = eval("(" + typeExpression + ")");
}
signature[i] = type;
}
}
我想这段代码相当容易理解,因此就不再解释了。我第一次写这段代码时忘记写上面的第一个if了,导致空白签名字符串""无法被正确识别为空白签名数组[],幸好我的unit test代码第一时间发现了这个缺陷。看来编写unit test代码还是十分重要的。
匹配函数签名
在我们得到函数签名的类型数组后,我们就可以用它和输入参数的实例数组做匹配了,以此找出正确的重载。在讨论具体如何匹配函数签名以前,我们先来看看C#或VB.NET这样的语言是如何处理函数重载匹配的。一般语言进行函数重载匹配的流程都是这样子的:- 参数个数 - 参数个数不对的重载会被排除掉
- 参数类型 - 参数类型无法隐式转换为签名类型的会被排除掉
- 匹配个数 - 排除完毕后,剩下匹配的签名个数不同处理方法也不同
- 0个匹配 - 没有命中的匹配
- 1个匹配 - 这个就是命中的匹配
- 2个或以上的匹配 - 如果能在这些匹配中找出一个最佳匹配,那就命中最佳匹配;否则不命中任何匹配
var matchSignature = function(argumentsArray, signature) {
if (argumentsArray.length < signature.length) {
return false;
} else if (argumentsArray.length > signature.length
&& !signature.more) {
return false;
}
for (var i = 0; i < signature.length; i++) {
if (!(signature[i] == Overload.Any
|| argumentsArray[i] instanceof signature[i]
|| argumentsArray[i].constructor
== signature[i])) {
return false;
}
}
return true;
};
为了作长度对比,我们需要在这个函数外对表示任何参数个数的"..."作一下特殊处理:
if (signature[signature.length - 1] == Overload.More) {
signature.length = signature.length - 1;
signature.more = true;
}
这一段代码将会整合到第一节的转换函数末端,以便matchSignature函数能够轻易判断出参数与签名是否匹配。在最理想的情况下,我们对输入参数类型匹配到0个或1个重载,这样我们很容易就判断出命中哪个重载了。但如果有2个或以上的重载匹配,那么我们就要从中挑选一个最优的了,这正是下一节要讨论的内容。
处理多重匹配
关于C#是如何从多重匹配中选出较为匹配的重载,可以看C# Language Specification中的有关章节。我觉得通过三个简单的例子就能说明问题: long Sum(int x, int y) { return x + y; }
long Sum(long x, long y) { return x + y; }
Sum(0, 1);
由于0和1这两个参数会被编译器理解为int类型,对于第1个重载它们都不用进行类型转换,都与第2个重载它们都要进行类型转换,因此第1个重载较优。
long Sum(int x, long y) { return x + y; }
long Sum(long x, int y) { return x + y; }
Sum(0, 1);
在第1个参数上,第1个重载较优;在第2个参数上,第2个重载较优。在这种情况下,任何一个重载都不优于另一个,找不到较优重载编译器就报错。
long Sum(int x, int y) { return x + y; }
long Sum(int x, long y) { return x + y; }
long Sum(long x, int y) { return x + y; }
Sum(0, 1);
在第1个参数上,第1个重载优于第3个重载,于第2个重载无异;在第2个参数上,第1个重载优于第2个重载,于第3个重载无异。尽管第2个重载于第3个重载分不出个优劣来,但我们可以确定第1个重载比它们都要好,因此编译器选择了第1个重载。
假设我们有一个overloadComparator的比较函数,可以比较任意两个签名之间的优劣,我们需要对签名仅仅两两比较,以找出最优重载吗?事实上是不需要的,我们可以利用Array的sort方法,让它调用overloadComparator进行排序,排序后再验证前两名的关系就可以了——如果并列,则不命中任何一个;如果有先后之分,则命中第一个。
具体的overloadComparator代码就不在这里给出了,它依赖于另一个名为inheritanceComparator的比较函数来对比两个签名的参数类型哪一个更贴实际传入的参数类型,里面用到了一种比较巧妙的方法来判断两个类型是否为继承关系,以及是谁继承自谁。
小结
现在我们有了一个JavaScript的函数重载库,完整代码请看这里:函数重载库Overload。希望这个库能有效帮助大家提升JavaScript代码的可读性,降低大型Ajax项目的维护成本。如果大家希望将来继续读到类似的JavaScript开发模式相关的文章,不妨考虑订阅我的博客:2009年7月1日星期三
让 JavaScript 轻松支持函数重载 (Part 1 - 设计)
JavaScript支持重载吗?
JavaScript支持函数重载吗?可以说不支持,也可以说支持。说不支持,是因为JavaScript不能好像其它原生支持函数重载的语言一样,直接写多个同名函数,让编译器来判断某个调用对应的是哪一个重载。说支持,是因为JavaScript函数对参数列表不作任何限制,可以在函数内部模拟对函数重载的支持。实际上,在很多著名的开源库当中,我们都可以看到函数内部模拟重载支持的设计。例如说jQuery的jQuery.extend方法,就是通过参数类型判断出可选参数是否存在,如果不存在的话就对参数进行移位以确保后面的逻辑正确运行。我相信很多人在写JavaScript时也写过类似的代码,以求为功能丰富的函数提供一个(或多个)简单的调用入口。
不过做种做法一个根本的问题,那就是违反了DRY原则。每个支持重载的函数内部都多出来一段代码,用于根据参数个数和参数类型处理重载,这些代码暗含着重复的逻辑,写出来却又每一段都不一样。此外,这些代码要维护起来也不容易,因为阅读代码时你并不能一眼看出函数支持的几种重载方式是什么,要对重载做出维护自然也困难。
描述重载入口的DSL
我希望能够在JavaScript中以一种简单的方式来描述重载入口。最好就如同在其它语言中一样,使用函数签名来区分重载入口,因为我认为函数签名就是这方面最好的DSL。我假想中最符合JavaScript语法的重载入口描述DSL应该是这样子的:var sum = new Overload();
sum.add("Number, Number",
function(x, y) { return x + y; });
sum.add("Number, Number, Number",
function(x, y, z) { return x + y + z; });
在描述好重载入口与对应函数体后,对sum函数的调用应该是这样子的:
sum(1, 2);
sum(1, 2, 3);
上述代码在我看来非常清晰,也非常容易维护——你可以一眼看得出重载入口的签名,并且要修改或者增加重载入口都是很容易的事情。但是我们遇到了一个问题,那就是JavaScript里面的函数是不能new出来的,通过new Overload()获得的对象一定不能被调用,为此我们只能把Overload做成一个静态类,静态方法返回的是Function实例:
var sum = Overload
.add("Number, Number",
function(x, y) { return x + y; })
.add("Number, Number, Number",
function(x, y, z) { return x + y + z; });
必要的重载入口支持
想象一下,有哪些常见的JavaScript函数入口是用上述DSL无法描述的?我所知道的有两种:任意类型参数
假想我们要写一个each函数,对于Array就迭代它的下标,对于其它类型就迭代它的所有成员,这两个函数入口的参数列表如何声明?如果用C#,我们会如此描述两个函数入口:void Each(IEnumerable iterator) { }
void Each(object iterator) { }
然而在JavaScript当中,Object不是一切类型的基类,(100) instanceof Object的结果为false,所以我们不能用Object来指代任意类型,必须引入一个新的符号来指代任意类型。考虑到这个符号不应该与任何可能存在的类名冲突,所以我选择了用"*"来表示任意类型。上述C#代码对应的JavaScript应该是这样子的:
var each = Overload
.add("Array",
function(array) { })
.add("*",
function(object) { });
任意数量参数
在JavaScript的函数里面,要求支持任意数量参数是很常见的需求,相信使用率比C#里面的params关键字要多得多。在我们之前制定的规则当中,这也无法描述的,因此我们要引入一个不和类名冲突的符号来表示C#中的params。我选择了用"..."表示params,意思是这里出现任意多个参数都是可以接受的。让我们看看jQuery.extend的重载应该如何描述: var extend = Overload
.add("*, ...",
function(target) { })
.add("Boolean, *, ...",
function(deep, target) { });
小结
在这篇文章当中,我们尝试设计出一种适用于JavaScript且易读易维护的函数重载写法。在下一篇文章当中,我们将会尝试编写Overload类,以实现这一设计。如果你不希望错过的话,欢迎订阅:写个 JavaScript 异步调用框架 (Part 6 - 实例 & 模式)
我们用了5篇文章来讨论如何编写一个JavaScript异步调用框架(问题 & 场景、用例设计、代码实现、链式调用、链式实现),现在是时候让我们看一下在各种常见开发情景中如何使用它了。
在我所调用的服务器端API中,只需要GET和POST,且数据都为JSON,所以我就直接把jQuery提供的其它Ajax选项屏蔽掉了,并设置数据类型为JSON。在你的项目当中,也可以用类似的方式将Ajax封装为若干仅仅返回Async.Operation的方法,将jQuery提供的选项都封装在Ajax这一层内,不再向上层暴露这些选项。
假设我们有一个Friend对象,它的get方法用于返回单个好友对象,而getAll方法用于返回所有好友对象。于此对应的是两个服务器端API,friend接口会返回单个好友JSON,而friendlist接口会返回所有好友名称组成的JSON。
首先我们看看较为基础的get方法怎么写:
就这么简单?对的,假如服务器端API返回的JSON结构正好就是你要的好友对象结构的话。如果JSON结构和好友对象结构是异构的,或许你还要加点代码来把JSON映射为对象:
在这里,我们假设friendlist接口返回的JSON就是一个Array,在获取到这个Array后构造一个等长的异步调用队列,其中每一个调用的逻辑都是一样的——取出Array中首个好友的名称,用get方法获取对应的好友对象,再将好友对象放入另一个Array中。在调用队列的末端,我们再追加了一个调用,用于返回保存好友对象的Array。
在这个例子当中,我们没有利用调用队列会把上一个函数的结果传递给下一个函数的特性,不过也足够展示调用队列的用途了——让多个底层为Ajax请求的异步操作按照固定的顺序阻塞式执行。
由于底层异步函数返回的就是Async.Operation,你可以直接把它传递给next方法,也可以用匿名函数包装后传递给next方法,而匿名函数内部只需要一个return。
在原本next方法调用的匿名函数中手动加入setTimeout是一个办法,但为什么我们不写一个辅助函数来解决这类问题呢?让我们来写一个辅助方法并让它和Async.Operation无缝结合起来。
在有了这个辅助方法后,我们就可以在上述getAll方法中轻松实现在每个Ajax请求之间间隔500毫秒。在for循环内的加上对wait的调用就可以了。
最后,如果大家希望将来继续读到类似的JavaScript开发模式相关的文章,不妨考虑订阅我的博客:
封装Ajax
设计Async.Operation的最初目的就是解决Ajax调用需要传递callback参数的问题,为此我们先把Ajax请求封装为Async.Operation。我在这里使用的是jQuery,当然无论你用什么基础库,在使用Async.Operation时都可以做这种简单的封装。var Ajax = {};
Ajax.get = function(url, data) {
var operation = new Async.Operation();
$.get(url, data, function(result) {
operation.yield(result);
}, "json");
return operation;
};
Ajax.post = function(url, data) {
var operation = new Async.Operation();
$.post(url, data, function(result) {
operation.yield(result);
}, "json");
return operation;
};
在我所调用的服务器端API中,只需要GET和POST,且数据都为JSON,所以我就直接把jQuery提供的其它Ajax选项屏蔽掉了,并设置数据类型为JSON。在你的项目当中,也可以用类似的方式将Ajax封装为若干仅仅返回Async.Operation的方法,将jQuery提供的选项都封装在Ajax这一层内,不再向上层暴露这些选项。
调用Ajax
把Ajax封装好后,我们就可以开始专心写业务逻辑了。假设我们有一个Friend对象,它的get方法用于返回单个好友对象,而getAll方法用于返回所有好友对象。于此对应的是两个服务器端API,friend接口会返回单个好友JSON,而friendlist接口会返回所有好友名称组成的JSON。
首先我们看看较为基础的get方法怎么写:
function get(name) {
return Ajax.get("/friend",
"name=" + encodeURIComponent(name));
}
就这么简单?对的,假如服务器端API返回的JSON结构正好就是你要的好友对象结构的话。如果JSON结构和好友对象结构是异构的,或许你还要加点代码来把JSON映射为对象:
function get(name) {
var operation = new Async.Operation()
Ajax.get("/friend", "name=" + encodeURIComponent(name))
.addCallback(function(json) {
operation.yield(createFriendFromJson(json));
});
return operation;
}
Ajax队列
接下来我们要编写的是getAll方法。因为friendlist接口只返回好友名称列表,因此在取得这份列表后我们还要逐一调用get方法获取具体的好友对象。考虑到在同时进行多个friend接口调用可能触发服务器的防攻击策略,导致被关小黑屋一段时间,所以对friend接口的调用必须排队。function getAll(){
var operation = new Async.Operation();
var friends = [];
var chain = Async.chain();
Ajax.get("/friendlist", "")
.addCallback(function(json) {
for (var i = 0; i < json.length; i++) {
chain.next(function() {
return get(json.shift())
.addCallback(function(friend) {
friends.push(friend);
});
});
}
chain
.next(function() { operation.yield(friends); })
.go();
})
return operation;
}
在这里,我们假设friendlist接口返回的JSON就是一个Array,在获取到这个Array后构造一个等长的异步调用队列,其中每一个调用的逻辑都是一样的——取出Array中首个好友的名称,用get方法获取对应的好友对象,再将好友对象放入另一个Array中。在调用队列的末端,我们再追加了一个调用,用于返回保存好友对象的Array。
在这个例子当中,我们没有利用调用队列会把上一个函数的结果传递给下一个函数的特性,不过也足够展示调用队列的用途了——让多个底层为Ajax请求的异步操作按照固定的顺序阻塞式执行。
由于底层异步函数返回的就是Async.Operation,你可以直接把它传递给next方法,也可以用匿名函数包装后传递给next方法,而匿名函数内部只需要一个return。
延时函数
在上面的例子中,使用队列是为了避免触发服务器的防攻击策略,但有时候这还是不够的。例如说,服务器要求两个请求之间至少间隔500毫秒,否则就认为是攻击,那么我们就要在队列里面插入这个间隔了。在原本next方法调用的匿名函数中手动加入setTimeout是一个办法,但为什么我们不写一个辅助函数来解决这类问题呢?让我们来写一个辅助方法并让它和Async.Operation无缝结合起来。
Async.wait = function(delay, context) {
var operation = new Async.Operation();
setTimeout(function() {
operation.yield(context);
}, delay);
return operation;
};
Async.Operation.prototype.wait = function(delay, context) {
return this.next(function(context) {
return Async.wait(delay, context);
});
}
在有了这个辅助方法后,我们就可以在上述getAll方法中轻松实现在每个Ajax请求之间间隔500毫秒。在for循环内的加上对wait的调用就可以了。
for (var i = 0; i < json.length; i++) {
chain
.wait(500)
.next(function() {
return get(json.shift())
.addCallback(function(friend) {
friends.push(friend);
});
});
}
小结
通过一些简单的例子,我们了解到了Async.Operation常见的使用方式,以及在有需要的时候如何扩展它的功能。希望Async.Operation能够有效帮助大家提高Ajax应用的代码可读性。最后,如果大家希望将来继续读到类似的JavaScript开发模式相关的文章,不妨考虑订阅我的博客:
2009年6月30日星期二
写个 JavaScript 异步调用框架 (Part 5 - 链式实现)
在上一篇文章里面,我们为异步调用框架设计了一种链式调用方式,来增强异步调用队列的代码可读性,现在我们就来编写实现这部分功能的代码。
在这里我们可以看到,链式调用本身也是一个Async.Operation,链式调用所需的go方法和next方法都是在Async.Operation上面做的扩展,并且这个扩展不会很难,这将在下一小节说明。
对于异步调用进行队列的支持,我们稍后再来处理,首先我们利用现成的addCallback方法和yield方法扩展出go方法和next方法。
实际上,go方法和next方法直接调用的正是yield方法和addCallback方法。go方法的语义与yield方法一样,传递一个参数给Async.Operation实例,并且启动调用队列。同时,next方法的语义和addCallback方法,添加一个调用到队列的末端。
如果调用返回了一个Async.Operation实例,我们就利用它自身的addCallback方法帮我们执行队列中余下的调用。准确来说,是我们构造了一个新的调用链,把队列余下的调用都转移到新的调用链上,然后让当前异步调用在回调中启动这个新的调用链。
此外还有一些地方我们需要略作修改,以兼容新的异步调用队列的。例如result、state、completed的状态变更,在链式调用中是有所不同的。
现在我们已经拥有了一个功能强大的Async.Operation,接下来我们就要看看如何将它投入到更多常见的使用模式中去,如果你不希望错过相关讨论的话,欢迎订阅我的博客:
调用入口
链式调用存在Async.go方法和Async.chain方法两个入口,这两个入口本质上是一致的,只是Async.chain方法在调用时先不提供初始参数,而Async.go方法在调用时提供了初始参数并启动异步调用链。Async.chain = function() {
var chain = new Async.Operation({ chain: true });
return chain;
};
Async.go = function(initialArgument) {
return Async.chain().go(initialArgument);
}
在这里我们可以看到,链式调用本身也是一个Async.Operation,链式调用所需的go方法和next方法都是在Async.Operation上面做的扩展,并且这个扩展不会很难,这将在下一小节说明。
扩展方法
我们都知道,通过addCallback方法添加的回调函数是会被逐一执行的,至少同步函数如此,因此我们可以用Async.Operation的这一特性来维护异步调用队列,前提是我们为它加上对异步调用进行队列的支持。对于异步调用进行队列的支持,我们稍后再来处理,首先我们利用现成的addCallback方法和yield方法扩展出go方法和next方法。
this.go = function(initialArgument) {
return this.yield(initialArgument);
}
this.next = function(nextFunction) {
return this.addCallback(nextFunction);
};
实际上,go方法和next方法直接调用的正是yield方法和addCallback方法。go方法的语义与yield方法一样,传递一个参数给Async.Operation实例,并且启动调用队列。同时,next方法的语义和addCallback方法,添加一个调用到队列的末端。
异步队列
如何才能让原本仅支持同步的队列变得也支持异步?这需要检测队列中的每一个调用的返回,如果返回类型为Async.Operation,我们知道是异步调用,从而使用特殊的方法等它执行完后再执行下去。callbackResult = callback(self.result);
self.result = callbackResult;
if (callbackResult && callbackResult instanceof Async.Operation) {
innerChain = Async.chain();
while (callbackQueue.length > 0) {
innerChain.next(callbackQueue.shift());
}
innerChain.next(function(result) {
self.result = result;
self.state = "completed";
self.completed = true;
return result;
});
callbackResult.addCallback(function(result) {
self.result = result;
innerChain.go(result);
});
}
如果调用返回了一个Async.Operation实例,我们就利用它自身的addCallback方法帮我们执行队列中余下的调用。准确来说,是我们构造了一个新的调用链,把队列余下的调用都转移到新的调用链上,然后让当前异步调用在回调中启动这个新的调用链。
此外还有一些地方我们需要略作修改,以兼容新的异步调用队列的。例如result、state、completed的状态变更,在链式调用中是有所不同的。
小结
我们在原有的Async.Operation上略作修改,使得它支持异步调用队列,完整的代码看这里:支持链式调用的异步调用框架Async.Operation。现在我们已经拥有了一个功能强大的Async.Operation,接下来我们就要看看如何将它投入到更多常见的使用模式中去,如果你不希望错过相关讨论的话,欢迎订阅我的博客:
2009年6月14日星期日
中国程序员有美国梦吗?
Jeff最近转载了一篇名为《贺计算机成“就业最困难专业”》的文章,然后抛出了一个问题来,问大家对此看法如何,接着自然又引起了新一轮博客园首页发文热潮。对此,我站在我的角度说说我的看法。
1971年,旧金山湾区南端由于“淘沙子”出了名,因而被大家叫做硅谷。1998年,Google在硅谷诞生了,然后开始疯狂地吸收硅谷乃至全球的技术人才。2006至2007年,Google在中国进行大规模招聘,这时候他们遇到了跟淘金者中国之行一样的难题……
中国是第一个成功迫使Google进行笔试的国家。这句话说出来可能会让大家觉得十分搞笑,国内哪家公司招聘一线工程师不是先笔试再面试的。在人力资源供求相当的国家里,确实不需要笔试这轮筛选。有资质并且自信有资格来应聘的人,一般都能够得到充分的沟通,然后再确定这份工作是否适合。但这在人力资源明显供过于求的中国来说,就一点也不现实。
Google中国也有高层承认,以ACM/ICPC竞赛成绩来挑人确实有所缺陷,很多适合这份工作的人会被直接淘汰掉,连面对面或电话沟通的机会也没有。但假如你去看一下Google中国的校园招聘场面,看一下在各省重点大学里连开多个大教室进行笔试的情景,你不得不承认,要跟所有这些学生进行沟通的成本实在是连Google中国也支付不起。
总结一下,中国不是没有人才,也不是培养人才的方法有严重缺陷,至少这些都不是根本问题。根本问题是,在如此庞大的人口基数下,再好的企业都只能选用一套折中的人才选拔方案。你不能怪企业,说假如能生在美国你就能被Google美国所挖掘,而现在则被Google中国拒之门外。
我们来做个数学建模,假设美国计算机人才中的top 100, top 1,000, top 10,000分别叫做A类、B类、C类。那么美国的人力资源供需关系大概是这样的:
在保持A类、B类、C类界线不变的情况下,放到中国来很可能就是这样子的:
就算中国的人才需求是美国的5倍,但依然供过于求。就算顶级人才的供求矛盾不明显,人才结构上的不合理也会给底层造成巨大的压力。
在A类里面,只有500人做不了自己应做的工作,被迫降级去做B类的工作。对于人口如此众多的一个国家来说,让500人屈就一下不是什么大问题。在B类里面,会有95,000人做不了自己应做的工作。而到了C类里面,会有9,950,000人做不了自己应做的工作,这时候问题就很严重了。
如果你搞不懂上面那堆数字说的是什么,那么我尝试换用浅白一些的语言来再说一次。如果你在美国,并且你达到了Microsoft美国招聘的要求,那么你成功加入Microsoft的概率就相当高了,因为你和Microsoft相互需要对方。
但如果你在中国,并且你达到了Microsoft中国招聘的要求,那么你成功加入Microsoft的概率并不怎么高,因为跟你一样符合这一条件的人数要远多于Microsoft的需求。如果你想要确保一个较高的应聘成功率,那么你必须远高于Microsoft的招聘要求。
刚刚好胜任的人,是一定很有压力的。如果上面的分析没错的话,你之所以能够获得这个职位,一定程度上依赖于运气。好几个拥有同等实力的人跟你抢这个职位,尽管他们没能抢到,但依旧对此虎视眈眈,时刻准备着抢你饭碗。企业没理由对此坐视不理,一定会让你一直处于失业的边缘,以此作为筹码逼你好好干活,这时候你没压力才怪。
过度胜任的人,忠诚度一定会打折扣。总有更好的职位在对你招手,尽管你知道那些职位你只能凭运气获得,尽管你知道得到了你也站到了失业的边缘上,但你就不心动吗?肯定还是会心动的。
知足常乐者?估计在顶端会多一些。我和Jeff这样的,属于在国内少数算是享有American Dream的人,也就是能够通过自己的奋斗来获得应有的财富与社会地位。Jeff倾向于认为这对广大程序员普遍适用,并且鼓励大家都去追求自己的梦想。然而我得到的结论却并非如此。对于这事情,你怎么看?
大浪淘沙,金子难寻
1848年,美国爆发了加州淘金热潮,大量人口涌到加州进行淘金,其直接后果就是让一个叫旧金山小村庄的转眼间变成了一座大城市。在淘金热潮之初,你拿个筛子在河床里筛泥沙也能找到金子,这是何其容易的事情。但我们假想一下,如果当时一个淘金者拿着他的筛子跑到中国来,他能够淘到金子吗?我觉得是不可以的。这不是说中国的黄金矿藏量就比不上美国的一个州,而是中国没有如此高浓度而且可以进行露天开采的金矿。1971年,旧金山湾区南端由于“淘沙子”出了名,因而被大家叫做硅谷。1998年,Google在硅谷诞生了,然后开始疯狂地吸收硅谷乃至全球的技术人才。2006至2007年,Google在中国进行大规模招聘,这时候他们遇到了跟淘金者中国之行一样的难题……
中国是第一个成功迫使Google进行笔试的国家。这句话说出来可能会让大家觉得十分搞笑,国内哪家公司招聘一线工程师不是先笔试再面试的。在人力资源供求相当的国家里,确实不需要笔试这轮筛选。有资质并且自信有资格来应聘的人,一般都能够得到充分的沟通,然后再确定这份工作是否适合。但这在人力资源明显供过于求的中国来说,就一点也不现实。
Google中国也有高层承认,以ACM/ICPC竞赛成绩来挑人确实有所缺陷,很多适合这份工作的人会被直接淘汰掉,连面对面或电话沟通的机会也没有。但假如你去看一下Google中国的校园招聘场面,看一下在各省重点大学里连开多个大教室进行笔试的情景,你不得不承认,要跟所有这些学生进行沟通的成本实在是连Google中国也支付不起。
总结一下,中国不是没有人才,也不是培养人才的方法有严重缺陷,至少这些都不是根本问题。根本问题是,在如此庞大的人口基数下,再好的企业都只能选用一套折中的人才选拔方案。你不能怪企业,说假如能生在美国你就能被Google美国所挖掘,而现在则被Google中国拒之门外。
人人都会发光的时候你该怎么办?
有时候用“是金子早晚会发光”的说法来自我安慰一下,也不是什么坏事。但当你发现人人都会发光的时候,你就知道事情有多不好了。我们来做个数学建模,假设美国计算机人才中的top 100, top 1,000, top 10,000分别叫做A类、B类、C类。那么美国的人力资源供需关系大概是这样的:
- 需求:100人;供应:100人。
- 需求:1,000人;供应:1,000人。
- 需求:10,000人;供应:10,000人。
在保持A类、B类、C类界线不变的情况下,放到中国来很可能就是这样子的:
- 需求:500人;供应:1,000人。
- 需求:5,000人;供应:100,000人。
- 需求:50,000人;供应:10,000,000人。
就算中国的人才需求是美国的5倍,但依然供过于求。就算顶级人才的供求矛盾不明显,人才结构上的不合理也会给底层造成巨大的压力。
在A类里面,只有500人做不了自己应做的工作,被迫降级去做B类的工作。对于人口如此众多的一个国家来说,让500人屈就一下不是什么大问题。在B类里面,会有95,000人做不了自己应做的工作。而到了C类里面,会有9,950,000人做不了自己应做的工作,这时候问题就很严重了。
如果你搞不懂上面那堆数字说的是什么,那么我尝试换用浅白一些的语言来再说一次。如果你在美国,并且你达到了Microsoft美国招聘的要求,那么你成功加入Microsoft的概率就相当高了,因为你和Microsoft相互需要对方。
但如果你在中国,并且你达到了Microsoft中国招聘的要求,那么你成功加入Microsoft的概率并不怎么高,因为跟你一样符合这一条件的人数要远多于Microsoft的需求。如果你想要确保一个较高的应聘成功率,那么你必须远高于Microsoft的招聘要求。
没有压力的都跳槽了
假如我们把胜任某个职位的人简单分为两类:刚刚好胜任的,以及过度胜任的。那么,我们可以得到一个有趣的推论。刚刚好胜任的人,是一定很有压力的。如果上面的分析没错的话,你之所以能够获得这个职位,一定程度上依赖于运气。好几个拥有同等实力的人跟你抢这个职位,尽管他们没能抢到,但依旧对此虎视眈眈,时刻准备着抢你饭碗。企业没理由对此坐视不理,一定会让你一直处于失业的边缘,以此作为筹码逼你好好干活,这时候你没压力才怪。
过度胜任的人,忠诚度一定会打折扣。总有更好的职位在对你招手,尽管你知道那些职位你只能凭运气获得,尽管你知道得到了你也站到了失业的边缘上,但你就不心动吗?肯定还是会心动的。
知足常乐者?估计在顶端会多一些。我和Jeff这样的,属于在国内少数算是享有American Dream的人,也就是能够通过自己的奋斗来获得应有的财富与社会地位。Jeff倾向于认为这对广大程序员普遍适用,并且鼓励大家都去追求自己的梦想。然而我得到的结论却并非如此。对于这事情,你怎么看?
2009年5月8日星期五
写个 JavaScript 异步调用框架 (Part 4 - 链式调用)
我们已经实现了一个简单的异步调用框架,然而还有一些美中不足,那就是顺序执行的异步函数需要用嵌套的方式来声明。
现实开发中,要按顺序执行一系列的同步异步操作又是很常见的。还是用百度Hi网页版中的例子,我们先要异步获取联系人列表,然后再异步获取每一个联系人的具体信息,而且后者是分页获取的,每次请求发送10个联系人的名称然后取回对应的具体信息。这就是多个需要顺序执行的异步请求。
为此,我们需要设计一种新的操作方式来优化代码可读性,让顺序异步操作代码看起来和传统的顺序同步操作代码一样优雅。
其中先执行的函数为后执行的函数提供所需的数据。然而使用我们的异步调用框架后,同样的逻辑必须变成这样子:
在这个用例当中,我们在go传入初始化数据,然后每一个next后面传入一个数据处理函数,这些处理函数按顺序对数据进行处理。
在上述用例中,我们期待能够看到0, 1, 2, 3的提示信息序列,并且1和2之间间隔为1000毫秒。
我们可以扩展一下上一个用例,得到如下用例代码:
把链式调用的返回保存下来,在链式调用完成时,它的result应该与最后一个操作的返回一致。在上述用例中,也就是3。
在这个用例当中,用户每隔1000毫秒添加一个操作,而其中第二个操作耗时2000毫秒。也就是说,添加第三个操作时第二个操作还没返回。作为一个健壮的框架,必须要能兼容这样的使用方式。
此外我们还要考虑,用户可能想要先构造调用链,然后再执行调用链。这时候用户就会先使用next方法添加操作,再使用go方法执行。
在上述用例中,用户通过chain和next添加了头同步操作和异步操作各一个,然后用go执行调用链,在调用链执行完毕之前又用next异步追加了一个操作。一个健壮的框架,在这样的用例当中应该能够如同用户所期望的那样提示0, 1, 2。
现实开发中,要按顺序执行一系列的同步异步操作又是很常见的。还是用百度Hi网页版中的例子,我们先要异步获取联系人列表,然后再异步获取每一个联系人的具体信息,而且后者是分页获取的,每次请求发送10个联系人的名称然后取回对应的具体信息。这就是多个需要顺序执行的异步请求。
为此,我们需要设计一种新的操作方式来优化代码可读性,让顺序异步操作代码看起来和传统的顺序同步操作代码一样优雅。
传统做法
大多数程序员都能够很好的理解顺序执行的代码,例如这样子的:var firstResult = firstOperation(initialArgument);
var secondResult = secondOperation(firstResult);
var finalResult = thirdOperation(secondResult);
alert(finalResult);
其中先执行的函数为后执行的函数提供所需的数据。然而使用我们的异步调用框架后,同样的逻辑必须变成这样子:
firstAsyncOperation(initialArgument)
.addCallback(function(firstResult) {
secondAsyncOperation(firstResult)
.addCallback(function(secondResult) {
thirdAsyncOperation(secondResult)
.addCallback(function(finalResult) {
alert(finalResult);
});
});
});
链式写法
我认为上面的代码实在是太不美观了,并且希望能够改造为jQuery风格的链式写法。为此,我们先构造一个用例:Async.go(initialArgument)
.next(firstAsyncOperation)
.next(secondAsyncOperation)
.next(thirdAsyncOperation)
.next(function(finalResult) { alert(finalResult); })
在这个用例当中,我们在go传入初始化数据,然后每一个next后面传入一个数据处理函数,这些处理函数按顺序对数据进行处理。
同步并存
上面的用例调用到的全部都是异步函数,不过我们最好能够兼容同步函数,让使用者无需关心函数的具体实现,也能使用这项功能。为此我们再写一个这样的用例:Async.go(0)
.next(function(i) { alert(i); return i + 1; })
.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 1000);
return operation;
})
.next(function(i) { alert(i); return i + 1; })
.next(function(i) { alert(i); return i; });
在上述用例中,我们期待能够看到0, 1, 2, 3的提示信息序列,并且1和2之间间隔为1000毫秒。
异步本质
一个链式调用,本质上也是一个异步调用,所以它返回的也是一个Operation实例。这个实例自然也有result、state和completed这几个字段,并且当整个链式调用完成时,result等于最后一个调用返回的结果,而completed自然是等于true。我们可以扩展一下上一个用例,得到如下用例代码:
var chainOperation = Async.go(0)
.next(function(i) { alert(i); return i + 1; })
.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 1000);
return operation;
})
.next(function(i) { alert(i); return i + 1; })
.next(function(i) { alert(i); return i; });
setTiemout(function() { alert(chainOperation.result; }, 2000);
把链式调用的返回保存下来,在链式调用完成时,它的result应该与最后一个操作的返回一致。在上述用例中,也就是3。
调用时机
尽管我们提供了一种链式调用方式,但是用户不一定会按照这种固定的方式来调用,所以我们仍然要考虑兼容用户的各种可能用法,例如说异步地用next往调用链添加操作:var chainOperation = Async.go(0);
chainOperation.next(function(i) { alert(i); return i + 1; });
setTimeout(function() {
chainOperation.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 2000);
return operation;
})
}, 1000);
setTimeout(function() {
chainOperation.next(function(i) { alert(i); return i + 1; });
}, 2000);
在这个用例当中,用户每隔1000毫秒添加一个操作,而其中第二个操作耗时2000毫秒。也就是说,添加第三个操作时第二个操作还没返回。作为一个健壮的框架,必须要能兼容这样的使用方式。
此外我们还要考虑,用户可能想要先构造调用链,然后再执行调用链。这时候用户就会先使用next方法添加操作,再使用go方法执行。
var chainOperation = Async
.chain(function(i) { alert(i); return i + 1; })
.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 2000);
return operation;
})
.go(0)
setTimeout(function() {
chainOperation.next(function(i) { alert(i); return i + 1; })
}, 1000);
在上述用例中,用户通过chain和next添加了头同步操作和异步操作各一个,然后用go执行调用链,在调用链执行完毕之前又用next异步追加了一个操作。一个健壮的框架,在这样的用例当中应该能够如同用户所期望的那样提示0, 1, 2。
小结
针对链式调用的需求,我们设计了如此多的用例,包括各种奇怪的异步调用方式。最终如何实现这样的功能呢?如果你想知道的话,欢迎订阅我的博客:2009年5月6日星期三
写个 JavaScript 异步调用框架 (Part 3 - 代码实现)
在上一篇文章里,我们说到了要实现一个Async.Operation类,通过addCallback方法传递回调函数,并且通过yield方法返回回调结果。现在我们就来实现这个类吧。
我们假设yield方法会把callbackQueue中的回调函数逐个取出来然后调用,因此如果compeleted为true,则使用已有的result再调用一次yield就可以了,这样yield自然会调用这次添加到callbackQueue的回调函数。
至于最后的return this;,只是为了方便jQuery风格的链式写法,可以通过点号分隔连续添加多个回调函数:
通过使用setTimeout,我们确保了yield的实际操作是异步进行的。然后我们把用户传入yield的结果及相关状态更新到对象属性之上,最后遍历callbackQueue调用所有的回调函数。
这个框架能够很好的解决调用栈中出现同步异步操作并存的情况,假设所有函数都返回Async.Operation,框架的使用者可以使用一种统一的模式来编写代码,处理函数返回,而无需关心这个函数实际上是同步返回了还是异步返回了。
对于串行调用多个异步函数的情况,我们现在可以用嵌套addCallback的方式来书写,但随着嵌套层数的增多,代码会变得越来越不美观:
我们能否把嵌套形式改为jQuery风格的链式写法呢?这是我们接下来要思考的问题,如果你不希望错过相关讨论的话,欢迎订阅我的博客:
类结构
首先我们来搭一个架子,把需要用到的似有变量都列出来。我们需要一个数组,来保存回调函数列表;需要一个标志位,来表示异步操作是否已完成;还可以学IAsyncResult,加一个state,允许异步操作的实现者对外暴露自定义的执行状态;最后加一个变量保存异步操作结果。var Cat = {};
Async = {
Operation: {
var callbackQueue = [];
this.result = undefined;
this.state = "waiting";
this.completed = false;
}
}
addCallback方法
接下来,我们要实现addCallback方法,它的工作职责很简单,就是把回调函数放到callbackQueue中。此外,如果此时completed为true,说明异步操作已经yield过了,则立即调用此回调。this.yield = function(callback) {
callbackQueue.push(callback);
if (this.completed) {
this.yield(this.result);
}
return this;
}
我们假设yield方法会把callbackQueue中的回调函数逐个取出来然后调用,因此如果compeleted为true,则使用已有的result再调用一次yield就可以了,这样yield自然会调用这次添加到callbackQueue的回调函数。
至于最后的return this;,只是为了方便jQuery风格的链式写法,可以通过点号分隔连续添加多个回调函数:
asyncOperation(argument)
.addCallback(firstCallback)
.addCallback(secondCallback);
yield方法
最后,我们要实现yield方法。它需要将callbackQueue中的回调函数逐个取出来,然后都调用一遍,并且保证这个操作是异步吧。this.yield = function(result) {
var self = this;
setTimeout(function() {
self.result = result;
self.state = "completed";
self.completed = true;
while (callbackQueue.length > 0) {
var callback = callbackQueue.shift();
callback(self.result);
}
}, 1);
return this;
}
通过使用setTimeout,我们确保了yield的实际操作是异步进行的。然后我们把用户传入yield的结果及相关状态更新到对象属性之上,最后遍历callbackQueue调用所有的回调函数。
小结
这样我们就做好了一个简单的JavaScript异步调用框架,完整的代码可以看这里:异步调用框架Async.Operation。这个框架能够很好的解决调用栈中出现同步异步操作并存的情况,假设所有函数都返回Async.Operation,框架的使用者可以使用一种统一的模式来编写代码,处理函数返回,而无需关心这个函数实际上是同步返回了还是异步返回了。
对于串行调用多个异步函数的情况,我们现在可以用嵌套addCallback的方式来书写,但随着嵌套层数的增多,代码会变得越来越不美观:
firstAsyncOperation().addCallback(function() {
secondAsyncOperation().addCallback(function() {
thirdAsyncOperation().addCallback(function() {
finalSyncOperation();
});
});
});
我们能否把嵌套形式改为jQuery风格的链式写法呢?这是我们接下来要思考的问题,如果你不希望错过相关讨论的话,欢迎订阅我的博客:
写个 JavaScript 异步调用框架 (Part 2 - 用例设计)
在上一篇文章里说到,我们要设计一个异步调用框架,最好能够统一同步异步调用的接口,同时具体调用顺序与实现方式无关。那么我们现在就来设计这样一个框架的用例。
在参数相当多的时候,我们可以把参数放到一个JSON里面,这样参数就如同具名参数一样,可以通过参数名选择性的传递参数,不传递的参数相当于使用默认值。这是从Prototype开始就流行起来的做法:
然而这两种做法都有一个坏处,就是把同步函数改为异步函数(或同步异步混合函数)时,必须显式地修改函数签名,在最后增加一个(或多个)参数。
由于在调用栈的底层引入异步函数对我们来说太常见了,为此可能要更改一大堆上层调用函数签名的成本实在是太高了,所以我们还是想一个不用修改函数签名的做法吧。
在这里我参考了.NET Framework的IAsyncResult设计,把异步操作有关的一切信息集中到一个对象上来,从而避免了对函数签名的修改。在此,我们假设一个异步函数的调用原型是这样子的:
在这段代码里,我们返回了一个Operation对象,用于将来传递回调函数。同时,我们通过setTimeout模拟了异步返回结果,而具体的返回方式就是yield方法。
接着,我们还要设计传递回调函数的方法。由于我们不能好像C#那样重载+=运算符,所以只能用函数传递回调函数:
在C#里面做这样的设计是不安全的,因为在异步操作可能在添加回调之前就完成了。但在JavaScript里面这样写是安全的,因为JavaScript是单线程的,紧接着asyncOperation的同步addCallback必然先执行,asyncOperation中的异步yield必然后执行。
就算把代码写成这个样子,我们也能确保addCallback先于yield的实际逻辑执行。
传递回调
我们首先要考虑的一个问题是,如何传递回调入口。在最传统的XHR调用当中,回调函数会被作为最后一个参数传递给异步函数:function asyncOperation(argument, callback)
在参数相当多的时候,我们可以把参数放到一个JSON里面,这样参数就如同具名参数一样,可以通过参数名选择性的传递参数,不传递的参数相当于使用默认值。这是从Prototype开始就流行起来的做法:
function asyncOperation(argument, options)
然而这两种做法都有一个坏处,就是把同步函数改为异步函数(或同步异步混合函数)时,必须显式地修改函数签名,在最后增加一个(或多个)参数。
由于在调用栈的底层引入异步函数对我们来说太常见了,为此可能要更改一大堆上层调用函数签名的成本实在是太高了,所以我们还是想一个不用修改函数签名的做法吧。
在这里我参考了.NET Framework的IAsyncResult设计,把异步操作有关的一切信息集中到一个对象上来,从而避免了对函数签名的修改。在此,我们假设一个异步函数的调用原型是这样子的:
function asyncOperation(argument) {
operation = new Async.Operation();
setTimeout(function() { operation.yield("hello world"); }, 1000);
return operation;
}
在这段代码里,我们返回了一个Operation对象,用于将来传递回调函数。同时,我们通过setTimeout模拟了异步返回结果,而具体的返回方式就是yield方法。
接着,我们还要设计传递回调函数的方法。由于我们不能好像C#那样重载+=运算符,所以只能用函数传递回调函数:
var operation = asyncOperation(argument);
operation.addCallback(function(result) { alert(result); });
在C#里面做这样的设计是不安全的,因为在异步操作可能在添加回调之前就完成了。但在JavaScript里面这样写是安全的,因为JavaScript是单线程的,紧接着asyncOperation的同步addCallback必然先执行,asyncOperation中的异步yield必然后执行。
调用顺序
可能有人要问,如果用户使用同步的方式来调用yield,这时候执行顺序不一样依赖于yield的实现吗?没错,不过yeild是在框架中一次性实现的,我们只要把它做成异步的就可以了,这样即使对它进行同步调用,也不影响执行顺序:function psudoAsyncOperation(argument) {
operation = new Async.Operation();
operation.yield("hello world");
return operation;
}
var operation = asyncOperation(argument);
operation.addCallback(function(result) { alert(result); });
就算把代码写成这个样子,我们也能确保addCallback先于yield的实际逻辑执行。
事后回调
有时候,框架的使用者可能真的写出了先yield后addCallback的代码。这时候,我认为必须保证addCallback中添加的回调函数会被立即触发。因为用户添加这个回调函数,意味着他期望当异步操作有结果时通知这个回调函数,而这与添加回调函数时异步操作是否完成无关。为此,我们再添加一个用例:function psudoAsyncOperation(argument) {
operation = new Async.Operation();
operation.yield("hello world");
return operation;
}
var operation = asyncOperation(argument);
setTimeout(function() {
operation.addCallback(function(result) { alert(result); });
}, 1000);
小结
到这里,我们就设计好了一个名为Async.Operation的异步操作对象,具体如何实现关键的yield方法和addCallback方法将在下一篇文章讲述如果。你不希望错过的话,欢迎订阅我的博客:2009年5月5日星期二
写个 JavaScript 异步调用框架 (Part 1 - 问题 & 场景)
问题
在Ajax应用中,调用XMLHttpRequest是很常见的情况。特别是以客户端为中心的Ajax应用,各种需要从服务器端获取数据的操作都通过XHR异步调用完成。然而在单线程的JavaScript编程中,XHR异步调用的代码风格实在是与一般的JavaScript代码格格不入。额外参数
考虑一个除法函数,如果它是纯客户端的同步函数,那么签名会是这样的:function divide(operand1, operand2)
然而假设我们对客户端除法的精度不满意,于是把除法转移到服务器端来执行,那么它是个需要调用XHR的异步函数,签名也就可能会是以下几种之一:
function divide(operand1, operand2, callback)
function divide(operand1, operand2, successCallback, failureCallback)
function divide(operand1, operand2, options)
我们必须在签名中引入新的参数来传递回调函数,不能选择让函数变成阻塞式的同步调用。
可传递性
不仅仅直接操作XHR的函数需要引入新的参数,这种复杂性还会顺着调用栈向外传递。例如说,我们对加减乘除四则运算作了封装,只向外暴露一个运算接口:function calculate(operand1, operand2, operator)
这个calculate函数根据operator参数来调用内部的plus, subtract, multiply, divide函数。然而,因为divide函数变成了异步函数,所以整个calculate函数不得不也转变为异步函数:
function calculate(operand1, operand2, operator, callback)
同时,在调用栈之上凡是需要调用到calculate的函数,都必须变成异步的,除非它并不需要向上一级调用函数返回结果。
同步并存
尽管calculate函数变成了一个异步函数,它所调用的plus, subtract, multiply函数还是同步函数,只有调用divide时是异步的,这时候calculate就是一个异步同步并存函数。这会带来什么问题?calculate的调用者看到函数签名自然会认为calculate是个异步函数,因为它需要传递回调函数,然而calculate的执行方式却是不确定的。考虑如下调用:
calculate(operand1, operand2, operator, callback);
next();
这里涉及到callback和next两个函数,它们哪个先执行哪个后执行是不确定的,或者说是依赖于calculate具体实现的。
如果calculate的实现是,当不需要进行异步操作时,直接调用callback。那么,在执行加减乘法时callback会在next之前被调用;在执行除法时next会在callback之前调用。
如果我们不喜欢这种不确定性,我们可以改变一下calculate的实现,把同步调用也都改为setTimeout形式的,这样在任何情况下next都一定会在callback之前被调用。
然而后面一种做法依赖于成本较高的实现方式,开发者一个不小心(或者摆明偷懒)就会漏掉setTimeout,导致函数调用顺序变得不确定,所以我们会希望这是框架帮助实现的功能,在使用框架时无法把这绕过。
场景
在这里,我举一个关于上述问题的具体应用场景。(为简化问题,描述已略作修改,与实际应用并不一致。)在百度Hi网页版里面,我们会在客户端保存一个用户对象列表,在打开和这个用户的聊天窗口时,我们需要从中读取这个用户的信息。这个操作就涉及很多可能同步又可能异步的分支:
- 用户对象未缓存
- 异步读取用户信息
- 用户对象已缓存
- 用户是好友(信息更新会由服务器端推送)
- 同步读取用户信息
- 用户不是好友(信息更新需要由客户端拉取)
- 可以接受缓存信息
- 同步读取用户信息
- 必须获取最新信息
- 异步读取用户信息
- 可以接受缓存信息
- 用户是好友(信息更新会由服务器端推送)
为了解决这个问题,我们需要写一个异步调用框架,用一种统一的方式来进行调用,把同步和异步调用都合并为一种返回方式。
具体的解决方案会在下一篇文章中给出,如果你不希望错过的话,欢迎订阅我的博客:
2009年4月11日星期六
豆瓣的『请勿联系我们』页面
如果说Don't Make Me Think的基本思想是让用户凭直觉都能找到他们需要的东西,那么豆瓣的联系我们页面就做了一个绝佳的例子,或者说是绝佳的反例,这视乎你怎么看这个问题。

接着搞笑的事情发生了,你发现这个货架其实是个锁上了的玻璃橱窗,上面有一个牌子写着『如需取商品,请找售货员』。这时候你会想,这不是一家便利店吗?还是有什么我理解错了?这就是豆瓣的联系我们页面给我的感觉,因为那些邮件地址不仅仅不可以点击,还刻意设计为图片使得不能够复制。
如果你跟我一样,习惯有什么想法直接点击网站的联系我们链接,你应该明白常见的联系我们页面是怎样的。网站往往为了保护邮件地址免受垃圾邮件骚扰而不会直接公开邮件地址,为此他们会设计一个专门的表单让你提交反馈信息并留下你的联系方式。随后如果你提交的信息确实有价值,网站会主动联系你,之后的通信就都可以在邮件里进行了。
这是用户对联系我们页面的印象,就如同对便利店的印象一样——自由地在货架前选取商品然后再去埋单。然而豆瓣选择了在便利店里放置锁上了的玻璃橱窗,这就会导致用户迷失方向,他们会想『这是一家便利店吗?还是说我走进了一家「珠宝便利店」?那算了,我就不买了,或者到附近的便利店买吧。』
这又是一个让用户感受挫折的地方,然而这还不是最后一个。如果用户坚信反馈表单存在,就如同顾客坚信这是每一家便利店都卖的东西一样,那么他们自然会继续找下去。在点击进入任何一个分类链接后,用户会看到又一个清晰的Q&A列表,然后他必须再一次经历同样的挫折,最后才在一个不显眼的地方找到他想要的东西,一个写着『没搞定?给管理员捎个话』的链接。
为什么说这个链接不显眼?因为特殊链接往往会放在页头、页脚或侧栏这类视觉上与内容分离开来的区域,而这个链接放在问题列表和解答列表之间的狭缝。这处的留白本应只用于表示两个列表互相对立的关系,在两个块区元素之间插入一个行内元素只会让人以为那也是分隔符的一部分而已。
联系我们
还是用Don't Make Me Think里面的比喻,在网站上寻找内容就如同在商店里寻找商品,最理想的情况自然是一抬头就看到导向牌说你想要的商品在哪一行的哪一个货架上。在这方面,豆瓣的联系我们页面是做得非常好的,通过清晰的分类说明了不同情况应该联系不同的邮件地址。就如同清晰的导向牌一样,你能够径直走到摆满你想要的商品的货架前。接着搞笑的事情发生了,你发现这个货架其实是个锁上了的玻璃橱窗,上面有一个牌子写着『如需取商品,请找售货员』。这时候你会想,这不是一家便利店吗?还是有什么我理解错了?这就是豆瓣的联系我们页面给我的感觉,因为那些邮件地址不仅仅不可以点击,还刻意设计为图片使得不能够复制。
如果你跟我一样,习惯有什么想法直接点击网站的联系我们链接,你应该明白常见的联系我们页面是怎样的。网站往往为了保护邮件地址免受垃圾邮件骚扰而不会直接公开邮件地址,为此他们会设计一个专门的表单让你提交反馈信息并留下你的联系方式。随后如果你提交的信息确实有价值,网站会主动联系你,之后的通信就都可以在邮件里进行了。
这是用户对联系我们页面的印象,就如同对便利店的印象一样——自由地在货架前选取商品然后再去埋单。然而豆瓣选择了在便利店里放置锁上了的玻璃橱窗,这就会导致用户迷失方向,他们会想『这是一家便利店吗?还是说我走进了一家「珠宝便利店」?那算了,我就不买了,或者到附近的便利店买吧。』
帮助中心
这个联系我们页面上面,只有一个链接可点击(除去全局链接外),那就是帮助中心链接了。用户找不到熟悉的反馈表单,自然会凭直觉沿着链接点击下去,而这个链接是唯一的选择。进去之后仍然是一个清晰分类的页面,不过就是没有用户需要的反馈表单。或许用户会浏览一下分类列表,然后发现他要反馈的问题不能算是严格属于某个特定分类。这时候就如同顾客根据导向牌走进了唯一一行他认为正确的货架,但是货架上的分类标识他想要的东西不属于这里的任何一个货架。这又是一个让用户感受挫折的地方,然而这还不是最后一个。如果用户坚信反馈表单存在,就如同顾客坚信这是每一家便利店都卖的东西一样,那么他们自然会继续找下去。在点击进入任何一个分类链接后,用户会看到又一个清晰的Q&A列表,然后他必须再一次经历同样的挫折,最后才在一个不显眼的地方找到他想要的东西,一个写着『没搞定?给管理员捎个话』的链接。
为什么说这个链接不显眼?因为特殊链接往往会放在页头、页脚或侧栏这类视觉上与内容分离开来的区域,而这个链接放在问题列表和解答列表之间的狭缝。这处的留白本应只用于表示两个列表互相对立的关系,在两个块区元素之间插入一个行内元素只会让人以为那也是分隔符的一部分而已。
小结
这就是为什么我说豆瓣的『联系我们』页面应该叫做『请勿联系我们』页面。它在向用户传达一种意思,那就是『我们豆瓣是刻意增加你联系我们的成本的,因为我们就是不欢迎你联系我们。』这就好比说,我们都知道每一家7-11都会在cashier前有一个放满了condom的架子,而豆瓣是一家和特别的7-11,它在那里放了一个牌子然后写上『我们就是不卖condom,你就不要找了,也不要问我们的售货员』。2009年3月29日星期日
Microsoft MVP Global Summit 2009 (Part 2 - Sessions)
这次去参加MVP Summit,我计划主要是听ASP.NET 4.0及Silverlight 3有关的session,结果在Microsoft Campus的两天也就泡在MSCC,也就是往返于Hood和Rainier这两个room。这些内容之前一直都不能说,现在MIX09开完之后,大部分的内容我们都可以自由讨论了。
此外,ASP.NET 4.0还包括ASP.NET MVC和Dynamic Data,前者终于发布了1.0,后者比最初我看到的第一个Preview已经要强大很多了,当然也换上了Entity Model这样的数据访问层组件。
Silverlight 3的突破是巨大的,假如我们都承认原来的Silverlight 2其实很弱的话。在工具方面,现在终于可以在Visual Studio 2010里面拖放编辑XAML了,而Expression Blend也支持C#编辑了,不再像过去的版本那样,Visual Studio和Blend各自只能完成一半的工作,必须在两个工具之间切换来切换去。
视频播放一直都是Silverlight非常重视的功能之一,Silverlight 3将会支持H.264,并把H.264/AAC/MP4作为标准的视频封装格式。如今有越来越多的设备支持H.264的硬件解码,而且Adobe与Apple的软件也广泛采用H.264作为高清视频的编码格式,所以将来我们无需再为编码格式的区别而烦恼了。
另外,Silverlight 3还引入了GPU硬件加速的支持,用于UIElement的合成与拉伸,同时支持浏览器内嵌模式与全屏模式。不过,如果你要让你的Silverlight 3应用支持GPU加速,还必须在浏览器插件及UIElement两个层面声明启用GPU加速,以保证其他内容不受此新功能引入的影响。
GPU硬件加速带来的是什么?当然是视觉上的新功能啦。过去Silverlight 2无法做梯形变换,所以也就做不了真正的Coverflow。现在Silverlight 3可以做Perspective 3D了,Coverflow也就能实现了。另外WPF拥有的Effects在Silverlight 3中也能用了,虽然Silverlight 3现在的Preview仅仅自带了两个Effects,但WPF自带的Effects甚至是第三方的Effects都可以导入到Silverlight 3中使用,因为它们都是用HLSL写的Pixel Shaders。
如果你对这些新功能感兴趣,我建议你去看看MIX 09的视频,里面涵盖了我所说到的所有内容。我准备接下来用Silverlight 3做一个Coverflow,如果你有兴趣了解这是怎么做的,欢迎订阅Cat in Chinese。
ASP.NET 4.0
ASP.NET 4.0方面,我最关注的是ASP.NET AJAX 4.0,不过Preview 4和Preview 3比起来没多少新增功能,我只能希望在Preview 3中存在的bug在Preview 4都修复了。在demo方面,DataView控件加上Live Binding确实非常cool,可以一个DataView做master另一个做details,在其中任何一处作出的修改立即同步到所有地方,包括对ADO.NET Data Service作出更新请求。不过demo始终就只是demo,现实中的场景总有一些更复杂更需要创意的地方。此外,ASP.NET 4.0还包括ASP.NET MVC和Dynamic Data,前者终于发布了1.0,后者比最初我看到的第一个Preview已经要强大很多了,当然也换上了Entity Model这样的数据访问层组件。
Silverlight 3
我对Silverlight 3的关注原本只是很表面的,但却发现Silverlight 3的新增功能比ASP.NET 4.0要多得多,这意味着Microsoft对Silverlight的投入巨大吧,因此我也觉得有必要去好好研究Silverlight 3了。Silverlight 3的突破是巨大的,假如我们都承认原来的Silverlight 2其实很弱的话。在工具方面,现在终于可以在Visual Studio 2010里面拖放编辑XAML了,而Expression Blend也支持C#编辑了,不再像过去的版本那样,Visual Studio和Blend各自只能完成一半的工作,必须在两个工具之间切换来切换去。
视频播放一直都是Silverlight非常重视的功能之一,Silverlight 3将会支持H.264,并把H.264/AAC/MP4作为标准的视频封装格式。如今有越来越多的设备支持H.264的硬件解码,而且Adobe与Apple的软件也广泛采用H.264作为高清视频的编码格式,所以将来我们无需再为编码格式的区别而烦恼了。
另外,Silverlight 3还引入了GPU硬件加速的支持,用于UIElement的合成与拉伸,同时支持浏览器内嵌模式与全屏模式。不过,如果你要让你的Silverlight 3应用支持GPU加速,还必须在浏览器插件及UIElement两个层面声明启用GPU加速,以保证其他内容不受此新功能引入的影响。
GPU硬件加速带来的是什么?当然是视觉上的新功能啦。过去Silverlight 2无法做梯形变换,所以也就做不了真正的Coverflow。现在Silverlight 3可以做Perspective 3D了,Coverflow也就能实现了。另外WPF拥有的Effects在Silverlight 3中也能用了,虽然Silverlight 3现在的Preview仅仅自带了两个Effects,但WPF自带的Effects甚至是第三方的Effects都可以导入到Silverlight 3中使用,因为它们都是用HLSL写的Pixel Shaders。
如果你对这些新功能感兴趣,我建议你去看看MIX 09的视频,里面涵盖了我所说到的所有内容。我准备接下来用Silverlight 3做一个Coverflow,如果你有兴趣了解这是怎么做的,欢迎订阅Cat in Chinese。
2009年3月22日星期日
Microsoft MVP Global Summit 2009 (Part 1 - Trip & Food)
虽然MVP Summit发生在3月初,但由于我一直都很懒,所以现在才来写写文章。而且,很多MVP Summit上讨论的受NDA保护的内容,过了MIX09也就成为公开内容了,我也就可以在这里说说了。
今年买票的时候,竟然找不到第二个人跟我同行。上海的一群MVP,都为了省钱买了海航的机票,从上海飞到北京再直飞西雅图。我还是更愿意飞NWA,因为里程可以积累到我的南航帐号上,所以虽然票价贵一些,我还是选择了一个人飞NWA。直到起飞前两天,才确定上海的包包跑来北京了,因为面签时被check了拖到最后一刻才买票,结果就和我同机了。
飞到东京之后,有趣的事情发生了,我们见到了一群穿着校服的日本女生走进来候机室,登机之后其中一位就坐在我旁边。我在吃完机上第一餐之后,无所事事就开始找她聊天,问问她有没有用过Baidu.jp,然后说说去美国做什么,她说她其实是去Vancouver参加交流活动。因为飞机引擎噪声很大,我们又都不太懂对方的口音,所以我拿了一个小本子出来把英语写下来让她看。在她有不懂表达的意思时,她拿了一个电子词典出来用日文查对应的英文,不过有时候我看看其中的日文解释,上面的汉字也足够我理解了。这样的沟通挺有趣的。
到了Seattle之后,打车去Sheraton Seattle Hotel,然后还是和去年一样带着今年第一次来的MVP去海边走走,去Crab Pot吃蟹。我们要了便宜的snow crab和dungeon crab,没有选最贵的king crab,吃起来感觉还不错,不过snow crab实在是太小了。最后一天晚上也是在Crab Pot吃,就是吃king crab,但这其实不是我们吃过最好的king crab。
说到这里,就不得不重点推荐一下我们在Oceanaire Seafood Room吃的那一餐Alaska red king crab了。虽然这里的king crab要$45/pound,而Crab Pot的只要$22/pound,但是这里的king crab确实比较大,而且非常甜!如果你想要尝一尝king crab是有多么的好吃,那就一定要去Oceanaire Seafood Room点Alaska red king crab。一般来说,一个人吃1~2 pounds是没问题的,如果你不再点其他东西的话。面包是送的,但既然是专门来吃king crab的,当然就不再点其他东西了,甚至连面包都不吃。

美国人吃法比较奇怪的地方在于,他们用石头那么硬的面包来搭配甜美的king crab。不仅仅这样,他们还会上热好了的黄油,让你点king crab来吃。我试了一下,发现这一点都不好吃,原本爽口的蟹肉变得油油的,感觉很奇怪。
今年MVP Summit的开场中说到去年MVP Summit抱怨最厉害的事情,那就是salmon吃得太多了,每餐都有salmon,所以今年的口号是No Salmon。我自然很失望啦,Seattle作为一个salmon产量如此多的地方,我过来自然要吃salmon啊,怎么可以没salmon呢?结果后来几天真的很多人在Twitter上抱怨No Salmon,于是乎今年最大的抱怨也就正好与去年相反了。看来做如此大的一场活动,关于吃什么的问题真是众口难调哦。
有关MVP Summit旅行与饮食的话题就说到这里吧,如果你有兴趣了解我接下来要说的技术内容,欢迎订阅Cat in Chinese。
今年买票的时候,竟然找不到第二个人跟我同行。上海的一群MVP,都为了省钱买了海航的机票,从上海飞到北京再直飞西雅图。我还是更愿意飞NWA,因为里程可以积累到我的南航帐号上,所以虽然票价贵一些,我还是选择了一个人飞NWA。直到起飞前两天,才确定上海的包包跑来北京了,因为面签时被check了拖到最后一刻才买票,结果就和我同机了。
飞到东京之后,有趣的事情发生了,我们见到了一群穿着校服的日本女生走进来候机室,登机之后其中一位就坐在我旁边。我在吃完机上第一餐之后,无所事事就开始找她聊天,问问她有没有用过Baidu.jp,然后说说去美国做什么,她说她其实是去Vancouver参加交流活动。因为飞机引擎噪声很大,我们又都不太懂对方的口音,所以我拿了一个小本子出来把英语写下来让她看。在她有不懂表达的意思时,她拿了一个电子词典出来用日文查对应的英文,不过有时候我看看其中的日文解释,上面的汉字也足够我理解了。这样的沟通挺有趣的。
到了Seattle之后,打车去Sheraton Seattle Hotel,然后还是和去年一样带着今年第一次来的MVP去海边走走,去Crab Pot吃蟹。我们要了便宜的snow crab和dungeon crab,没有选最贵的king crab,吃起来感觉还不错,不过snow crab实在是太小了。最后一天晚上也是在Crab Pot吃,就是吃king crab,但这其实不是我们吃过最好的king crab。
说到这里,就不得不重点推荐一下我们在Oceanaire Seafood Room吃的那一餐Alaska red king crab了。虽然这里的king crab要$45/pound,而Crab Pot的只要$22/pound,但是这里的king crab确实比较大,而且非常甜!如果你想要尝一尝king crab是有多么的好吃,那就一定要去Oceanaire Seafood Room点Alaska red king crab。一般来说,一个人吃1~2 pounds是没问题的,如果你不再点其他东西的话。面包是送的,但既然是专门来吃king crab的,当然就不再点其他东西了,甚至连面包都不吃。
美国人吃法比较奇怪的地方在于,他们用石头那么硬的面包来搭配甜美的king crab。不仅仅这样,他们还会上热好了的黄油,让你点king crab来吃。我试了一下,发现这一点都不好吃,原本爽口的蟹肉变得油油的,感觉很奇怪。
今年MVP Summit的开场中说到去年MVP Summit抱怨最厉害的事情,那就是salmon吃得太多了,每餐都有salmon,所以今年的口号是No Salmon。我自然很失望啦,Seattle作为一个salmon产量如此多的地方,我过来自然要吃salmon啊,怎么可以没salmon呢?结果后来几天真的很多人在Twitter上抱怨No Salmon,于是乎今年最大的抱怨也就正好与去年相反了。看来做如此大的一场活动,关于吃什么的问题真是众口难调哦。
有关MVP Summit旅行与饮食的话题就说到这里吧,如果你有兴趣了解我接下来要说的技术内容,欢迎订阅Cat in Chinese。
2009年3月13日星期五
ASP.NET AJAX 4.0 Preview 3 (Part 2 - ASP.NET AJAX Template)
在上一篇文章里,我们说到了如何使用ADO.NET Data Service Client Library能够轻松访问到存在服务器端的数据,然而将数据展现出来仍需要人手拼接HTML这点就实在是让人难以接受,所以我们现在就来看看如何利用 ASP.NET AJAX Template解决这个问题。文章中所用到的示例代码,可以在这里下载:ASP.NET AJAX 4.0 Preview 3 Demo,然后参考里面的AspNetAjaxTemplateDemo.aspx。
如果你把一个Array绑定到
如果你把单个Object绑定到
然后我们就可以基于itemTemplate这个HTML元素创建控件了:
现在,页面显示出来的结果和之前我们人手拼接HTML的版本完全一致,不过我们已经不在需要维护嵌入在JavaScript中的HTML代码了。
我们所需要更改的代码包括:
经过这三步,我们就可以将原来使用
由于我觉得ASP.NET AJAX 4.0 Preview 4很快就要出来了,所以我也就不准备去深入研究Preview 3了,等Preview 4出来了再去好好看看源代码。如果你有兴趣关注这方面的技术文章,欢迎订阅我的博客,点击侧栏上的订阅链接就可以了。
Sys.UI.DataView
为了解决展示数据的问题,我们需要用到一个全新的客户端控件,那就是Sys.UI.DataView
了,简称DataView
。我们会用DataView
替换掉上一篇文章中所说到的人手拼接HTML的部分,用于迭代生成一个ul中的li元素,因此看起来是把DataView
当作Repeater
来用。实际上,DataView
的功能类似于ListView
加上DetailsView
。如果你把一个Array绑定到
DataView
上,它会显示为一个ListView
。与ListView
的LayoutTemplate
相类似的是,它也能够定义控件展示的整体布局,并且仅仅是迭代输出其中的一小部分。例如说,编写一个有thead的table,并且仅仅是迭代输出thead之后的tr。在这方面,是DataView
和ListView
完全一致的。唯一不同的是,客户端暂时还没有DataPager
这样的控件,所以DataView
必须一次性的完整显示整个Array的数据。如果你把单个Object绑定到
DataView
上,它就会显示为一个DetailsView
。这使得你可以使用两个DetailsView
就做出经典的Master-Details展示模式,和在服务器端分别用ListView
和DetailsView
做出来的一样。当然,你不能指望DataView
能够好像DetailsView
那样,自动帮你分析每一个数据项并映射出对应的HTML模板,因此HTML 模板还是要你自己手写的,但至少那是在HTML中编写模板,编写时能够享受IDE带来的各种好处,维护时也更容易看懂自己(或别人)原来写下的 HTML。JavaScript语法
接下来,我们就要把DataView
投入到实际应用中去了。首先,我们说一下如何用JavaScript来实例化一个DataView
。有编写ASP.NET AJAX客户端代码经验的人对$create
应该不会觉得陌生,因为DataView
继承自Sys.UI.Control
,我们仍然可以用$create
来实例化它。不过,在此之前,我们先要把对应的 HTML编写好:<ul id="itemTemplate" class="sysTemplate">
<li>
<span class="award">{{ Award }}</span>
<span class="winner">{{ Winner }}</span>
<span class="film">{{ Film }}</span>
</li>
</ul>
然后我们就可以基于itemTemplate这个HTML元素创建控件了:
$create(Sys.UI.DataView, {
dataSource: new Sys.Data.AdoDataSource(),
serviceUri: "WebDataService.svc",
query: "OscarWinners"
}, {}, {}, $get("itemTemplate"));
现在,页面显示出来的结果和之前我们人手拼接HTML的版本完全一致,不过我们已经不在需要维护嵌入在JavaScript中的HTML代码了。
声明性语法
如果你觉得上面的做法还不够好,要在pageLoad()
里面写一个$create
,那么声明性语法可能正是你需要的。大家应该记得很久很久之前,在ASP.NET AJAX还叫做Atlas的时候,就已经有过声明性语法的设计,那就是xml-script。不知为何,后来Microsoft放弃了这一设计,现在声明性语法又回来了,而且设计得比以前的xml-script还要更好。假如不用$create
的话,通过声明性语法实例化一个DataView
仅需要这样做:<body
xmlns:sys="javascript:Sys"
xmlns:dataView="javascript:Sys.UI.DataView"
sys:activate="*">
<ul id="itemTemplate" class="sysTemplate"
sys:attach="dataView"
dataView:datasource="{{ new Sys.Data.AdoNetDataSource() }}"
dataView:serviceuri="WebDataService.svc"
dataView:query="OscarWinners">
<li>
<span class="award">{{ Award }}</span>
<span class="winner">{{ Winner }}</span>
<span class="film">{{ Film }}</span>
</li>
</ul>
</body>
我们所需要更改的代码包括:
- 在body元素上声明有关的xmlns,将JavaScript中的名字空间映射到HTML上,或者你可以理解为映射到XML/XHTML上。
- 通过sys:activate="*"这个声明,让ASP.NET AJAX知道它需要去解释页面上所有的声明性语法,并激活对应的组件。
- 将原本使用
$create
初始化时传递给实例的属性、事件、引用改为用声明性语法,直接写在HTML元素的定义上。
经过这三步,我们就可以将原来使用
$create
创建的组件改为使用声明性语法创建了。小结
DataView
使得我们能够使用HTML模板,来避免手工拼接HTML带来的种种问题,同时声明性语法让我们能够如同声明服务器端控件一样声明客户端组件。虽然在 ASP.NET AJAX 4.0 Preview 3中这些功能仍有小bug,例如我想用声明性语法创建我自己编写的InPlaceEditoBehavior
,这在初始化阶段毫无问题,但却会在页面卸载销毁对象之时抛出脚本错误。由于我觉得ASP.NET AJAX 4.0 Preview 4很快就要出来了,所以我也就不准备去深入研究Preview 3了,等Preview 4出来了再去好好看看源代码。如果你有兴趣关注这方面的技术文章,欢迎订阅我的博客,点击侧栏上的订阅链接就可以了。
订阅:
博文 (Atom)