2011年12月30日星期五

Jawbone UP 测评

感谢 Peter Fang 和他朋友的帮助,我星期一晚上拿到了我的 Jawbone UP。之前我一直对健康类的数码产品观望,最终 UP 引起了我的兴趣,因为它的设计理念足够好——只要你把它戴在手腕上,它自己就能够工作,你根本不需要注意到它的存在。这种纯被动式的产品往往能带来较好的体验,至少不会需要你去折腾。尽管它也有不完美的地方,例如不支持 Fitbit 那样的无线同步,因此每次同步时还是要脱下来的。

UP 发布后,各大网站对它的评价都不错,例如说 The VergeSummer Tomato。随之而来的是,用户反馈说他们的 UP 变砖了(尽管它的形状看起来无论如何都不像块砖),这确实影响了我对 UP 的信心。不过我想,既然变砖的都可以找 Jawbone 免费更换,那买一个回来试试也好嘛,如果真的出问题了再看看谁方便帮忙拿去更换咯。$100 对于一个健康数码设备来说还是可以接受的,于是我就下决心要买一个。

UP 到手后第一件事情自然是拆包装。iPhone 的 app 我早就装好了,但不把 UP 接上 app 就拒绝你使用。UP 的包装设计得还可以,没有任何难以拆开的地方,附件只有一本说明说和一个充电转换头。我把 UP 插到 iPhone 的耳机口后,app 就允许我注册了。首先需要注册一个账号,然后填写你的性别、生日、身高、体重,顺便让你设置你的目标(例如一天睡 7 个小时,走 5000 步,吃 2 餐能让你充满能量的)。完成向导后,你就可以开始使用 UP 了。首次同步时我的 UP 有 49% 的电量,所以我也没去充电,先用一晚上再说。

硬件

UP 的设计很简单,只有一个硬件按钮和两个指示灯,指示灯也仅仅在状态切换时亮一下,以表示你切换到什么状态了。第一天晚上我为了测试运动模式,在睡觉前使用 GAIN Fitness 运动了半小时。在运动前,先双击并按住按钮,UP 就会提示你它切换到运动模式了。接下来的运动过程 UP 会记录下来,不过由于它是戴在手腕上的,那意味着它只能记录上肢有活动幅度的运动,如果你戴着它做俯卧撑它就记录不了什么了。由于 GAIN Fitness 提供的是室内运动项目,包括热身、几组练习、放松,UP 的设计使得它认为热身是最激烈的部分,而放松(主要是瑜伽动作)是最轻松的部分。

UP 的另一个模式是睡眠模式,点击并按住按钮 UP 就会显示蓝色月亮,并且震动提示你模式切换成功。在睡眠模式里,它会跟踪你的上肢运动以便跟踪你是否在深度睡眠。智能闹钟能够利用这点避免在你深度睡眠时叫醒你,而且唤醒的方式也十分有趣——就是让 UP 震动起来。唯一不足是这个闹钟不支持贪睡模式,一旦点击按钮结束震动,就算你继续睡 UP 也不会再尝试叫醒你。

最后,UP 的一般模式只会跟踪你的步数。由于它不像 Fitbit 一样夹在衣服上,所以在你刷牙时它会记录非常多的步数,而在你端着一杯水走时我猜它会记录不到步数。这 UP 为便利性牺牲准确性的设计,不过我觉得如果它能成功刺激你多走动,效果就已经达到了,我那 $100 也不是白花了。此外,UP 的活动提醒功能也能够刺激你多活动。在每天指定的时段内(通常就是坐在办公室的时段),只要你连续若干分钟不活动(我设置为 30 分钟),UP 就震动一下提醒你起来活动活动。

软件

UP 的软件设计显然不如硬件优雅。尽管界面和交互设计得还可以,不过细节处理得还不是很好,性能尤其不行。如此简单的一个界面,在后台打开应用程序多时滚动起来还会卡,我实在想不明白为什么它需要如此多的资源。当然,如果你愿意为了 UP 而关闭所有后台应用的话,这就不是个问题了。

UP 的主界面显示你今天的目标完成度。例如说,如果你设置了一天要走 5000 步,它就会告诉你 4000 步只完成了 80%。在这个界面点击一下,或者旋转屏幕,UP 就会切换到时间轴界面,显示 UP 跟踪的历史记录。

UP 本地会缓存一天到两天的历史记录,再往前滚动的话就需要重新获取数据进行绘图。在这个图表里面,你可以清楚看到自己的活动分布是怎样的——在什么时候进食,在什么时候运动,在什么时候走的路比较多。通过这个图表,你可以反思自己的生活是否合理,然后调整自己的生活习惯。在这个图表上点击某一个时间段的话,可以查看该时间段的详细信息。

睡眠记录把你的睡眠时间划分为深度睡眠、浅层睡眠、清醒三个状态,通常进入睡眠模式后都会有一段时间的清醒然后才能入睡。睡眠质量分数我猜想是跟深度睡眠时间长短有关的,至于比例是否有所影响我暂时不确定。

运动记录同样是分成三个状态:剧烈、中等、轻度。我的猜想是,这跟你手臂活动的幅度和频率有关系,但暂时没有通过实验去证明,因为我不想把 UP 脱下来专门做实验并且污染我的数据。

其他

尽管 UP 说自己有一米的防水深度,并声称洗澡的时候也不用脱下来,但考虑到那么多 UP 变砖的事件我还是尽量让它不碰水。洗手的时候我尽量不会弄湿它,洗澡的话干脆就把它先脱下来。

UP 还有带 GPS 的运动功能,不过 GPS 实际上由 iPhone 提供,只是把 UP 的数据一起合并到 app 里面去。考虑到北京的空气质量如此糟糕,我是肯定不会进行任何户外活动的,还是等我回广州后去滨江跑步时再测试这个功能吧。

更新:实测表明,UP 的电力确实能够支撑至少 10 天。我充满后用了整整 10 天,电量还有 20% 剩余。

2011年12月13日星期二

前端工程师的职业发展路线在哪?

我猜想国内很多前端工程师都想过这个问题吧。前端工程师往往属于产品研发团队,但却很容易被边缘化——后端工程师觉得自己才是主力,没有后端工程师产品就不存在了,但没有前端工程师产品还能有,只是界面非常糟糕而已。这时候前端工程师就开始感觉自己像是个外包似的,只是来帮别人完成一些任务而已,对产品没有归宿感。这时候前端工程师的职业发展路线在哪?成为一个更好的外包吗?

要做关键任务

我觉得,要别人重视你的工作,不仅仅是你做得好就行了,还要求你的工作对别人来说足够重要。这跟产品定位有关——例如说对搜索引擎来说,前端对产品的影响不会非常大,用户只要能搜索到自己想要的结果就行了。搜索引擎最复杂的交互可能就是搜索框的自动完成了,但有自动完成和无自动完成的区别到底有多大呢?跟准确率和召回率相比,有没有自动完成实在没有多重要。况且,自动完成的结果本身也依赖于准确率和召回率,所以后端工程师比前端工程师重要得多。

因此,前端工程师在选择工作时首先要选择前端足够重要的工作。重要用什么来衡量?务实的话,是钱;务虚的话,是产品。如果一个功能只能在前端实现,并且这个实现能够提高多少的转化率,使得多少原本不产生利润的点击产生利润,那么前端对这个产品来说一定十分重要。可惜往往跟钱相关的事情不由前端工程师来研究和决定,所以这部分工作还是安心交给产品设计师来做吧,让他们来决定怎么样的产品能赚钱,然后由你来完成这个产品的实现,这时候你的目标就是把产品做好。

回到刚才的问题,有些产品更依赖于后端,例如搜索引擎,当然也有些产品更依赖于前端。什么样的产品更依赖于前端?就是后端难以建立起技术壁垒的产品。这类产品要抄袭一个功能差不多的并不难,因此只有细节做得最好的能够获得足够多的用户。这类产品在 iOS App Store 上很常见——有很多 app 拥有相似的功能,而其中只有一个交互设计得最好的能够获得绝大多数的用户。尽管 app 不存在 HTML + CSS + JS 这个前端,不过道理是一样的。当年 Tweetie 能够取代老牌的 Twitterrific 成为主流 Twitter 客户端,靠的就是交互上的创新,外加不差的性能和稳定性。如果交互对于一个 web app 来说十分重要,这个 web app 自然也就需要十分优秀的前端工程师。

总结一下,由于前端工程师的价值在于实现复杂的前端细节,因此如果可以选择的话尽量选择一个细节决定成败的产品。如果产品的成败已经由后端工程师决定了,例如某某数据规模要么能做要么不能做,那么这个产品就没你什么事了。

要懂核心业务

每一个公司,每一个项目,都有它的官方语言。不是指普通话,也不是指 C++,我指的是大家围绕什么问题来展开项目,什么问题的讨论能让大家为之兴奋。举个例子来说,百度的官方语言就是搜索,跟搜索没有关系的产品也会使用「准确率」、「召回率」这样的术语用来做比喻。前端工程师有多少知道什么是「准确率」、「召回率」的?估计不多,因为前端根本没有这样的概念。这时候前端工程师要跟后端工程师沟通也就不容易了。久而久之,你对人家很兴奋在讨论的什么 O(1) 还是 O(n) 不感兴趣,人家也不理解你的 {} != {} 是什么意思,你就被边缘化了。

如果不想被边缘化,就算前端不是公司的核心业务,你也必须懂公司的核心业务,然后说着官方语言,而不是前端的方言。这就意味着,如果你在一家后端技术很强大的公司,你最好也懂后端技术。我知道国内有很多前端工程师并不是计算机系毕业的,就算是国内的教育也不怎么样,这时候你只能恶补相关的基础知识了。如果你不懂这些,就算你能把整本《JavaScript 权威指南》背下来,你说的还是方言,说官话的人还是会鄙视你。如果公司主要服务于某个垂直领域的话,你必须对这个垂直领域十分了解,随时能用这个领域的行话来沟通。

总结一下,由于每个人已经熟悉的领域都不一样,所以没办法说哪个领域更适合前端工程师。如果你原本已经有某个领域的从业经验,进入服务于该领域的技术公司总是有显著优势的。如果你进入了一个自己不熟悉的领域,那就一定要补充相关基础知识,否则你对这个领域不感兴趣,这个领域也不会对你的前端工作感兴趣。

实际例子

为什么我选择加入豌豆荚?主要考虑的还是上面两点。

我在百度的时候一直就在想,既然前端对搜索引擎来说不重要,那对什么类型的应用来说比较重要呢?当时看到 Facebook 做得不错,所以觉得社区会需要复杂的交互,而如果复杂交互做不好则会影响用户使用,因此前端对社区来说应该十分重要。现在看来,也不完全是这样子。前端对社区来说确实重要,但 Facebook 并不是一个典型的例子,它是一个前端做得尤其优秀的例子。

在我了解到豌豆荚 Windows 客户端的实现方式时,我立即意识到它可以通过我的第一个判别标准——前端对它来说是关键任务。它使用 Webkit 做了一个容器,然后把所有的交互都通过 web app 的形式做在里面,然后通过一组接口跟 native 进行交互。如果一个应用决定要这样做了,那么前端就能影响到它的成败,因为这时候前端后端的分隔线已经很明确了。如果一项功能应该由前端来做那就必须由前端来做,后端基本不可能成为实现此项功能的备选方案,这时候前端就具备了无可替代的位置。

至于第二个判别标准——豌豆荚的核心业务是什么?我觉得豌豆荚做的很多事情都是以产品设计为起点的,而这至少是我感兴趣并且也有点感觉的东西。从细节上来说,就是大家喜欢谈论的事情是一致的,例如产品如何做一些很智能的设计,最新的技术方案如何能够巧妙地帮助这些设计得以实现。Junyu 说「设计就是创造性地解决问题」,这是我喜欢的解决问题方式。这个世界上能够把逻辑转化为代码的人非常多,同时有一定数学和计算机专业基础的人也不少,因此要拼谁的解决方案更好的话那还要加上创造力。

我知道国内有很多产品设计师,在考虑产品时首先想到的是百万千万级用户量,这样无论从单个用户身上赚到的钱多么的少,最终产品还是能赚大钱。百度曾经就属于这种思维方式,但这不是我喜欢的风格,因为没有明确的目标用户定位。我知道国内由很多工程师,在编写代码时用尽各种技巧以展示自己过人的才智,但是这样的代码还有可复用性吗?除了作者本人没有人能够维护啊。不同的人有不同的品味,能够跟品味一致的人一起工作是一件幸福的事情。

额外信息

这个话题到此就结束了吗?其实不是的。关于前端工程师的职业发展,我还有很多可以说的。不过我觉得找到一份让自己满意的工作必然是其中的第一步,因为你必须对工作充满兴趣,然后才能把事情做好,所以我把这部分内容放在最前面并且先发出来了。如果你不想错过后继讨论的话,欢迎订阅本博客。

此外,豌豆荚现在还招前端工程师,包括全职和实习,有兴趣的可以联系我:catchen@catchen.me。对于全职的前端工程师,我期望你熟悉 web app 的开发与调试,如果我让你手写一个 HTTP GET 请求你连个大概都写不出来,那我就要怀疑你平时都有多少时间对着 debug console 调试 AJAX 代码了。对于实习生,我期望你至少有扎实的 web page 基础,能够用简洁的代码实现符合语义的页面。至于豌豆荚提供什么?就是我前面所说的,但还有个前提——至少我们要有一致的品味。

2011年12月12日星期一

看对的书 (Part 1 - Tribal Leadership)

很久很久之前(准确来说是两年前),我开始了一个系列叫做「看对的书」,然后只写了一篇 intro 就没有写下去。背后的原因包括,我确实越来越懒了,同时写书评也不容易——剧透太多了,大家觉得没必要去看原书;剧透太少了,大家觉得看不出书的吸引力在哪里。如何把握剧透多少,怎么剧透才能吸引大家取看原书,我还没有想好,不过我可以先尝试着写。

我今天想要推荐的书是《Tribal Leadership》,也就是《部落领导力》,不过这本书暂时没有中文版。尽管这是一本讲「领导力」的书,但里面说的并不是你要做什么事情才能拥有领导力。书中说得更多的是部落——只有在你理解到部落的 5 个阶段后,你才能够使用你的能力把你的部落往更高的阶段迁移。那到底什么是部落了?不知道是否还有人记得「为什么早期手机只能存储 150 个联系人」的问题,答案是 150 是 Dunbar's number邓巴数),这个数是一个人能够维持的熟人数量。而一个部落,就是有 20 到 150 个熟人组成的组织。如果一个组织的人数超过 150 人,它会自动分裂为多个部落,从而形成一个由部落组成的部落。

在部落形成后,你就可以尝试去识别他们到底处于哪一个阶段。这本书的作者发现了一个有效识别部落所处阶段的方法,就是看部落成员说话的方式。例如说,如果你在医院的电梯里听到 3 个医生在交谈——第一个医生说「你看到我在《新英格兰医学杂志》上发表的文章了吗?」第二个医生用讽刺的语气说「看到了,印象深刻。然而在你忙于做研究的时候,我完成的手术比楼层内的任何一个人都要多。」大家笑了一笑,第三个医生接着说「当你忙于码字的时候,当你忙于切肉的时候,我教授的新人比本院的任何人都要多。」大家又笑了,互相拍拍肩膀走出电梯。这 3 个医生所说的话,背后的意思都是一致的——「我比你牛,而且我有数据支撑。」这时候,你就可以认定这是一个处于第 3 阶段的部落了。

部落的 5 个阶段对应的话语都很有特色,所以要鉴别一个部落处于哪个阶段并不难:
  1. 「人生就是个悲剧」——第 1 阶段的人认为没有谁的生活是好过的。
  2. 「我就是个悲剧」——第 2 阶段的人看得到别人生活美好的一面,只是觉得自己的生活很糟糕。
  3. 「我很牛,但你不是」——第 3 阶段的人觉得自己的生活不错,因为自己很牛,但觉得自己身边的人都不如自己牛,也不愿意给予自己更多支持,因此自己没办法完成更大的目标。
  4. 「我们很牛,但你们不是」——第 4 阶段的人觉得自己的部落很牛,而且竞争对手都不如自己。
  5. 「世界很美好」——第 5 阶段的人只专注于做一些很崇高的事业,无视竞争对手的存在。
就美国而言,有一半的人处于第 3 阶段的部落,而且绝大多数都是专业化的知识工作者,例如医生、律师,当然也包括工程师。由于现代教育体系奖励很牛的小朋友,而不是奖励很会跟别人合作的小朋友,所以大多数专业人士到达第 3 阶段后就难以获得突破,除非他们能够获得顿悟。顿悟的来源通常有两种:要么是明白到自己想要追求的目标实在是太大,无论自己有多牛都不可能实现,所以必须放弃单干的幻想,学会通过支持别人来换取自己的目标得以实现;要么是觉得自己足够牛了,获取更多的个人成就已经变得没意思了,那还不如帮助别人实现他们的目标。只有在不看重个人成就高低的前提下,部落才可能进入第 4 阶段。

此外,美国还有四分之一的人处于第 2 阶段。他们往往有一个处于第 3 阶段的老板,这些老板为了保证自己能够获得最好的职业发展而只雇佣能力比自己低的第 2 或第 3 阶段下属。第 2 阶段的人往往就在这样的老板手下痛苦挣扎,看着老板的生活很好,同时感觉自己生活很悲催,因此发誓将来成为老板的话一定不能这样子。但是,等第 2 阶段的人成为老板并且过上好生活后,他们会进入第 3 阶段,成为他们过去痛恨的老板。这时候,他们就不想放弃晋升带来的冲劲了,希望能够更好地证明自己也是很牛的,同时迅速往上爬。

这背后的道理是,人的行为变更是不可能跨越阶段进行的。书中甚至把每一个阶段分成了 3 个子阶段——初期、中期、末期。你不能指望一个人在脱离第 2 阶段后能够立即拥有第 4 阶段的意识,放弃个人成长优势而去帮助他人。因此,如果你想要成为一个部落领袖的话,你需要能够鉴别部落所处的阶段,同时你自己要能穿梭于不同的阶段,去到部落所处的阶段然后把部落带到更高级的阶段。至于每一个子阶段的特征是什么,以及如何能够帮助部落迁移到更高的阶段,我在这里就不详细解释了,大家去看书中的内容吧。如果不愿意看书,到 Tribal Leader 官网上下载有声读物也可以,而且还是免费的,只不过需要注册一下。如果买书的话,我推荐买 Kindle 版,因为看起来方便,不用想方设法把一本进口书弄回国内。

我之所以选择推荐这本书,是因为我很认同里面所说的部落阶段划分。我经历过不少第 3 阶段的部落——名校、名企大多如此。我知道,自己再牛能做的事情也有限;但是,我不知道如何能够突破。这本书解释清楚了一个问题——个人要突破,必须先放下职业发展的得失。然后还要推动整个部落突破,这需要部落拥有核心价值观和高尚的目标,同时制定达成高尚目标的战略。我相信国内也会有很多优秀的工程师卡在第 3 阶段找不到突破口,所以我建议大家读读这本书看看有什么启发。

2011年12月4日星期日

工具:开发者使用,企业埋单

我喜欢写一些小工具来简化我的工作,通常是一些小组件。利用这些小工具我可以提高自己代码的可读性,同时维持我的 DRY (Don't Repeat Yourself) 洁癖。工具对我来说很重要,因为时间对我来说很宝贵。能够用工具自动化完成的事情绝对不手工反复操作,能够用工具避免人为错误的地方一定让工具来确保质量。使用工具节省下来的时间用于玩游戏的话,绝对是值得的!

过去我主要做工具给自己用,或者是给自己所在的团队使用,所以觉得自己就是用户,自己设计产品给自己用肯定是没问题的。然而做工具给更多人用呢?看起来就不那么容易了。之前在百度经历过 Tangram 的需求分析与设计阶段,每个产品线都派出了前端工程师代表参与需求评审,通过多轮的辩论来决定哪些需求是要覆盖到的,哪些需求是可以忽略的。最后做出来的产品?只获得了一部分产品线的认可。推广也得不到公司层面的支持。

工具的需求难道不是来自使用者吗?使用者就是坐在自己隔壁并且做在同一领域开发的团队,这样都可能出现需求理解偏差吗?这个问题我一直没有想明白。

后来我到了 Yahoo,在对 YUI 表示恶心一端时间后,我开始理解到恶心背后的原因。无论是 Yahoo YUI 还是 Google Closure,目的都是一样的——通过工程性来保证,就算工程师素质很低,产出的代码质量很差,项目也能交付。我当时只是觉得大公司为了交付而造出一些压制工程师创意的工具是很恶心的事情,而没有考虑到这是否跟我要思考的问题相关。

到了豌豆实验室后,我开始着手构建一些前端工具,这一次我终于明白到需求源自于哪里了。工具确实是给开发者使用的,而需求来源于企业。企业在不同时期可能有不同的需求。企业刚刚成立时,肯定希望尽快把产品概念做出来,所以要求工具能够快速实现产品原型。等到产品成功发布后,企业又希望能够拓宽产品的市场,于是要求工具能保证产品复杂度增加的同时维护成本可控。随后企业会同时开发多个产品,这时候就会要求工具能够帮助降低产品共有的成本。

在文化相对开放的企业,工具的选择看起来是由开发者决定的。开发者爱用什么工具就用什么工具,企业不干预。实际上,企业不是因为文化相对开放而不干预,而是缺乏干预的动机。如果提供足够的动机,例如使用某种工具能够使得利润翻倍,任何企业都会愿意为之而干预。不干预的企业,往往只是没看清楚工具能给它的成本造成什么影响而已。

此外,在企业决定干预开发者使用什么工具时,企业肯定不会在乎工具使用起来爽不爽。开发者不爽是一种成本,但把这项成本计算在内后,如果特定的工具还是能降低企业成本,企业还是会选择这种工具。至于开发者不爽的成本,是将来可以通过优化工具来避免的。

回顾之前百度 Tangram 推广不利的问题,答案就显而易见了。为什么 Tangram 缺乏百度自顶而下的支持?为什么作为同类产品的 YUI 却能得到 Jerry Yang 的支持并在整个 Yahoo 得到推广?因为 Tangram 覆盖了开发者的需求,但没有很好地考虑到百度作为企业的需求,尤其是没有考虑到这种需求在百度成长中的变化。

Tangram 在设计初期,考虑的是如网页搜索这样的产品线的需求,要求 JavaScript 尽可能优化,少占用带宽。于是 Tangram 设计为支持函数级别的依赖项。然而等 Tangram 做出来时,贴吧则决定直接使用 jQuery。这是因为网页搜索和贴吧的成长进入了两个不同的阶段,网页搜索要求在细节上继续优化,而贴吧要求通过快速迭代尝试不同的产品概念。需求差异如此之大,一个工具又怎么可能都覆盖到?既然 Tangram 没办法在多个产品线证明它能有效降低成本,百度又如何下决心为它埋单?

这同时解释了一个问题:为什么企业内部工具应该由架构师来主导。(YUI 及后来的 Cocktails 都是由架构师主导的。)因为架构师的职位能让他更好地解企业的需求,同时他的位置也更靠近老板。此外,架构师在一家企业内的资历也会对此有帮助。跟随这家企业成长的经历能让他更好地看到这家企业将来的需求。因此,企业内部工具不是随随便便挑一个技术足够好的人来主导就行的,他的职位会影响到他是否胜任此项工作。

对此我可以补充一点信息。我见过 Tangram 推广所用的幻灯片,里面堆满了对开发者有用的信息——Tangram 在 gzip 前后跟其他库的体积比较、能够覆盖到多少功能点、在百度产品线的代码覆盖率能够达到多少。(代码覆盖率还曾经是 Tangram 的执行目标。)然而里面缺乏对老板有用的信息——使用 Tangram 能够省多少钱。由于位置的缘故,做 Tangram 的团队确实缺乏获取相关信息的途径,也更没办法获取到老板对不同产品线的长远规划。

再来看看 Yahoo 架构师 Bruno 是怎么介绍 Cocktails 项目的。幻灯片里面没有一点开发者关心的信息,前半部分说的都是 Yahoo 接下来的战略——必须同时抢占桌面、平板、移动 3 个平台,后半部分讲的是如果为这 3 个平台独立开发应有成本有多高,而使用 Cocktails 的话能够缩减多少成本。我听 Bruno 讲完后,就立即明白到这其实是用来向老板要钱的幻灯片,只是顺带再讲一遍以便让开发者知道公司战略而已。

老板实际上不明白也不在乎开发者对工具的需求是什么,幻灯片上他能看懂的就是那一堆 $ 后面跟着的数字。如果你给不出这堆数字,老板对工具的态度就会是既不支持也不反对。因此,找一个足够了解公司现有数字同时知道如何利用技术去改变这些数字的人,对内部工具项目来说尤其重要。这个人往往就是个架构师。

2011年8月8日星期一

「云端 JavaScript 漫游指南」

7 月 30 日在 w3ctech 的 JavaScript 活动广州场 讲了一节「云端 JavaScript 漫游指南」,实质上就是 Node.js 入门讲座。希望通过这一节讲座,让原本熟悉 JavaScript 的前端工程师尝试使用 Node.js 开发一些应用,探索 Node.js 为 Web 开发带来的可能性。

这场讲座上主要使用了两个小 demo 来解释 Node.js 的一些基础概念,并且展示了如何把自己的应用部署到云端。需要使用 Node.js ,最起码你要知道 CommonJS Modules 的概念,然后你才能在一个 js 文件里面使用另一个 js 文件实现的功能。这是通过 requireexports 这两个接口实现了。接下来,你还应该知道 CommonJS Packages 的概念,这样你就可以复用别人写好的包,无需样样从头开发。跟现在非常热门的脚本语言如 Python 、 Ruby 一样, Node.js 也有丰富的包仓库,基本上你能想得到的基础功能都有别人做好的包可以实现。

在有了一个好的产品创意后,你就可以以搭积木的形式构建自己的应用了。由于基础功能都有做好的包了,你可以专注于应用的业务逻辑,快速把产品做出来。在我的讲座中,我讲解了两个我自己编写给自己使用的小应用,一个用于把我的 catchen.biz 链接全部重定向到 catchen.me 去,另一个用于将短地址解析为原地址。关于这两个应用的技术细节我会在将来的文章中解释清楚,在这里我们就先跳过这部分的内容吧。

产品做好后,最后一步就是部署。过去我们需要购买或租用服务器,至少租用虚拟主机, OS 级别的事情还是要自己动手处理的。要在一个全裸的 OS 上把 Node.js 跑起来,还是要安装和配置一堆东西的。幸好现在有了如 Heroku 这样的云端解决方案,我们可以完全不管 OS 上跑着的是什么,把写好的 Node.js 做成包推到目标 Git 库上就可以了,所有的依赖项和配置都不需要自己来管理。

上述讲座内容都可以在我的 Github 上找到,包括幻灯片以及 biz-to-metraceurl 这两个 demo 。下面的是讲座现场的视频: YouTube | Youku

关于视频的任何问题,你都可以留言提问。如果你关注 Node.js 开发,欢迎订阅我的博客

2011年7月7日星期四

为什么 script 标签不能写成自关闭形式

今天早上在 Stack Overflow 看到了这个问题: Why don't self-closing script tags work? 。答案给出的解释是,在 XHTML 的标准里面规定非 EMPTY 标签不能使用自关闭形式。注意这里使用的是全大写的 EMPTY ,所以我不把它翻译为「空白」。

那么 EMPTY 到底是什么呢?写过 DTD 的人应该知道它是个关键字,用来指明一个标签的内容必须是空白,而不能包含文本内容或子节点。看看具体的例子就很容易明白了:

<!ELEMENT img EMPTY>
<!ATTLIST img
  %attrs;
  src %URI; #REQUIRED
  alt %Text; #REQUIRED
  longdesc %URI; #IMPLIED
  height %Length; #IMPLIED
  width %Length; #IMPLIED
  usemap %URI; #IMPLIED
  ismap (ismap) #IMPLIED
  >


这是 img 标签的定义。 ELEMENT 关键字说明它是一个元素, EMPTY 关键字说明它的内容必须是空白。因此,我们可以使用自关闭形式:

<img src="image.png" alt="some image" />

留意 ATTLIST 里面声明了两个属性是 #REQUIRED 的,所以必须提供。

接下来我们再看看 script 标签的定义:

<!ELEMENT script (#PCDATA)>
<!ATTLIST script
  id ID #IMPLIED
  charset %Charset; #IMPLIED
  type %ContentType; #REQUIRED
  language CDATA #IMPLIED
  src %URI; #IMPLIED
  defer (defer) #IMPLIED
  xml:space (preserve) #FIXED 'preserve'
  >


可以看到 script 标签通过 (#PCDATA) 声明了它的内部允许包含 CDATA 数据,因此它不是一个带 EMPTY 关键字的标签,也就不可能使用自关闭的写法。

总结一下:有空看看 Stack Overflow 还是挺有意思的。学习 DTD 的语法,并且看看 XHTML 1.0 Strict DTD 也会帮助你增加对 XHTML 的理解。

2011年6月24日星期五

如何购买 Amazon MP3 音乐

之前写过《如何购买 Amazon Kindle 书籍》,后来还发了一个更新版本。现在来说说如何购买 Amazon MP3 。

为什么要上 Amazon MP3 购买音乐呢?因为它比 iTunes Store 便宜,而且全部都是 DRM Free 的。例如说,同样是 Inception 的原声, Amazon MP3 的价格是 $5.99 ,而 iTunes Store 的价格是 $10.99 ,价格机会差一倍。

现在要购买 Amazon MP3 音乐,方法就跟最初买 Amazon Kindle 书籍一样。如果你要使用中国的信用卡就必须先买 Gift Card 给自己,然后把自己的地址和信用卡信息删除干净,再用 Gift Card 的余额来买音乐。

这个操作其实十分麻烦,因为你要不停地给自己的账号增删地址和信用卡信息。我就遇到过购买 Amazon MP3 音乐后忘记把信用卡信息加上去了,结果后来在 iPhone 上购买 Amazon Kindle 书籍时发现购买不了,然后又要拿信用卡然后在 iPhone 的小屏幕上输入上去。因此,我个人的建议是分开两个 Amazon 账号:一个有信用卡信息,专门负责购买 Gift Card 发给自己;另外一个只有一个美国地址,没有信用卡信息,专门消费 Gift Card 余额。

2011年5月29日星期日

在 JavaScript 中监听 IME 键盘输入事件

在 JavaScript 中监听用户的键盘输入是很容易的事情,但用户一旦使用了输入法,问题就变得复杂了。输入法应当如何触发键盘事件呢?是每一下击键都触发一次事件,还是选词完毕才触发事件呢?整句输入又该如何触发事件呢?不同的操作系统和不同的浏览器对此有不同的看法。在最糟糕的情况下,用户使用输入法后浏览器就只触发一次 keydown ,之后就没有任何的键盘事件了。这对于 Suggestion 控件的实现来说是个大问题,因为 Suggestion 控件需要监听文本输入框的变化,而事件是最准确也最节省计算资源的做法,如果换成轮询的话性能就可能受到影响。

首先,要监听启用输入法后的击键事件应当使用 keydown 事件,这是信息最丰富的一个事件,因为在启用输入法后别的键盘事件可能不会被触发。其次,大多数操作系统和浏览器都实现了一个事实标准,就是在用户使用输入法输入时, keydown 事件传入的 keyCode 取值为 229 。然而触发 keydown 的频率是不确定的,有些情况下每一下击键都触发事件,有些情况下只有选词完毕才触发事件。这时候,如果我们还是要实时监控文本框的内容变化,就必须使用轮询了。

var timer;
var imeKey = 229;

function keydownHandler (e) {
  clearInterval(timer)
  if (e.keyCode == imeKey) {
    timer = setInterval(checkTextValue, 50);
  } else {
    checkTextValue();
  }
}

function checkTextValue() {
  /* handle input text change */
}


Opera 是一款有趣的浏览器,别人做的事情它都不做,别人都不做的事情它都喜欢做。例如说,它偏偏不支持 keyCode == 229 这个事实标准,而要使用 keyCode == 197 来表示输入法的使用。因此,你需要在上述代码的基础上做一下改良,如果监测到是 Opera 浏览器,就换一个 keyCode 常量来做比较。

var imeKey = (UA.Opera == 0) ? 229 : 197;

最后,还有一个更不受重视的浏览器叫做 Firefox for Mac 。估计是因为 Mac 版本对于 Mozilla 来说实在是太不重要了,所以很多 Windows 版本都没问题的地方 Mac 版本就会出小问题,例如说对上述事件的支持。 Firefox for Mac 不会出现 keyCode == 229 的情况,而且在输入法启用后只有第一下击键会触发 keydown 事件,因此只能在击键后一直使用轮询。

if (e.keyCode == imeKey || UA.Firefox > 0 && UA.OS == 'Macintosh') {

在添加了这两项改进后,实时监控文本框的变化就没有问题了,即使用户启用了输入法。完整的代码如下:

var timer;
var imeKey = (UA.Opera == 0) ? 229 : 197;

function keydownHandler (e) {
  clearInterval(timer)
  if (e.keyCode == imeKey || UA.Firefox > 0 && UA.OS == 'Macintosh') {
    timer = setInterval(checkTextValue, 50);
  } else {
    checkTextValue();
  }
}

function checkTextValue() {
  /* handle input text change */
}

2010年12月24日星期五

Tangram 前端库通过 Github 开源了

Tangram 是百度内部一直在开发和使用的前端库之一,功能与 jQuery 、 Prototype 等库类似,主要功能是简化 DOM 操作,并且扩展 JavaScript 语言。这部分功能准确来说属于 Tangram Core ,另外一个叫做 Tangram Component 的库提供一些类似 YUI 、 Sencha 这个级别的组件。

之前 Tangram 说要开源很久了,一直卡在流程上,并且也有人觉得必须把库做得足够好了才好意思拿出来开源。我个人的看法是,跟 John Resig 的一样,前端库应该从第一天开始就开源,因为就算你不开源别人也一样能看到,所以还不如开放出来接受别人的贡献。如果你写得不好,就算你不开源,别人要看也还是能看得到的,所以还是直接把代码晒出来好了,看得不顺眼的可以直接说,实在看不下去了可以动手改,改完了再把代码贡献回来。

说完了我对开源的看法,接下来我们看看 Tangram 和 Git 分别有什么好,先从 Tangram 说起。

Tangram 能做什么

熟悉我的人都知道,我从来不讨论哪个工具更好的,我只讨论在特定的情况下哪个工具更适用。因此,我们来看一下什么情况下 Tangram 是适用的。

Tangram 的总体设计很大程度上是参考了 Mootools 的做法,就是将框架拆散到函数的级别,你可以引用单个函数,而不一定要加载整个库。这样做的好处是节省带宽流量,尤其适用于那些流量很大但 AJAX 功能不多的网站。百度的很多服务流量都不小,而且常用页面上需要的 AJAX 功能也不多,因此 Tangram 成为了一个很好的解决方案。

那么什么情况下 Tangram 不适用呢?如果你要写一个 AJAX Web App , Tangram 就没有什么特别的优势了,除非你尤其熟练使用 Tangram 。一个 AJAX Web App 本身就依赖于库中大量分散的功能,把一个库拆分到函数级别并没有什么意义。当然,在 AJAX Web App 中, Tangram 也没有什么明显的劣势,跟 jQuery 、 Prototype 都差不多,这时候就由团队成员对不同库的使用熟练程度来决定选用哪个库了。

现在 Tangram 的最大弱势在于,它缺乏一种机制让你对页面逻辑的描述变得流畅( fluent ),而这正是我们使用 DSL 时所追求的。过去我也说过 jQuery 是一种 DSL ,它允许你用一种很流畅的语言来描述页面的交互行为,这使得页面交互行为变得很容易管理──读懂别人写的 jQuery 页面并不难,在上面做调整也很简单。这是 Tangram 为了减少下载体积做作出的牺牲,不过我希望它将来可以通过编译工具等方法来弥补这个缺陷──例如说,我还是用某一种 DSL 来描述页面交互,然后这种 DSL 能够被编译为 Tangram 代码。

Git 有什么好

为什么选择开源到 Github ? Git 到底有什么好处?我觉得一个简单的例子就能很好地说明问题。

例如说,你想看看 Tangram 的源代码,那么你可以直接打开 Tangram Github 的首页,然后以只读的方式把代码都复制到本地。

git clone https://github.com/BaiduFE/Tangram-base.git

读着读着,你觉得 Tangram 写得也不是那么好,想改改看。于是你回到刚才那个页面上,点 Fork 按钮,然后就相当于把 BaiduFE 下面的 Tangram 项目整个复制到你个人帐号下了。你当然拥有你个人帐号下 Tangram 项目的完全读写权限啦,这时候你就可以把它复制到本地了。

git clone git@github.com:CatChen/Tangram-base.git

可以看到,这是我的帐号( CatChen )下的 Tangram ,不再是 BaiduFE 下面的。这时候你就可以随意改动啦,改动完提交就是了。

git commit -a -m 'Tangram improvement'

如果你习惯使用 SVN 或者 CVS ,那么你需要注意啦, Git 的提交都是本地的,不会提交到服务器上去。你 Github 帐号下的 Tangram 是一个仓库,你本地编辑的则是另外一个仓库。别忘记了,你刚刚是用克隆命令把 Github 上的仓库复制下来的。所以在提交后,你还必须用推送命令把本地仓库复制回 Github 去。

git push origin master

在这里, origin 是一个远程仓库的别名。因为你本地的仓库是从 Github 上克隆下来的,所以 Github 上的仓库叫做 origin 。默认情况下,仓库只有一个分支,叫做 master ,所以你要把本地仓库推送到这个分支上去。

这时候,你自己的 Tangram 是更新了。如果你希望 Tangram 的官方版本也接受这个更新的话,你可以点击页面上的 Pull Request 按钮,这时候 Tangram 的管理员就可以考虑从你这里把更新拉取到官方版本上去。

如果你在开发自己版本的 Tangram 时,看到别人的 Fork 有更新了,并且也想要那个更新,怎么办呢?你可以主动地从别人那里拉取,然后 Git 就会帮你完成合并。例如说,我发现 Leeight 那里在做的一个 Tangram 升级不错,尽管他还没完成这个升级,也没提交到官方版本中去,但我就可以先把这部分升级拉取到我本地的仓库中来。

git pull https://github.com/leeight/Tangram-base.git

这样子,我就能看到 Leeight 所做的升级,跟我正在做的改动是否能够良好地兼容了。或者,我可以先做一些依赖于他的升级的事情,等他把升级做完了并且被官方版本采纳了,我再向官方版本提出 Pull Request 。

可以看到, Git 对开源项目来说是非常友善的,尤其是跟 SVN 和 CVS 做对比的话。 SVN 和 CVS 尽管允许分支,但分支之后通常要到项目完成时才会进行合并,这时候主干已经发生了很多变更,合并起来就相当痛苦。 Git 允许你分支后随时从别人的分支拉取变更,同时你还可以在自己的仓库内做很多子分支,这就使得开源项目管理变得十分方便了。

小结

说了那么多道理,建议大家还是动手实验一下比较好。试用 Tangram 无需下载,直接创建一个页面然后引用我们放在 CDN 上的脚本即可,然后可以尝试按照入门指南做些简单的东西试试。

使用 Git 管理开源项目的话,推荐阅读 Git 开发管理之道 ,能够让你更好地了解 Git 项目一般是如何进行分支的,以及如何利用这些分支获得更好的灵活性。如果你想看完整的 Git 手册,可以看看 Pro Git 这本书,作者把这本书放到网上并且免费公开了。

最后插播一下小广告,我的 jsHelpers 库也跟随着 Tangram 开源了,大家可以来 Fork ,我很欢迎大家提交 Pull Request 。

2010年12月10日星期五

Tech·Ed 2010 及动手实验室资源下载

今年是第二年以讲师身份参加 TechEd 了,没有了往年的兴奋,认真把工作做好才是关键。 TechEd 对我来说,更多是一种年度聚会,能够跟国内 Microsoft 及社区的朋友见面聊天。

课程

第一天下午到得比较晚,来到的时候 Keynote 快要开始了,赶紧领了讲师的书包和衣服后就去听 Keynote 了。今年的 Keynote 对我来说没有什么吸引力,因为主要是面向 Azure 和 Windows Phone 7 的内容,这两样东西都是面向企业用户的,自己一个人玩没什么意思。 Keynote 后,两个基础课程都没去听,主要还是基于上面所说的原因,自己回到了讲师休息室,继续调整 PPT 。

All TechEd China shirts and bags are from Vancl.

今年的书包和衣服都是 Vancl 赞助的,不过至少拥有 TechEd 徽标。去年的衣服连徽标都没有,讲师都要另外发一个印着 speaker 的别针。此外今年还有 Vancl 赞之的围巾,过去从来没见到过印着 Microsoft 的围巾,这是第一次见到。

今年我要讲两个动手实验室,一个是关于 IE9 + HTML5 的,另外一个则是关于 WCF Data Service + ASP.NET MVC 的。内容我都熟悉,唯一让人担心的是环境。我们使用的材料跟美国 TechEd 的是一样的,但是美国 TechEd 时这些虚拟机都跑在 Azure 上面。不知道是不是带宽问题,中国 TechEd 无法使用 Azure 上面的虚拟机,所以必须在实验室中部署 Windows Server 2008 Hyper-V ,然后在本机跑虚拟机。

我的动手实验室都在第三天,所以我第二天就可以去看看别人讲得怎么样了。 TechEd 对我来说最重要的已经不是内容了,我更倾向于去了解那些讲得非常出色的讲师到底是如何演讲的。此外,由于今年我第一次讲动手实验室,所以我也需要了解一下别人是怎么讲的。一个完整的实验手册,到底有多少应该是我讲的,有多少是应该让大家动手做的,这是我最关心的问题。我围观了王兴明的动手实验室,觉得他的课程安排很不错——先讲一个简单的例子,再让大家动手做两个类似的稍微复杂一点点的实验。

资源

我讲的两个动手实验室,都遇到了虚拟机带来的问题,导致有些人无法完成实验,所以我就向大家承诺资源会发布到我的博客上,以便大家会后再深入学习。

IE9 的实验相对简单,它基本不依赖于任何外部软件环境,只要有一个 IE9 就能做,假若你愿意使用记事本写代码的话。下载包括:实验手册及代码,其中手册中的一个 bug 在我的完成版代码中已修复。(先提供英文手册,中文手册稍后提供,到时候会更新下载包的内容。)

ASP.NET MVC 的实验依赖于 Visual Studio 2010 。由于虚拟机太大了,我不可能提供下载,所以你需要在自己的机器上安装 Visual Studio 2010 后进行实验。下载包括:实验手册及代码。(下载稍后放出。)

花絮

我觉得 Microsoft 的会议组织形式是非常好的,尽管有时候会务公司执行起来会出一些细节问题。 Microsoft 非常懂得如何鼓励大家参与到互动中来,以达到预期的市场活动效果。举个例子来说,去年的 MVP 展台让大家去找 MVP 盖章,今年的做法就更近一步了——你找到 MVP 后需要跟他合影,然后发布到微博上。这使得活动不仅仅覆盖现场的参会者,还让微博上关注参会者的人都注意到 MVP 是什么以及 MVP 都有谁,这是十分成功的推广。MSDN 和 TechNet 的做法也是类似的,只要你发微博就能换取围巾,这能让微博上的微软社区气氛瞬间活跃起来,就算没有来到现场,也能感受到身边的人对 TechEd 的关注。

之前幾天在微博上活動,是為了拿 #TechEd 的禮品~

Baidu 现在也开始尝试组织自己的会议,尽管规模非常小,通常就只有一个 track ,一组话题。学习 Microsoft 成功的会议组织经验是很有必要的,尤其是对于向来就不懂得做开发者关系的 Baidu 来说。在未来的 Baidu 会议中,我们也会尝试学习 Microsoft 的成功经验,让开发者更好地参与到互动中来,体会到乐趣的同时又能赢取奖品。

2010年12月4日星期六

Windows 和 Mac 都支持 ExFAT 了

过去在 Windows 和 Mac 之间交换文件,总要受到文件系统差异所造成的限制。 NTFS 在 Windows 下很好用,但是在 Mac 下面只能读取; HFS+ 则正好相反, Windows 下面只能读取。如果使用 FAT32 的分区作为中转站,则无法容纳超过 4G 的单个文件。

自从 Snow Leopard 升级到 10.6.5 后, Disk Utilities 增加了 ExFAT 这个格式,看了一下 Wikipedia ,原来它就是传说中的 FAT64 ,一个能够支持超过 4G 文件的解决方案。考虑到 Windows 7 内置了 ExFAT 的支持,低版本的 Windows 安装 Service Pack 后也能支持 ExFAT ,我立即把我的移动硬盘切换到 ExFAT 格式。

Disk Utilities

2010年11月22日星期一

John Resig 见面会视频

上个星期 John Resig 来到了中国,谋智网络跟 CSDN 组织了一场 John Resig 见面会。 John Resig 在大会上介绍了 jQuery 的两个新特性(暂时还是插件): Data Link 以及 Templates 。引入这两个插件后,前端的数据呈现及交互将变得更简单,开发数据驱动型 Web 应用的成本也会随之而降低,估计将来我们能够见到更多基于 jQuery 的数据交互型 Web 应用。

此外, John Resig 还花了半个小时讲解 jQuery Mobile 。其中一项核心概念就是,基于 jQuery Mobile 开发的 Web 应用能够优雅地降级,自动适应功能比较弱的浏览器,同时又保证了在一流浏览器上的最佳体验。具体的做法与 YUI 的浏览器分级方法一样,将浏览器分为 A 级、 C 级和 X 级。确保 A 级的到最好支持,同时保证 C 级得到最低支持,此外假设 X 级能够获得一定支持。最后, jQuery 不会专门为了 jQuery Mobile 而多增加一个分支。无论你针对什么浏览器进行开发,都只会有一个版本的 jQuery 。如果有必要,你可以自己到 jQuery 的 github 获取具体子模块的代码并自行组合。


YouTube 高清Youku 高清

如果你对这一类的活动视频感兴趣,欢迎到 Twitter 上 follow 我: @CatChen 。将来有同类信息我将会首先发布到 Twitter 上。

2010年11月16日星期二

如何让你的网站支持 IE9 Pinned Site (Part 2 - 实战)

该如何展示 IE9 Pinned Site 呢?我可以写一个新的应用,完全是为了展示 Pinned Site 的特性,但这样就像是为了实现这些特性而利用这些特性。所以我想还是升级一个现有的网站好了,这样更能说明 Pinned Site 是如何起到优化用户体验的作用的。

我选择了 hack 现在的百度地图。由于 IE9 正式版还没有发布,所以我 hack 的代码暂时也不能发布到线上,只能私下玩玩。在这里,我选择使用 Fiddler 来替换百度地图首页代码,从而实现 hack 的效果。

添加静态信息

添加静态信息是最容易的,只要写几个 meta 标签就可以了。那么我们就把百度地图的相关信息填充上去吧。“任务”里面放什么链接呢?考虑到用户使用地图,通常就是搜索几类信息:地点、公交、驾车、周边,我们就把这几类搜索的快速入口链接放上去吧。不过,由于百度地图本身没有实现这几个快速入口的地址,所以我们需要在 JavaScript 里面实现一些小 trick 。

<meta name="application-name"
      content="百度地图" />
<meta name="msapplication-tooltip"
      content="使用百度地图浏览地图、搜索地点、查询公交驾车线路" />
<meta name="msapplication-window"
      content="width=1024;height=768" />
<meta name="msapplication-task"
      content='name=搜索;
               action-uri=./#json={"type":"poi"};
               icon-uri=/favicon.ico' />
<meta name="msapplication-task"
      content='name=公交;
               action-uri=./#json={"type":"bus"};
               icon-uri=/favicon.ico' />
<meta name="msapplication-task"
      content='name=驾车;
               action-uri=./#json={"type":"drive"};
               icon-uri=/favicon.ico' />
<meta name="msapplication-task"
      content='name=附近;
               action-uri=./#json={"type":"circle"};
               icon-uri=/favicon.ico' />
<meta name="msapplication-navbutton-color"
      content="#2319DC" />
<meta name="msapplication-starturl"
      content="./" />

我选择的 trick 时,在百度地图首页地址后面加上一个锚点,锚点内含一个 JSON ,用里面的信息表明使用哪个分类搜索。在文章的后面会说明如何用利用 JavaScript 识别 JSON 然后做相应的处理,在这里我就不做解释了。

加入了上述信息后,当我们把百度地图固定到任务栏上,就能看到对应的图标和 Jump List 。

Baidu Map Jump List

添加动态信息

接下来我们要添加动态信息。百度地图适合添加什么样的动态信息呢?考虑到用户可能经常需要搜索相同或相似的路线,我们可以把用户的搜索记录保存下来,并放到 Jump List 的一个名为“历史记录”的分类里面。

要实现这样一个分类,首先要记录用户点击“百度一下”进行搜索时文本框内的信息,然后再把这些信息写到 Jump List 里面去。先看看如何捕获用户点击“百度一下”按钮的事件吧。

$('#form1').submit(function () {
  var historyItem = {
    'type': 'poi',
    'word': $('#PoiSearch').val()
  };
  addHistoryItem(historyItem);
  updateJumpList();
});

我们监听表单的提交事件,然后把搜索类型和文本框内的信息保存到一个 JSON 里,然后把 JSON 存到 localStorage ,最后更新 Jump List 。如何把 JSON 保存到 localStorage ,以及在将来从 localStorage 中取回 JSON ,这些都不属于本文的话题,大家可以看看资源下载一节的源代码,这里就不展开讨论了。我们关注的是如何将 JSON 中的数据添加到 Jump List 中去。

var updateJumpList = function () {
  var history = loadHistory();

  try { /* try is for IE9 beta only and RTM will change */
    if (window.external.msIsSiteMode()) {
      window.external.msSiteModeClearJumpList();
      if (history.length > 0) {
        window.external.msSiteModeCreateJumpList('历史记录');
      }
      for (var i = 0; i < history.length; i++) {
        var historyItem = history[i];
        switch (historyItem.type) {
          case 'poi':
            window.external.msSiteModeAddJumpListItem(
              '搜索' + historyItem.word,
              'http://map.baidu.com/#json=' +
                JSON.stringify(historyItem),
              'http://map.baidu.com/favicon.ico');
            break;
        }
      }
      window.external.msSiteModeShowJumpList();
    } else {
      /* it’s not in side mode */
    }
  } catch (e) { console.dir(e); }
};

我们把历史记录读取出来,然后检查现在是否在 IE9 中,再检查现在是否在 Site Mode 中(也就是用户已经把站点固定到任务栏了)。由于 IE9 Beta 的缺陷,无法通过特性检查来得知浏览器是否支持 Site Mode ,所以需要使用 try catch 模式,这将在正式版中修复。

接下来,我们遍历历史记录,根据类型信息组合 Jump List 项目的文本信息和地址,然后把项目添加到 Jump List 上。地址的做法同样是使用锚点加 JSON ,到底这个 JSON 在页面打开时是如何解释的,请大家看资源下载一节的源代码吧,我就不浪费篇幅解释了。

Baidu Map Jump List

这就是完成后 Jump List 的样子。

资源

代码

完整代码示例下载

视频

这是我在北京 PDC Party 做的演讲,里面的内容与文章的内容相当,大家把这当作补充材料。当天去参加了活动的人,也可以在我文章中直接获取到代码。

Youtube 高清
Youku 高清

小结

希望通过这两篇文章让大家了解到,要实现 IE9 Pinned Site 其实并不难,但它可以帮助你提升网站的用户体验,让用户更方便快捷地使用你的网站功能。接下来,我会分享更多关于 IE9 和 HTML5 的内容,欢迎订阅本博客

如何让你的网站支持 IE9 Pinned Site (Part 1 - 理论)

Windows 7 任务栏有何不同

自从 Windows 95 开始,任务栏就一直没什么变化,都是一个个长条表示正在运行的应用程序,直到 Windows 7 。 Windows 7 的任务栏发生了巨大的变化,这个变化其实就是更像 Mac OS X 的 Dock 了。无论是否在运行的应用程序,都可以在任务栏上占一个位置,并且这个位置可以固定下来,让用户可以更方便的操作这个应用程序。从此,任务栏由单向地提供应用程序运行状态信息,变成了双向——用户也能随时随地地操作某一个应用程序。这就弱化了系统托盘的角色,过去要通过系统托盘做的各种交互,现在直接坐到任务栏上就可以了。

我们以最新版的 Windows Live Messenger 2011 来看看, Windows 7 的任务栏都能干什么:

Taskbar

我们可以看到,在 Windows Live Messenger 的图标上,叠加了一个绿色的小图标,表示用户当前在线。过往,这个信息只能显示系统托盘里,现在可以直接在任务栏上显示了。

Live Messenger Jump List

右键点击 Windows Live Messenger 的图标,会出现 Windows 7 特有的 Jump List 。它跟过往上下文菜单的区别在于,开发者能够灵活定制里面的内容。例如说, Windows Live Messenger 就在此处显示“常用联系人”(我已经把我的联系人列表隐藏掉),同时显示若干“任务”。在过往的 Windows 版本中,开发者往往需要定制应用程序图标的上下文菜单,或者定制系统托盘图标的上下文菜单,来达到同样的效果。

Live Messenger Thumbnail

此外,当你把鼠标悬停在 Windows Live Messenger 图标之上时,除了显示预览外,还会看到预览下方的几个快捷按钮。用户可以在不调出 Windows Live Messenger 的前提下更改在线状态。

Internet Explorer 9 如何用上新特性

不要以为上面所提到的交互方式,只有 Windows 本地应用程序能够使用。在 IE9 里面,通过简单的 meta 标签声明,也能定制网站的任务栏图标交互方式。当然这是有前提的,就是用户要把网站固定到任务栏上,也就是所谓的 Pinned Site 。

要让站点变成 Pinned Site 很简单,让用户按着本页的标签或者图标拖到任务栏上就可以了。然后站点就会从普通的浏览器窗口里面脱离出来,成为一个独立的浏览器窗口,并且有自己对应的任务栏图标。

这时候的任务栏图标跟普通的 IE9 没有什么区别,只是图标变成了站点的 favicon (假如有的话)。需要加上新功能的话,就需要额外加一些代码。

使用 meta 标签

首先,我们可以加入如下一组 meta 标签:

<meta name="application-name"
      content="Sample Site Mode Application"/>
<meta name="msapplication-tooltip"
      content="Start the page in Site Mode"/>
<meta name="msapplication-starturl"
      content="http://example.com/start.html"/>
<meta name="msapplication-window"
      content="width=800;height=600"/>
<meta name="msapplication-navbutton-color"
      content="red"/>

他们制定了站点的名称、提示信息、启动地址、窗口大小、导航按钮颜色。

然后,我们可以再加上一组 meta 信息,使得 Jump List 出现“任务”一栏,用于显示用户常用的固定链接。

<meta name="msapplication-task"
      content="name=Task 1;
               action-uri=http://host/Page1.html;
               icon-uri=http://host/icon1.ico"/>
<meta name="msapplication-task"
      content="name=Task 2;
               action-uri=http://microsoft.com/Page2.html;
               icon-uri=http://host/icon2.ico"/>

这使得 Jump List 中出现两个任务,分别拥有对应的图标和描述文字,指向对应的地址。

添加 JavaScript

接下来我们可以添加一些动态内容。这些内容根据当前用户的登录状态,或者用户的浏览历史记录生成,因为无法使用固定的 meta 标签来声明,只能使用 JavaScript 来动态生成。

Jump List 除了固定有一个“任务”栏目外,我们还可以增加一个自定义栏目,里面放进若干个链接(具体数目由用户的 Windows 任务栏选项决定)。

window.external.msSiteModeCreateJumplist('List1');
window.external.msSiteModeAddJumpListItem(
  'Item 1',
  'http://host/Item1.html',
  'http://host/images/item1.ico');
window.external.msSiteModeShowJumplist();

在站点的 favicon 之上,可以叠加一个图标表示状态。可以好像 Live Messenger 那样表示在线状态,也可以用于表示有新消息。

window.external.msSiteModeSetIconOverlay(
  'http://host/images/overlay1.ico',
  'Overlay 1');

此外,缩略图下方的按钮也可以定制,最多放下 7 个按钮。在创建按钮时你能获取到一个 id ,类似 setTimeout 返回的 id ,你要将它保存下来以标识这个按钮。随后,无论是哪个按钮被点击都,都会触发同一个时间,你就需要上述 id 来判别到底是哪个按钮被点击了。

var botton1 =
  window.external.msSiteModeAddThumbBarButton(
    'http://host/images/button1.ico',
    'button 1');
document.addEventListener('msthumbnailclick', function(e) {
  if (e.buttonID == button1) {
  }
}, false);
window.external.msSiteModeShowThumbBar();

最后一个就是,你可以让任务栏图标闪烁以引起用户的注意。

window.external.msSiteModeActivate();

小结

加入上述简单代码,你就能够让自己的站点把 IE9 Pinned Site 的新特性充分利用起来,为你的站点用户带来更好的体验。如果关注接下来的实战部分,欢迎订阅本博客

2010年11月12日星期五

使用 Wacom Bamboo Special Edition 写博客

这篇文章真的是写出来的,我已经很久没用笔写过那么多字了。能让我如此流畅写那么多字,也就只有 Windows 7 能做到了, Mac 完全不行。

星期一下午,我的购物欲爆发了,很想买一个 Magic Trackpad ,于是就去 Apple Store HK 下单买了一个。在不知道 Trackpad 什么时候能送到的情况下,我无无聊聊地在看幻灯片写作的书,突然想起我确实需要一个能把手绘导入电脑的工具。现在做幻灯片的最大障碍在于缺乏好的素材。有些人选择抱一个 DSLR 出门去拍素材,有些人选择上网买别人拍好的素材,我选择一个比较省钱(至少看起来如此)的方案,也就是自己画素材。我不会做很复杂的东西,所以自己画画是足够的。

MIX 09 的时候有一个 Dan Roam 的 session ,叫做 The Way of the Whiteboard ,它让我见识到幻灯片是完全可以画出来的。不需要在编辑模式下写任何的内容,直接创建一堆空白页然后进入播放模式,在上面画满东西,结束放影时保存下来就行了。尽管我已经很久没有画过东西了,更别说接受数字化创作的专业训练,但我还是觉得画画比打字要更加 expressive ,所以我想要尝试一下。

星期一晚上,我跑到 Wacom 的网站上看最新的选择都有哪些。原来现在入门级的 Bamboo 系列都有 1024 级压感了(尽管我不知道这跟 512 级的区别有多大,但我总是觉得 512 级不够用),而且还引入了多点触击的支持。最新发布的一款 Bamboo 产品是 Bamboo Special Edition ,支持 4 点触击,那不就是一块超大的 Trackpad 吗?于是我决定买一块回来当超大 Trackpad 用。

Wacon Bamboo Special Edition 中文名叫做“限量版”,其实根本不限量。官方价格是 ¥1580 ,京东卖 ¥1520 ,不过淘宝上同样的价钱还送一堆东西。我问 @linzheming ,看他中关村的朋友能否以更低价出货,最终我在中关村买了。由于 @linzheming 的朋友一开始搞错了,所以给我发过来一块 Bamboo Fun ,我发现不对就退回去了。在对方把正确的板子发过来之前,我又问了一下他有没有 Buffalo 高功率无线路由器。他说有,于是我又要了一个。总价 ¥1880 ,感觉非常划算。

Unboxing Bamboo Special Edition

最终板子和路由是星期四下午到的,我先把路由配置好,中途还插了个面试,面了个女工程师,然后再回来拆箱板子。箱子是 Special Edition 的主色调,也就是黑色(而 Fun 对应的是橙色)。里面有一个包包、一捆 USB 捆绑线、一块板子、一支笔,还有一盒说明书和光盘。

Unboxing Bamboo Special EditionUnboxing Bamboo Special EditionUnboxing Bamboo Special EditionUnboxing Bamboo Special EditionUnboxing Bamboo Special Edition

把板子接上 MacBook ,然后自动就能识别其基本功能。把驱动装上后,它就是一块超大 Trackpad 了。跟 Trackpad 不一样的是, jitouch 和 BTT 都不支持它,所以无法自定义更多手势。看来 Trackpad 还是有它的优势的,等我明年换一部 MacBook Pro ,那样我也有 Trackpad 了。

Unboxing Bamboo Special Edition

在使用专业绘图软件之前,数位板在 Mac 上面毫无用处。英文手写是可以开启了,但识别率实在是不行,根本无法流畅书写。YouTube 上有 Mac 和 Windows 手写对比的视频, Mac 基本处于不可用状态。此外, Mac 不支持中文手写,所以本文是用 Windows 上的 Windows Live Writer 写成。幻灯片的手绘,也只有 PowerPoint 支持, Keynote 无此功能。 Mac 下面的 PowerPoint 呢?结束放影后不能保存之前手绘的内容,所以 Windows 成了唯一选择。

如果使用专业绘图软件呢?我正在边写这篇文章边安装 Adobe Creative Suite 5 呢!我要装完以后试用过了才知道,而且学习成本还不低。

最后送上 Bamboo Fun 的开箱照。这就是发错了的那块板子。

Unboxing Bamboo FunUnboxing Bamboo FunUnboxing Bamboo FunUnboxing Bamboo Fun

2010年10月26日星期二

CMDC 2010 及视频资源

这个星期去参加了 CSDN 和创新工场举办的中国移动开发者大会( CMDC ),感觉最大的收益是认识了一些做移动开发的人,同时了解了各家公司在做移动开发时积累的经验。

《植物大战僵尸》成功的秘密


最有趣的一个 session 是「《植物大战僵尸》成功的秘密」,演讲者是 PopCap Games 亚太区总裁 James Gwertzman 。这个如此成功的游戏经过了4年的开发,进行了多次迭代,才做出了我们看到的版本──易于上手同时充满乐趣。最初的 PvZ 设置为先要培养植物,等植物成熟了再来打僵尸,游戏的前期显得十分无聊所以这个迭代被否决的(这估计是正式版中 Zen 的来源)。第二个迭代设计为随机生成植物卡片,你要收集够太阳了,并且你想买的植物卡片随机出现了,你才能种植该植物,而且卡片的使用是一次性的,这个版本由于随机性太高而导致可操作性太低,也被否决的(这估计是正式版中某些随机卡片关卡的原型)。在第三个迭代中,太阳花和豆子炮的价格都是 100 太阳,尽管战略游戏玩家都知道要先积累资源再打仗,但悠闲游戏玩家在面对压力时,积累到 100 太阳后总是购买豆子炮,这样玩最终总会输掉。为了解决这个问题,设计团队尝试了若干修改,最后发现只要简单地把太阳花价格从 100 调整为 50 就可以了,悠闲游戏玩家自然会先购买太阳花。

YouTube 高清版本Youku 标清版本

从 Tap Tap Revenge 看 iPhone 应用成功之道


另一个英语 session 是「从 Tap Tap Revenge 看 iPhone 应用成功之道」。 Tapulous 从 iOS 1.0 开始做 App ,当时还不开放 SDK ,他们就做出了一款很成功的跳舞机游戏叫做 Tap Tap Revolution 。随后随着 iOS 升级, Tap Tap Revenge 不停发布新版本,并且在 Apple 的 iOS 4 发布会上进行了展示。如果做一个 App ,能够让 Steve Jobs 愿意在发布会上使用它来做演示?经验是:
  1. 做正确的 App
  2. 尽早进入市场
  3. 不停地添加新东西
  4. 专注于社区
  5. 具备社交性
  6. 专注于公关
  7. 跟 Apple 保持良好关系
  8. 保持在榜单上
  9. 免费应用加增值服务模式
YouTube 高清版本Youku 标清版本

天才团队和统一过程


「天才团队和统一过程」是一个有趣的 session 。我说不出这个 session 讲的是什么,但是郑飞科讲了很多有趣的故事。

YouTube 高清版本Youku 标清版本

美国硅谷移动创业经验分享


这是另一个我认为有点意思的 session 。赚美元花人民币开起来总是很幸福的事情,但如何才能赚美元呢?如何设计美国人愿意买的应用呢?柯博文介绍了他在硅谷创业的经验。

YouTube 高清版本Youku 标清版本

Panels


有两个论坛是比较有趣的,一个「 UI/UX 经验分享」,另一个是「海外市场面面谈」。

YouTube 高清版本Youku 标清版本YouTube 高清版本Youku 标清版本

如果你对各种技术会议感兴趣,欢迎订阅。接下来我将会提供 UPA 、 SD2C 、 TechEd 等会议的信息与视频。

2010年7月14日星期三

用 JavaScript 对 JSON 进行模式匹配 (Part 2 - 实现)

上一篇文章里,我们完成了 Dispatcher 类的接口设计,现在我们就来考虑一下如何实现这个类。

Notify & Capture



要实现 notifycapture 就太容易了,我们只需要把 capture 传入的 handler 都保存下来,然后在 notify 里面找到匹配的 handler 就可以了。

var filterHandlerBundles = [];

Dispatch.capture = function(pattern, handler) {
  var filter = createFilter(pattern);
  filterHandlerBundles.push({
    "filter": filter,
    "handler": handler
  });
};

Dispatcher.notify = function(json) {
  for (var i = 0; i < filterHandlerBundles.length; i++) {
    if (filterHandlerBundles[i].filter.apply(this, arguments)) {
      filterHandlerBundles[i].handler(json);
    }
  }
};


这段代码的逻辑很清晰,关键就在于 createFilter 的部分。这个函数负责把一个描述模式的 JSON 转换为一个判断 JSON 是否匹配的函数。

Operators



我们设计了不少的运算法,如何实现他们呢?记住,我们不要 switch case 。因此,我们使用一个关联数组来保存运算符与实现之间的映射关系好了。

var operators = {};

operators["lt"] = function(testValue, value) {
  return arguments.length == 2 && value < testValue;
};
operators["lte"] = function(testValue, value) {
  return arguments.length == 2 && value <= testValue;
};
operators["gt"] = function(testValue, value) {
  return arguments.length == 2 && value > testValue;
};
operators["gte"] = function(testValue, value) {
  return arguments.length == 2 && value >= testValue;
};


这样我们只要把 "$" 后面的运算符抽取出来,就可以立即找到对应的判断函数了。上面4个是比较运算符,由于实现比较容易,所以放在这里做例子。

一个比较难的函数是 eq ,因为它需要根据数据类型来选择具体的判断方式。对于 String 、 Number 、 Boolean , eq 的含义就是 == ;对于 Array , eq 的含义就是里面的每一个元素都 eq ,而且顺序一致;对于 Object , eq 的含义是每一个子条件都符合,因此我们需要将每一个子条件的运算符字符串提取出来,然后调用对应的运算符。具体可以参考完整代码

其他运算符会简单一些,在此我仅仅给出提示,大家可以根据自己的实际需求这些运算符的子集或超集:

  • in - 遍历数组,看能否找到至少一个 eq 的。
  • all - 遍历数组,看是否每一个都存在 eq 的。
  • ex - 如果有传入值,则子元素存在。
  • re - 用正则表达式判断字符串是否匹配。
  • ld - 直接调用函数进行判断。


写好了吗?不太确信自己写得是否正确?这是我们下一篇文章要讨论的内容,让我们先加上一个默认运算符。

operators[""] = function(testValue, value) {
  if (testValue instanceof Array) {
    return operators["in"].apply(this, arguments);
  } else if (testValue instanceof RegExp) {
    return operators["re"].apply(this, arguments);
  } else if (testValue instanceof Function) {
    return operators["ld"].apply(this, arguments);
  } else {
    return operators["eq"].apply(this, arguments);
  }
};


为什么需要一个默认运算符?这其实只是一个快捷方式。在大多数时候,我们需要的都是 eq 运算,如果每一处都要把运算符写上,代码将变得很复杂,也不美观。对比一下两个 JSON ,你觉得哪个更自然?

Dispatcher.capture({
  "status": 200,
  "command": "message"
}, function(json) { /* display message */ });

Dispatcher.capture({
  "status$eq": 200,
  "command$eq": "message"
}, function(json) { /* display message */ });


显然,第一个更直观一些。因此,我们需要一个默认运算符,当运算符字符串就是 "" 时,就通过默认运算符选择一个运算符。

Pattern to Filter



最后,我们需要把 operatorscreateFilter 接上。这部分工作其实也不难,只要调用默认运算符就可以了。

var createFilter = function(condition) {
  return function(json) {
    if (arguments.length > 0) {
      return operators[""](condition, json);
    } else {
      return operators[""](condition);
    }
  };
};


为什么需要考虑 json 参数没有传入的情况?下次文章再告诉你。不这样做也可以,只是有些很细小的问题而已。

写运算符,最需要的是严谨性。因为 Dispatcher 是一个封装好的组件,运算符一点点的不严谨,都会把缺陷埋藏得很深,很难找出来。因此,下一篇文章我们要讨论的是单元测试,通过单元测试我们可以大大提高 Dispatcher 的健壮性。如果你对此感兴趣,记得到 Twitter 上来关注我: @CatChen

2010年7月12日星期一

用 JavaScript 对 JSON 进行模式匹配 (Part 1 - 设计)

在《从 if else 到 switch case 再到抽象》这篇文章里面说到,解决 if else 和 switch case 分支过多的一个方法,就是做一个专用的 dispatcher ,让它来负责进行筛选与转发。至于筛选条件的描述,模式匹配是一种很常见也很好用的方式。在 JavaScript 里面,用 JSON 来描述模式又是相当方便的事情,所以我们来做一个 JSON 模式匹配工具吧。

用例设计



作为一个 dispatcher ,我们只需要两个方法: notifycapture 。一个最简单的用例是这样的:

Dispatcher.capture({
  "status": 200,
  "command": "message"
}, function(json) { /* display message */ });

Dispatcher.notify({
  “status": 200,
  "command": "message",
  "content": {
    "from": "user1",
    "to": "user2",
    "text": "hello"
  }
});


当然,只有局部的全等匹配是不够的,我们还需要一些其他运算符。

Dispatcher.capture({
  "value1$eq": "hello", /* equal */
  "value2$ne": true, /* not equal */
  "value3$lt": 0, /* less than */
  "value4$lte: 1, /* less than or equal */
  "value5$gt": 2, /* greater than */
  "value6$gte": 3, /* greater than or equal */
  "value7$in": [1, 3, 5, 7, 9], /* in */
  "value8$nin": [2, 4, 6, 8, 10], /* not in */
  "value9$all": [1, 2, 3, 4, 5], /* all */
  "value10$ex": true, /* exists */
  "value11$re": /^A.*/, /* regular expression */
  "value12$ld": function(json) { return true; } /* lambda */
}, function(json) { /* handler */ });

Dispatcher.notify({
  "value1": "hello",
  "value2": false,
  "value3": -1,
  "value4": 1,
  "value5": 3,
  "value6": 3,
  "value7": 5,
  "value8": 5,
  "value9": [1, 3, 5, 2, 4],
  "value10": "hello",
  "value11": "A13579",
  "value12": "anything"
})


随手写下来一堆运算符,看起来实现会很复杂?其实不会有多复杂。在下一篇文章里面,我们会讨论如何设计一个运算符接口,然后逐一实现这些运算符。如果你对此有兴趣,欢迎在 Twitter 上关注我: @CatChen

2010年7月4日星期日

从 if else 到 switch case 再到抽象

大家觉得在接手遗留代码时,见到什么东东是最让人感到不耐烦的?复杂无比的 UML ?我觉得不是。我的答案是,超过两个 else 的 if ,或者是超过两个 case 的 switch 。可是在代码中大量使用 if else 和 switch case 是很正常的事情吧?错!绝大多数分支超过两个的 if else 和 switch case 都不应该以硬编码( hard-coded )的形式出现。

复杂分支从何而来



首先我们要讨论的第一个问题是,为什么遗留代码里面往往有那么多复杂分支。这些复杂分支在代码的首个版本中往往是不存在的,假设做设计的人还是有点经验的话,他应该预见将来可能需要进行扩展的地方,并且预留抽象接口。

但是代码经过若干个版本的迭代以后,尤其是经过若干次需求细节的调整以后,复杂分支就会出现了。需求的细节调整,往往不会反映到 UML 上,而会直接反映到代码上。例如说,原本消息分为聊天消息和系统消息两类,设计的时候自然会把这设计为消息类的两个子类。但接着有一天需求发生细节调整了,系统消息里面有一部分是重要的,它们的标题要显示为红色,这时候程序员往往会做如下修改:
  1. 在系统消息类上面加一个 important 属性
  2. 在相应的 render 方法里面加入一个关于 important 属性的分支,用于控制标题颜色
程序员为什么会作出这样的修改?有可能因为他没意识到应该抽象。因为需求说的是「系统消息里面有一部分是重要的」,对于接受命令式编程语言训练比较多的程序员来说,他或许首先想到的是标志位──一个标志位就可以区分重要跟不重要。他没想到这个需求可以用另一种方式来解读,「系统消息分为重要和不重要两种类别」。这样子解读,他就知道应该对系统消息进行抽象了。

当然也有可能,程序员知道可以抽象,但基于某些原因,他选择了不这样做。很常见的一种情况就是有人逼着程序员,以牺牲代码质量来换取项目进展速度──加入一个属性和一个分支,远比抽象重构要简单得多,如果要做10个这种形式的修改,是做10个分支快还是做10个抽象快?区别显而易见。

当然, if else 多了,就有聪明人站出来说「不如我们改成 switch case 」吧。在某些情况下,这确实能够提升代码可读性,假设每一个分支都是互斥的话。但是当 switch case 的数量也多起来以后,代码一样会变得不可读。

复杂分支有何坏处



复杂分支有什么坏处?让我从百度 Hi 网页版的老代码里面截取一段出来做个例子。

switch (json.result) {
  case "ok":
    switch (json.command) {
      case "message":
      case "systemmessage":
        if (json.content.from == ""
          && json.content.content == "kicked") {
          /* disconnect */
        } else if (json.command == "systemmessage"
          || json.content.type == "sysmsg") {
          /* render system message */
        } else {
          /* render chat message */
        }
        break;
    }
    break;


这段代码要看懂不难,因此我提一个简单问题,以下这个 JSON 命中哪个分支:

{
  "result": "ok",
  "command": "message",
  "content": {
    "from": "CatChen",
    "content": "Hello!"
  }
}


你很容易就能得到正确答案:这个 JSON 命中 /* render chat message */ (显示聊天消息)这个分支。那么我想了解一下,你是如何作出这个判断的?首先,你要看它是否命中 case "ok": 分支,结果是命中了;然后,你要看它是否命中 case "message": 分支,结果也是命中了,所以 case "systemmessage": 就不用看了;接下来,它不命中 if 里面的条件;并且,它也不命中 else if 里面的条件,所以它命中了 else 这个分支。

看出问题来了吗?为什么你不能看着这个 else 就说出这个 JSON 命中这个分支?因为 else 本身不包含任何条件,它只隐含条件!每一个 else 的条件,都是对它之前的每一个 ifelse if 进行先非后与运算的结果。也就是说,判断命中这个 else ,相当于判断命中这样一组复杂的条件:

!(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")

再套上外层的两个 switch case ,这个分支的条件就是这样子的:

json.result == "ok" && (json.command == "message" || json.command == "systemmessage") && !(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")

这里面有重复逻辑,省略后是这样子的:

json.result == "ok" && json.command == "message" && !(json.content.from == "" && json.content.content == "kicked") && !(json.content.type == "sysmsg")

我们花了多大力气才从简简单单的 else 这四个字母中推导出这样一长串逻辑运算表达式来?况且,不仔细看还真的看不懂这个表达式说的是什么。

这就是复杂分支难以阅读和管理的地方。想象你面对一个 switch case 套一个 if else ,总共有3个 case ,每个 case 里面有3个 else ,这就够你研究的了──每一个分支,条件中都隐含着它所有前置分支以及所有祖先分支的前置分支先非后与的结果。

如何避免复杂分支



首先,复杂逻辑运算是不能避免的。重构得到的结果应该是等价的逻辑,我们能做的只是让代码变得更加容易阅读和管理。因此,我们的重点应该在于如何使得复杂逻辑运算变得易于阅读和管理。

抽象为类或者工厂


对于习惯于做面向对象设计的人来说,可能这意味着将复杂逻辑运算打散并分布到不同的类里面:

switch (json.result) {
  case "ok":
    var factory = commandFactories.getFactory(json.command);
    var command = factory.buildCommand(json);
    command.execute();
    break;
}


这看起来不错,至少分支变短了,代码变得容易阅读了。这个 switch case 只管状态码分支,对于 "ok" 这个状态码具体怎么处理,那是其他类管的事情。 getFactory 里面可能有一组分支,专注于创建这条指令应该选择哪一个工厂的选择。同时 buildCommand 可能又有另外一些琐碎的分支,决定如何构建这条指令。

这样做的好处是,分支之间的嵌套关系解除了,每一个分支只要在自己的上下文中保持正确就可以了。举个例子来说, getFactory 现在是一个具名函数,因此这个函数内的分支只要实现 getFactory 这个名字暗示的契约就可以了,无需关注实际调用 getFactory 的上下文。

抽象为模式匹配


另外一种做法,就是把这种复杂逻辑运算转述为模式匹配:

Network.listen({
  "result": "ok",
  "command": "message",
  "content": { "from": "", "content": "kicked" }
}, function(json) { /* disconnect */ });

Network.listen([{
  "result": "ok",
  "command": "message",
  "content": { "type": "sysmsg" }
}, {
  "result": "ok",
  "command": "systemmessage"
}], function(json) { /* render system message */ });

Network.listen({
  "result": "ok",
  "command": "message",
  "content": { "from$ne": "", "type$ne": "sysmsg" }
}, function(json) { /* render chat message */ });


现在这样子是不是清晰多了?第一种情况,是被踢下线,必须匹配指定的 fromcontent 值。第二种情况,是显示系统消息,由于系统消息在两个版本的协议中略有不同,所以我们要捕捉两种不同的 JSON ,匹配任意一个都算是命中。第三种情况,是显示聊天消息,由于在老版本协议中系统消息和踢下线指令都属于特殊的聊天消息,为了兼容老版本协议,这两种情况要从显示聊天消息中排除出去,所以就使用了 "$ne" (表示 not equal )这样的后缀进行匹配。

由于 listen 方法是上下文无关的,每一个 listen 都独立声明自己匹配什么样的 JSON ,因此不存在任何隐含逻辑。例如说,要捕捉聊天消息,就必须显式声明排除 from == "" 以及 type == "sysmsg" 这两种情况,这不需要由上下文的 if else 推断得出。

使用模式匹配,可以大大提高代码的可读性和可维护性。由于我们要捕捉的是 JSON ,所以我们就使用 JSON 来描述每一个分支要捕捉什么,这比一个长长的逻辑运算表达式要清晰多了。同时在这个 JSON 上的每一处修改都是独立的,修改一个条件并不影响其他条件。

最后,如何编写一个这样的模式匹配模块,这已经超出了本文的范围。如果你感兴趣,你可以订阅我的博客,我会在将来写文章描述这个模块:

2010年5月27日星期四

iPad 到手

首先不得不说的是,买 iPad 的过程有多么地折腾,所以上个周末才拿到 iPad 。

一开始预定 iPad 的时候,我的态度是观望的,直到机器出来了,我也亲自玩过了,才决定要买。那是某个周末,我们在西单吃饭, @Paveo@heqian@petefang 都把自己的 iPad 带来了,然后我每一个都拿过来玩了一下,发现最好玩的就是 @Paveo 那个了,因为游戏比较多。当晚 @Paveo 在 Twitter 上说可以帮朋友买 iPad ,我就立即向 iPad 下单了。

@Paveo 买 iPad 的方法比较省钱,也比较折腾──到 Apple 那里下单,东西送到免税州朋友的手上,从而把税省掉;然后再发到加州某位专业代购的手上,他帮忙发回国内,并且承诺不发生关税(发生关税他承担)。这样算来,价格就是 iPad 加美国国内快递费用再加代购费用,国际快递费用和中国国内快递费用都包含在代购费用里面了,因此总额不到 ¥4000 。

比较神奇的是, @Paveo 的 iPad 有两个错误发往广州了,最后到了 @LEMONed 手上,而且比北京的早到了,于是我就请当时在广州出差的 @eustacia 帮我把其中一个领回来了。整个购买周期加起来一个月,其实挺长的,不过也没办法──为了省钱。去中关村买的话,比这个折腾价要贵 ¥500 左右。

Got my iPad from @eustacia. It's sealed by @LEMONed.

在 iPad 拿到手之前,我就下载了一个 iPhone SDK 3.2 来看看 iPad 模拟器是什么样子的,结果发现模拟器里面的 iPad 挺 beta 的── iPhone 模拟器是内置照片和联系人数据的,这让测试 App 变得容易──如果你的 App 需要调用内置的照片和联系人,打开就有样板数据,你不需要操心任何事情。 iPad 是没有样板数据的,测试调用联系人还勉强可以,最多就自己手动创建一组样板数据,但是照片就无法创建了,除非你再写一个 App 往模拟器里灌样板照片。

拿到 iPad 后,发现这个产品确实有点 beta ,不像 iPhone 那样做到尽善尽美之后才放出来。当然, iPhone 所谓的尽善尽美也只是发布时的观点,发布后发现只有 Web 不行,又增加 App 了。 iPad 拿到手后,你会发现内置应用跟 iPod Touch 差不多,甚至就不如 iPod Touch ── Stocks 、 Weather 、 Voice Memos 、 Clock 、 Voice Memos 都没有了。此外, iPad 硬件上是有数字罗盘的,但就是不配 Compass 这个应用,内置的 Maps 也不提供此项功能。不知道这些功能会否在未来的 iPhone OS 4.0 中补上,补上的话不知道 iPad 未来的 OS 升级会不会如 iPod Touch 一样收费。

硬件方面,感觉没什么大问题。谁第一次拿起来都会觉得它有点重,并且怀疑长期使用是否能锻炼上肢肌肉,结果是对上肢肌肉完全没有锻炼效果的,拿着拿着就习惯这个重量了。屏幕是 iPad 最大的亮点,看起来非常鲜艳,看图片和视频的效果都非常好。屏幕背后是两块大电池,这是 iPad 比较重的原因,因此才有所谓的10小时续航能力。需要对 iPad 充电,必须用 iPad 附带的 10W 充电器,一般 USB 充电器(包括 iPhone 自带的)都是 5W 的,没办法向 iPad 充电。我的 MacBook 也能对 iPad 充电,这依赖的是 MacBook 对 USB 良好的供电能力,其他电脑的 USB 就不一定能对 iPad 充电了。

软件使用方面,我主要用它来做阅读器,偶尔也玩玩游戏。我买过一些 Kindle 书,也买过一些 PDF ,在 iPhone 上阅读的体验并不算好,现在有了 iPad 终于可以很爽地看书了,而且可以睡觉前抱 iPad 上床看书。阅读器 App 方面,我个人推荐 iBunkoHD ,它的翻页效果很好,同时也支持无缝的双页并排,这对看杂志来说非常重要,因为杂志往往会做一些跨越两页的整版大图。

http://farm5.static.flickr.com/4020/4643874451_202c24765c.jpg

Kindle 书看起来也很舒服,相对 iPhone 而言,一个屏幕内能够容纳下更多的内容,自然也就不用经常翻页了。图片也能看清楚,不需要放大后才能看到细节。当然,有了全屏幕的 Mobile Safari ,也更诱惑人去买书。

Kindle

游戏方面,推荐 iPhone 上非常耐玩的 Flight Control 对应的 iPad 版,也就是 Flight Control HD 。无聊时打发时间不错,随时可以开始玩,随时可以结束,而且还不需要网络。

http://farm4.static.flickr.com/3389/4643874609_47274a77df.jpg

我的 iPad 暂时没装太多的东东,除了阅读和游戏什么都不干,而且主要也就是阅读。用 iPad 阅读确实需要更好的定力,因为你随时可以退出阅读器然后切换到游戏,这是 Kindle 做不到的。