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 做不到的。

2010年3月16日星期二

MVP Summit 2010 Trip (CA)

跟张诚、马志文飞抵 San Francisco 以后,我父亲的朋友来机场接我们到酒店。我们住在 Hilton ,当初 bid 这个酒店是想看看五星级酒店如何,确实是 bid 到了很好的价格,但是网络、早餐都不免费。相比之下,之后住的那些三星四星酒店都有免费的网络和早餐。当晚安顿好之后,父亲的朋友开车带我们到 Bay Bridge 对面的 Oakland 吃晚餐──到美国之后的第一顿中餐,而且还有海鲜。之后回到 San Francisco ,游览了一下 San Francisco 的夜景。

DSC02810

第二天才是真正的 San Francisco 游览。我们去看了 China Town 那个「天下为公」的牌坊,这里所有的商店都有中文招牌,只要你懂粤语就能生存。我们去逛了著名的 City Lights Bookstore ,尽管里面的文学作品我们都看不懂,所以我只好买了一本 Lonely Planet Los Angeles Encounter Guide 。然后还去了 Lombard Street (九曲花街),可惜季节不对,看不到花。还好的是,尽管当天没有晒太阳的机会, Pier 39 的海豹们还是爬到木板上来了,让我们拍摄。

DSC02881DSC02960DSC02987

最后一个景点,当然也是最重要的,自然就是 Golden Gate Bridge 了。其实这不是一个景点,我们去了很多不同的观景地点,从不同角度拍摄了 Golden Gate Bridge 。至于哪个观景地点才是最好的,这个争论从来没有停止过。

DSC03018DSC03019DSC03049DSC03074DSC03079

第三天的行程是开车去 Monterey Bay 看 17 Mile Drive 。这是加州西海岸很漂亮的一段路,可惜去的路上一直阴天,到了之后稍微出来了一会儿阳光,接着又变成暴风雨了。这是我们第一天在美国租车,张诚作为司机就紧张得要死了,特别是迎着暴风雨开的那一段。

DSC03182DSC03192DSC03212DSC03215DSC03232DSC03280DSC03283

美国的 Freeway 都是限速 65mph (105kph) 的,天气晴朗的时候所有人都超速到 80mph (129kph) ,就算是暴风雨也能开到 60mph (97kph) 。相比起国内的高速,这样的速度是挺快的,不习惯美国开车方式的话,也是挺危险的。在国内,你胆小的话可以开慢一点,别人爱超你就让他超;在美国,汽车就如同轨道交通工具一样,所有车都走在自己的车道上,都按照一个速度来开,整条 Freeway 非常有秩序地流动,变道比较少发生。在国内,你要变道就先减速,因为大家觉得这样做比较安全;在美国,变道必须先打灯,确认有位置给你变道了,再加速然后变道。

为什么需要加速然后变道?设想一下,你要变道进入一条有轨电车车道,前后两辆有轨电车都开着 80mph ,你要如何才能插进去?为了保证后面那辆车不会撞上你,你的速度必须不低于 80mph 。如果慢了,你也不知道慢了多少,更不知道还差多少就会被后面车撞上;但如果快了,你是知道还差多少会撞上前面那辆车的,这个你可以控制,变道之后再把速度降下来。走在后面的车不是应该让前面的车吗?这是中国的法律。在美国,人家保持车道和保持速度就是没错的,你要变道,你就要小心。所以说,美国人守规矩,马路上没有轨道,但人们按照有轨道的方式来开车,轨道在人们心中。

晚上在 San Jose 一家叫 Pearl River Restaurant 的粤菜餐馆吃饭,味道还不错。随后在跟 Elliott Ng 吃早餐时,他说到一个观点:国内有很多人并不原意相信,美国的粤菜做得比北京要好,但这是事实,不相信的人只是没能在美国找到一家正中的粤菜餐厅而已。我非常认同这个观点,因为每一次我从北京跨越太平洋飞到美国,就饮食方面而言,都让我感觉到离家更近了。

DSC03303

为什么一个离家两万公里的地方比一个离家两千公里的地方更有家的感觉?或许这是文化上的差异吧。说不定,广东人当中愿意飘洋过海到美国的,比愿意到北京的还要多,而且这部分人比到北京的那部分人更有影响力。

第四天早上从 San Jose 开车出来,去跟 Elliott 吃早餐。这是我们第一次在没人带的情况下自己开车,导航全靠 Nuo Yan 借给我们的 GPS 。为了让张诚专心开车,所以就不用他和 GPS 直接交互了,我来负责操作 GPS 和将 GPS 的语音提示用中文转述一遍。即使是这样,我们还是绕了很多弯路,因为我们还不习惯 GPS 的导航方式。当 GPS 提示转弯的时候,我们往往会提前转了,或者来不及转了,于是又要绕回来。

DSC03309

吃完一顿正中的美式早餐后, Elliott 带我们去一家投资公司参观了一下他们的收藏品。我问 Elliott ,加州每天天气都那么好吗? Elliott 说,是的,基本上不用看天气预报,加州每天都会经历四季,但每天的天气都差不多。然后我又问,那为什么我们之前在华盛顿州就阳光灿烂,反而来到加州就遇上暴风雨了呢,这对这两个周来说都很反常啊。 Elliott 说,一定是我们把华盛顿州的坏天气带到加州来了。

随后 Elliott 因为有会议,而让我们自己去参观 Stanford ,于是我们就开车来到了 Stanford 。 我们在 Stanford 里面转了一圈,也不知道看什么,于是就开到 Stanford Shopping Center 停下来,找到有无线信号的地方,上网搜索 Stanford Visitor Center 的位置。接着开去 Stanford Visitor Center ,在 Visitor Center 前面的停车场绕了 20 分钟才搞明白什么车位是给访客的以及哪里有空余访客车位。在 Visitor Center 我们被告知, Golf Cart Tour 仅在提前预约的情况下才能参加,所以我只好就打电话找我在 Stanford 的同学,问他 Stanford 有什么好看的。他建议我们去看 Hoover Tower 和 Memorial Church ,顺便到 Alumni Cafe 吃饭。

DSC03347DSC03348DSC03351DSC03352DSC03355DSC03367DSC03368DSC03371DSC03373DSC03376DSC03381DSC03393DSC03396DSC03399DSC03411

晚上跟 Elliott 去参加 GSR Ventures 的一个新年聚会,由一群华裔投资者和创业者分享回中国投资创业的经验。说实在的,我过去对 entreprenuership 一直没什么兴趣,参加完这次聚会以后至少有那么一点点兴趣想要去了解了。在这里,我必须要感谢 Elliott 为我们提供这样一次机会,尽管后来 Elliott 在 Stanford 的课程我们没能参与旁听。

张诚在开了两天车后已经疲劳得要死了,而且我的 Stanford 同学向我介绍了加州有名的 In-N-Out Burger ,于是第五天起来后我们就来到一家 In-N-Out Burger 补充能量。据说 In-N-Out Burger 有 secret menu ,我们就问服务员 secret menu 是什么,然后被告知其实没有什么 secret menu ,只是有一些特殊的汉堡制作方法而已,例如说放 4 块牛肉加 4片芝士的 4 * 4 。我们三个人各自要了一份 4 * 4 的套餐,我很享受地吃完了,然后张诚和马志文吃了一半就饱得要吐了。

DSC03422

沿着 US-101 开车到 Santa Maria ,尽管无聊但很舒服。习惯了美国的 Freeway 之后,跑起来真的比中国的告诉公路要舒服──你固定车道固定速度巡航就是了,不会有人乱变道,你只要盯着自己车道前方的道路以及头顶的路牌就可以了。下午到达 Santa Maria 后,发现我们住的 motel 竟然还有游泳池,可惜这个季节不开放。晚上到 Ichiban Japanese Restaurant 吃寿司,发现店主一家原来是台湾人,见到有中国游客就特别热情。估计因为 Santa Maria 是小地方,很少有机会见到中国游客。

DSC03445

在 motel 睡了一觉以后,继续沿着 US-101 赶往 Los Angeles 。正如别人跟我们说的一样, Los Angeles 的司机素质比较低, Freeway 的状况跟国内高速类似,不是你守规矩就行了,你还要提防那些不守规矩的人。到达 Los Angeles 后,我们在把 Hollywood Boulevard 上的几个经典都看了一眼,也就回酒店去了。晚上张诚和马志文去看 NBA ,我自己宅在酒店上网,结果他们也只看了一般就因为无聊而回来了。

DSC03480DSC03485DSC03488DSC03492DSC03511

在美国最后完整的一天,我们安排给了 Universal Studio ,想着要花一天的时间来参观这个地方,结果大半天就搞掂了。 Universal Studio 里面的每一项游乐项目都很赞,让你觉得完全值回票价。在 Studio Tour 里面,你能看到很多著名电影当中的场景,而且电瓶车会停在这些场景旁边等候着特技的出现──在山谷里体验洪水从山上而来;在地铁隧道里体验大地震,迎面开来的地铁出轨,路面上的油罐车塌方掉到隧道里;在湖边体验大白鲨出没,即使点燃了码头上的油桶,大白鲨还是敢跃到水面上来。

DSC03560DSC03565DSC03576DSC03581DSC03589DSC03590DSC03593DSC03595DSC03608DSC03616DSC03619DSC03634DSC03636DSC03643DSC03669DSC03675DSC03676DSC03677

离开 Universal Studio 后,我们发现还有时间,于是想赶到 Santa Monica 看日落。可惜我们没有选择让 GPS 给出路线方案,只是朝着大致方向开,结果在绕来绕去的 Sunset Boulevard 上浪费了不少时间,最后赶到 Santa Monica 的时候太阳已经贴着地平线了。

DSC03693

最后一天,我们一早开车到机场,我搭上了回国的飞机,而张诚和马志文则等晚上的红眼航班飞往 New York City 。