2012年7月28日星期六

华附高中奥班 2012 级经验交流

Benny 在知道我回广州后,跟我预约了 2012 级高中奥班的入学培训时间,让我去分享经验。然后我就开始想,有什么事情是我已经习以为常但高中入学时又特别想要知道的。我把我想到的点都先以大纲的方式写了下来,然后调整了一下顺序,最后使用 iPad 上的 Paper 把我觉得通过图像来表达更有效的信息画出来。

Untitled

开篇第一个问题自然是「我是谁」。

Untitled

我 1998 年进入华附奥班,2004 年毕业离开,随后经历了中大、百度、Yahoo 全球研发中心和豌豆实验室。在华附的时候,高一黑过华附论坛,从此开始做华附论坛管理员,并且创立了华附学生网。高二高三拿过 NOIP 省一等奖,然后保送中大。进入中大后负责成立 Maxcell 东校区分会,到大四的时候由于写博客而获得微软 MVP,随后又多拿了两次。

Untitled

不过,对于华附高三的老师来说,可能我让他们印象深刻的是另外一件事。2004 年华附高考重点上线率为 89%,但升重率为 91%,那就是说有 2% 的人高考没有过终点线,然而又进入了重点大学,我就是其中那 2%。当然,我一个人产生不了 2% 那么大的影响力,只是其中之一而已。

Untitled

接下来进入正题,我想讲的第一个点是「vision」。如果站在 2012 年往前看,你能看到 2018 年吗?有什么是你现在开始努力能在 2018 年获得的?你所在的领域 2018 年会需要什么样的你?

Untitled

我在华附初中时就看到校内的 3 大主流媒体:负责黑板报的华通社,还有广播站和电视台。尽管当时还没有接触到互联网,但我相信计算机能够带来下一个主流媒体,所以没有加入 3 大社团的我一直在寻找建立自己社团的机会。

Untitled

我中学的时候就知道有 MCSE,同期中国也开始评选 MVP。哪一样长远来说更有价值?当然是 MVP,因为 MVP 的数量是有限的。尽管国内有很多机构能够让你不怎么学习也能获得 MCSE,而 MVP 却需要 Microsoft 员工或 MVP 提名,但只要有实力总是有机会的。实力不是短时间能够获得的,但长远来说有实力才是最重要的。

Untitled

讲完「vision」下一个点自然是「opportunities」。你看得到你的目标,但你不知道合适的机会在哪里以及什么时候发生。你需要坚持下去,不断尝试,直到找到合适的机会为止。

Untitled

在我直觉认为第 4 大主流媒体一定会出现的时候,我尝试过用计算机做不同的事情。初中的时候试过用 Word 来做出版物,也是过在图书馆的多媒体阅览室(俗称「网吧」)做过管理员,直到高一黑了华附论坛接过管理员账号,才开始为学校做网站。高二做了华附学生网,实现了建立第 4 大主流媒体的梦想,尽管最终被证明网络在华附里不那么主流,因为那个时候大家不可能有那么多时间使用电脑,智能手机也还没有普及。

我一开始在 Blogger 上写技术博客,但一直没什么人来看。后来我把内容导入到博客园,迅速来了不少读者和高质量的评论,接着 dudu 问我有没有兴趣申请评选 MVP。这也说明了,坚持做同一件事情,也要遇到合适的机会才会成功。

Untitled

我想说的下一个点是「right people」。有正确的人将能够帮助你事半功倍。

Untitled

「知识就是力量」,别人的知识也是你的力量。在奥班读书最大的优势就是你周围的人都足够聪明,他们的知识都能为你所用。这显然包括你的专业知识,例如说一道题一个人要想出多种解法来不容易,但在奥班很容易就能发现不同人有不同的解法。这也包括不显眼的非专业知识,例如奥班因为保送的人多而在这方面积累了足够多的知识,从而帮助你更好地挑选学校,更有策略性地申请学校。

Untitled

从学习效率来说,最高的当然是有老师指导的训练,其次是小组学习,最次是自学。如果能找到正确的老师,他使用恰当的方法来为你提供训练,你的学习效率能够比别人高很多倍。在奥校学习就是一个很实在的例子。

Untitled

500 年前,中等数学还是只有数学家才会花毕生精力去研究的东西,一般人都不会去学,现代教育却确保了人人都能在 20 年之内学会。至于竞赛所需要使用到的高等数学知识,500 年前甚至还没出现,是 17 世纪后期才有的。可见正确的人给你正确的训练有多重要。

Untitled

要选择相信你能成功并且愿意帮助你成功的人在一起。这也是一流公司招聘时常说的话。一流的公司不会说「我们这里缺一个人力资源单位,正好你能胜任,所以你来填充这个位置吧」。他们会说「我们觉得你在我们公司能够获得成功,面试过你的人也愿意跟你合作帮助你成功」。我觉得我在百度时最幸运的就是遇到愿意帮助我成功的人。我想要做什么样的项目,经理会给机会我去了解。我觉得写博客是我的长处,经理给机会我去成立官方博客。

Untitled

最后,有两点是需要特别注意的。一方面,你不能被你的不安全感驱使。精英教育会使得你害怕平庸。你能够把事情做得很好,但是你拒绝把事情做得很一般。最终这也会成为一种无能,就是你不能把事情以一个很低的成本随便处理掉。

Untitled

另一方面,你也不能被你的贪婪驱使。接受精英教育的结果是,你确实能够为自己赚取越来越多的东西,但你总会低估你长远目标所需要的成本,结果一旦精力分散了就可能无法完成长远目标,这真的是得不偿失。

Untitled

最后附上这次演讲所用的幻灯片 PDF 下载地址。这是我第一次完全使用 Paper 来绘制一份幻灯片,所以我希望分享出来让大家看看。其实纯手绘的幻灯片能够做出非超好的效果来,关键还是你要能把想法转变为易于传达信息的画面。

2012年6月16日星期六

HTTP 状态码详解

最近看《REST in Practice》,发现 HTTP 如此之多的状态码都有各自的含义,要准确使用并不难,但现实当中很少人能够做得到。大多数人熟悉的状态码就那几个,平时也不会去阅读 RFC 2616,结果反复使用的也就是那几个状态码。其实很多 REST 中可能遇到的情况,在 HTTP 状态码中都已经有考虑到,不需要自己去发明新的状态码,也不需要在 header 或者 body 自定义错误信息。

在说状态码之前,首先建议大家还是先阅读一下 RFC 2616 中的相关章节,看看已有的状态码描述都是什么。我相信有部分状态码是你看了描述也不知道用来干什么的,这时候就需要有更具体的例子来告诉你怎么用了。(我准备详细说的是那些比较少人知道但又实际上应该被更广泛使用的状态码。)

2xx

200 OK

所有人都知道 200 OK 是什么。这估计是最经常被滥用的状态码。很多人在应该使用其它 2xx 状态码时都选用了 200 OK 来表示。

201 Created

如果你在设计一个 REST API,或者一个 CRUD API,当你使用 POST(或者 PUT)成功创建一个新的资源后,服务器应该返回 201 Created 同时在 header 的 Location 字段给出刚刚创建好的这个资源的 URI。

例如说,如果你使用 POST 请求通过 \comments URI 创建了一个新的评论,那么服务器应该返回 201 Created,同时带上形如 Location: \comments\1234 的字段表明新创建的评论的 URI。

202 Accepted

如果服务器在接受请求后,需要进行一个异步处理才能有结果,并且觉得不需要让 TCP 连接保持到结果出来再返回,它可以返回 202 Accepted,意思是请求已接受,但没有立即可返回的结果。

204 No Content

在一个 REST API 或者 CRUD API 里面,当你使用 PUT 成功更新一个资源后,如果服务器完整接受了客户端的更新,没有拒绝也没有额外更新,那实际上是不需要返回任何东西的,因为现在客户端和服务器端已经拥有完全一致的状态。在这种情况下,服务器可以返回 204 No Content,同时 body 为空,客户端就知道它的状态已经跟服务器端同步了。

206 Partial Content

断点续传和多线程下载都是通过 206 Partial Content 实现的。

请求的 header 必须包含一个 Range 字段,表明自己请求第几个字节到第几个字节的内容。如果服务器支持的话,就返回 206 Partial Content,然后使用 header 的 Content-Range 指明范围,并在 body 内提供这个范围内的数据。

3xx

301 Moved Permanently

永久性重定向。目标由 header 的 Location 字段给出,同时 body 中也应该有指向目标的链接。新请求使用的方法应该和原请求的一致。如果用户使用 HEADGET 以外的方式发起原请求,客户端在遇到 301 Moved Permanently 后应当询问用户是否对新的 URI 发起新请求。

302 Found

临时性重定向。

这应该是浏览器实现最不符合标准的一个状态码了。理论上,除了临时性这一点,302 Found301 Moved Permanently 应该是完全一样的。然而实质上,很多浏览器在遇到 302 Found 后就会使用 GET 去请求新的 URI,而无论原请求使用的是何种方法。由于这种现象的普遍存在,使得这成为了一个与书面标准相违背的事实标准,新的客户端在实现时很难选择应该遵守哪一个标准,所以 RFC 2616 专门新增了 303 See Other307 Temporary Redirect 两个状态码来消除二义性。

303 See Other

临时性重定向,且总是使用 GET 请求新的 URI。

304 Not Modified

如果客户端发起了一个「条件 GET」,同时资源确实没被修改过,那么服务器端就应该返回 304 Not Modified,同时 body 不包含任何内容。

所谓的「条件 GET」,是指 GET 的 header 带上了 If-Modified-SinceIf-None-Match 字段。这两个 header 就是「条件」,如果条件符合了 GET 就应该正常执行,否则就应该返回 304 Not Modified,以便告诉客户端它想要请求的资源在上一次请求之后没有被更新过,客户端可以继续使用之前的版本。

307 Temporary Redirect

临时性重定向,且总是使用原请求的方法来进行新请求。

4xx

400 Bad Request

服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。

401 Unauthorized

请求未授权。如果请求 header 没有 Authorization 字段,服务器端应该在返回 401 Unauthorized 的同时在 header 中用 WWW-Authorization 字段指出授权方式,以便客户端带上登录信息重新发起请求。如果 Authorization 字段已经存在,则表明登录信息不正确。

402 Payment Required

需要支付。这是一个在任何浏览器中都没有被实现的状态码,仅预留将来使用。

百度曾经有一个部门印过一批背上写着 402 Payment Require 的衣服,并且开玩笑说这批衣服最适合在互联网企业员工讨薪时穿。

403 Forbidden

禁止访问。即使使用 Authorization 字段提供登录信息也会得到相同的结果。

如果客户端使用 HEAD 以外的方法请求,403 Forbidden 必须同时在 body 中返回禁止访问的原因。如果原因不能够公开,则应该使用 404 Not Found

404 Not Found

找不到如何与 URI 相匹配的资源。服务器无需指出资源是临时性不存在还是永久性不存在,但如果服务器端知道该资源已经被永久性删除则应该返回 410 Gone

404 Not Found 是服务器端在不愿意提供理由的情况下拒绝提供资源的最佳借口。

405 Method Not Allowed

请求的方法被拒绝。

如果你有一个 REST API 或 CRUD API 被设计为只读,那么在遇到 PUTPOST 或者 DELETE 方法时服务器端都应该返回 405 Method Not Allowed,同时在 header 的 Allow 字段说明允许的方法(如 GETHEAD)。

409 Conflict

冲突,且需要用户手工解决。

如果你使用 git(或者其他源代码管理软件),你已经知道「冲突」是什么了。409 Conflict 通常发生在 PUT 请求时,如果要更新的资源已经被其他人更新过了,你再更新就可能产生冲突。

410 Gone

如果服务器端将此资源标记为已被永久性删除,则应该使用 410 Gone 而非 404 Not Found,其用意在于告诉客户端资源是被有意删除的,而且删除是永久性的,客户端不应该再保留这个 URI 的链接。

举例来说,你有一个 REST API 或 CRUD API 用于向用户提供优惠信息。有一则优惠的 URI 是 /promotions/1234,但由于优惠活动已经结束了,所以这一则优惠信息不再有效且应当被永久性删除,那么这时候服务器端就应该让该 URI 永远返回 410 Gone 了。

412 Precondition Failed

条件判断失败,操作不会被执行。

在解释 304 Not Modified 时提到了「条件 GET」的概念,但「条件」本身也可以应用于非 GET 请求,这时候如果条件判断失败服务器端就应该返回 412 Precondition Failed,同时拒绝执行客户端请求的方法。

条件请求可以被看作是一种乐观锁。它不需要服务器端有任何逻辑判断操作是否存在冲突,服务器端只要记录资源的时间戳(或其它版本信息)即可。

5xx

500 Internal Server Error

最常见的服务器端错误。

503 Service Unavailable

服务器端暂时无法处理请求(可能是过载或维护)。

返回 503 Service Unavailable 的意思是当前的状况是临时性的,客户端可以稍后重试。服务器端可以在返回时通过 header 的 Retry-After 字段告诉客户端多久后可以重试。如果不提供这个字段的话,客户端应当把 503 Service Unavailable 等同于 500 Internal Server Error 处理。

总结

在看完这篇文章后,你有发现经常用错的状态码吗?如果有的话,将来在设计 REST API 或 CRUD API 时就应该改过来。由于状态码和 header 字段数目众多,所以我建议一般用户尽可能复用主流的 REST 框架或 CRUD 框架,而不要自己重新实现一遍。

如果你喜欢这篇文章,欢迎订阅或者捐赠。如果我有时间的话,我会再写写常见的 header 字段详解。

2012年6月9日星期六

向我的博客捐赠

阮一峰一年前说他的 AdSense 账号被封了,所以尝试接受捐赠,并且说一年后会公布收入详情的,不过至今仍未公布。考虑到现在支持小额支付的平台越来越多,所以我决定也试试接受捐赠,看看是否能带来收入。

看了几个不同的平台后,发现最简单易用的还是 Gumroad。有些专门针对内容收费的平台(如 ReadabilityFlattr),需要读者先去注册,然后在阅读过程中支付。考虑到我的读者里面没有多少比例可能注册过这些网站,这种平台首先被否决掉。至于专业做小额支付的平台,我觉得 Gumroad 是界面最好看流程最简单的了——你只要填个邮箱和卡号就可以支付了。当然,Gumroad 也有不如意的地方,那就是它不支持 PayPal 支付,估计是因为再加上 PayPal 手续费它就没有利润空间了吧,但这会使得不愿意提供信用卡信息给 Gumroad 的人拒绝支付。

如果你觉得我博客提供的信息对你来说有价值,你可以使用我的 Gumroad 链接进行捐赠。Gumroad 还有一个优势是可以设置价格为 n+,最终买家可以输入任何大于 n 的价格进行购买。我的捐赠链接设置为 $1+,大家觉得给多少合适就填多少。

2012年6月8日星期五

关于「如何追男生」的回答摘要

之前写了《关于「如何追女生」的回答摘要》,然后就有人问我有没有反过来的版本,所以我决定把 Why Men Love Bitches 的读书笔记整理出来写一篇文章。

先说说女人跟男人看待这个问题的本质区别。看 PUA 的资料,你会发现一切方法论皆以扑倒为终结点,随后的事情大家都觉得是不需要教的。但是给女人看的书就不一样,如果吸引男人的兴趣只是内容的一小半,另外一大半则是关于如何保持男人的兴趣。(我之所以感兴趣看 Why Men Love Bitches,也是因为好奇后面这一部分内容,因为 PUA 资料都不会覆盖到这些话题。)需要注意的是,这本书是假设女人采取 submissive 姿态的,所以如果你习惯了以很 dominant 的姿态来对待男人,这本书里面的部分观点对你来说并不适用。

自信

自信这一点是性别无关的,所以无论站在男人的观点来看还是站在女人的观点来看,自信都是放在第一位的。我觉得英文里面描述「自信」最好的一句短语是「comfortable in one’s own skin」,展开的解释比中文抽象的「自信」二字要具体多了:

表现出对自我清晰了解而带来的自信,没什么能让你感到不适。

自信跟盲目自大的区别就在于「对自我清晰了解」,因此要做到自信首先要增加对自我的了解——什么是我的强项,什么是我的弱项。对于强项,你应该尽可能多的发挥出来,尤其是在能够表现你特质时;对于弱项,你不应该表现出有任何的安全感缺失,就算别人指出了你也能坦然承认不会反应过度。

对于女人来说,缺乏安全感是很常见的,但为了表现出真实的自信,这又是一个很有必要解决的问题。通常的安全感缺失来源来自竞争对手(例如对方的前任),这时候如果你表现出受到威胁了,你的吸引力就会消失,同时你还帮助了你的竞争对手。

书中说,为什么大多数女人要到 30 岁后才能达到 sexual peak,那是因为到她们要到那个阶段才开始有足够的安全感,或者是意识到自己其实是不需要跟任何女人竞争,然后才能轻松地表达自己喜欢什么和不喜欢什么。但因为能做到这一点对男人来说是个 turn-on,所以显然你不会希望自己等到过了 30 岁然后克服安全感缺失。

满足男人成就感

这在书中也是一个很重要的话题,所以说这本书适合 submissive 的女人看,然后驱动男人在 dominant 的同时获得成就感。由于征服是人类本能,所以并不是说你应当可以刁难谁,而是你需要让他通过征服的过程获得满足感,因为这才符合人性。只要他觉得他对你来说还是 sexually desirable 的,同时他觉得自己还有机会,他就会继续接受新的挑战。

不要侵犯男人领地

男人的「自由」也就是他的「领地」。他想什么时间去哪里做什么,这是他的自由,你不应该八卦。一旦问了,就是侵犯对方领地了。就算你安全感缺失,有些事情你很想知道,你也应该控制住自己,因为在他爱你爱到一定程度之后他会很乐意主动告诉你这些事情。如果你想抛弃一个男人,你可以反过来使用这条规则。在他感觉到窒息的时候,他自然会远离你。

赞美而非唠叨

赞美是最好的调教方法,而唠叨则是毫无疑义的。不要说「你以前会送花给我的」,而应该在他送花之后说「这是我见过最美的」。如果男人请你吃饭,你要赞美食物或者餐馆。(如果真的不好吃,就别作任何评价。)无论如何都不要尝试通过唠叨来解决问题,因为一旦某一件事情你重复说三次,你就变成他母亲了,而他就会表现出反叛精神。

保持吸引力

PUA 的目标很单一,甚至可以说大多数男人的目标都很单一,那就是扑倒。所以作为女人总是会很在意如何保持吸引力。其中一个方法是,男人想要的东西不要一次性满足他,而必须一点一点提供给他。在这个过程中,你可以让他更好地了解你,慢慢地发现你的特质,他会发现你还有更多他想要的东西,然后他就会想要继续在你身边。

如果你发现他开始对你丧失兴趣了,那意味着你需要去寻找一件比他更能让你充满激情的事情。只要有一件事情对你来说显得比他还重要,他就会想要回到你身边。

总结

以上是我觉得 Why Men Love Bitches 里面比较有意思的论点。因为我是从看过那么多 PUA 资料的角度出发来看这本书的,我的目标主要是看女人观点和男人观点的异同,所以可能我看到的跟对你有帮助的不一定重合。我看过很多的 PUA 资料,但我看过写给女人看的这方面的书就几本,这本是我觉得写得最好的了,所以我会建议你去看原书。

此外,如果你觉得我写的文章对你有价值,欢迎订阅或者捐赠

2012年6月6日星期三

关于「如何追女生」的回答摘要

因为时不时就有人会问我「如何追女生」或者是「如何追某某特定女生」的问题,我又不像某些 dating coach 一样靠教这个赚钱,所以就决定把我知道的写下来。如果以后有谁还要问我相似的问题,可以先看这篇文章的内容再说。

尽管我几年前看过很多 PUA 相关的书籍和资料,但大多都不记得了,所以我不准备写很复杂的内容。由于原本我是想要基于 The Natural 的读书笔记写一篇书评的,所以这篇文章主要还是基于 The Natural 的读书笔记来写。

自信

无论你想要的是工作还是女人,第一条建议总是「自信」。(关于工作你可以参考另一篇文章,所以这里只说女人。)自信与否起到了关键作用,这可以从两个不同的角度来解释。

一方面,你可以认为自信会影响到你的发挥。如果你相信自己能拿 90 分的话考试成绩可以有 95 分,但如果你觉得自己拿不到 90 分的话考试成绩可能只有 85 分。这种现象也叫做 self-fulfilling prophecy

另一方面,自信也是一种说服力。The Natural 里面说到,当几个拿不准主意的人在尝试说服别人时,谁越坚信自己是正确的谁越能说服别人。这看起来很 tricky,但其实很合理——如果所有人都是理性的,并且所有人都如实表现自己拥有的信息的置信度,自然置信度最高的胜出。因此,跟其他人在一起时,包括跟女人在一起时,如果你确信你的经验能做出正确的判断(那家西餐厅很好吃所以我们去那里午餐),你就应当自信地宣布这个判断。

首领气质

在 PUA 的社区里,你会经常听到「alpha male」这个词,意思是动物群族中的雄性首领。PUA 还会强调要成为雄性首领,那是因为在很多哺乳类动物中,雄性首领拥有在群族中优先获得食物和配偶的权利。尽管人类社会已经进化到没有明确群族首领的阶段,但能够展示首领气质的男人还是在吸引女人方面有优势的。

在 The Natural 一书当中,作者认为以下 5 种特征属于首领气质:

  • 强烈的自我信念
  • 强壮的体格
  • 领导和抉择的能力和意愿
  • 在高压环境下保持泰然自若
  • 社交智商(与他人结识和沟通的能力)

至于如何培养这些气质,因为这是篇很概览性的文章,所以我不准备细说,想知道的人还是去看原书吧。

3 个阶段

The Natural 把追女分作 3 个阶段:

一开始你应该是 Mr. Sociable。你很善于交际,无论对方对什么感兴趣你都能说半天;就算对方不对任何事情感兴趣,你还是能分享很多有趣的事情。同时你还能传递正面能量,让人想要跟你聊天,跟你进行更多互动。

然后你变成 Mr. Comfort。能让人感觉到你像一位认识多年的老朋友,可以很放松地跟你聊任何事情,发生肢体接触也不会有任何不适。

最后你要成为 Mr. Seducer。逐步展现你的欲望,让对方感觉到你逐步被她吸引住了。

这 3 个阶段基本上跟 The Mystery Method 里面提到的模型是一致的,只是名字不一样:Attraction、Comfort、Seduction。

这 3 个阶段发生的顺序不对,或者漏了其中某一阶段,或者某一阶段做得不够好,都会导致结果不如人意。例如说「让我们继续做朋友吧」通常就是因为身陷第 2 阶段,不敢展现自己的欲望,结果也无法进入第 3 阶段。

训练勇气

要做对上面所说的,必然需要一些勇气。例如对于内向的人来说,如何慢慢地展现自己真实的欲望是很有挑战的事情,但如果你跨不过去你就永远停留在「让我们继续做朋友吧」这一阶段。The Natural 说了一件其它 PUA 材料没有提及的事情,那就是勇气是可以被训练出来的。

有些事情你因为感觉不舒服所以总是回避,但如果你能在确认安全的情况下多练习几次,你就会发现结果其实也不会有多糟糕,然后你不舒服的感觉就会开始消失。继续练习,你就习惯做这件事情了。这个道理很简单,但它能帮助你克服很多存在已久的毛病。

总结

上面是 The Natural 里面几个我觉得比较重要的点,如果大家觉得有价值的话,建议还是去读原书。对于入门的人来说,可能书中一些我认为很平常的点对你更有帮助。

P.S. 我知道肯定有人要问有没有「如何追男生」版本的了,所以我把 Why Men Love Bitches 的读书笔记整理成一篇《关于「如何追男生」的回答摘要》。

2012年6月4日星期一

三藩市湾区一周游

上个星期四一早从广州飞往三藩市,到这周三中午返回,大约一周时间,在三藩市湾区充满拜访了多位好友,也参观了几家互联网公司。还记得两年前去三藩市湾区,遇上暴风雨,不仅景色没看到,还让人担心自驾安全。这次旅行途中南湾阳光明媚,就算是三藩市也只是有雾而已,没有遇到下雨的情况,感觉好多了。

5/24

因为 Delta 取消了广州到东京的航线,所以尽管这次还是经东京转机,但飞的是 ANA 和 United。到达三藩市机场后,我做的第一件事情是把手机掏出来搜索机场免费 Wi-Fi。两年前的三藩市机场好像是没有免费 Wi-Fi 的,但我很确信现在是有的。找到 Wi-Fi 后先用 Foursquare check-in 一下,再打开 Twitter 回复了几条 tweet,然后才尝试连接手机漫游网络。很奇怪的是,我的联通 3G 卡开了漫游后到美国竟然连不上任何一个网络,AT&T 和 T-Mobile 都不行。无奈之下只好先打车去酒店。

头两天住的是 Sheraton Palo Alto,从机场打车过去要半个小时,花费 $100。酒店环境很不错,有泳池有喷泉,靠近 Caltrain 站同时 Stanford Shopping Center 也在步行范围内。住进酒店后,第一件事情当然是使用酒店 Wi-Fi 连接 MacBook Air,结果才发现连接 iPhone 时要再次收费。打电话给酒店的技术支持,对方说跟前台说一声就会撤销重复扣费,我想了想决定将来还是用有线再共享好了。

既然联通漫游失败,接下来肯定要买一张电话卡,否则离开 Wi-Fi 后连打电话联系朋友的能力也丧失了。根据几位之前来过美国出差的朋友推荐,购买 T-Mobile 每天 $3 的预付费卡是最合适的了,拥有无限量的电话、短信和流量,而且每天的前 200MB 流量还可以有 4G 网络的速度。幸好 University Avenue 上就有一家 T-Mobile Store,散步过去花 $30 买张卡够用一星期了,然后再散步回来顺便看看周围的环境。

晚上三藩市的朋友开车过来接我去晚餐,吃的是 San Mateo 的 Hokkaido Seafood Buffet北海道布斐)。在美国吃这类自助餐最好的地方在于分量充足的同时价格也不贵,$10 到 $20 能吃到的东西在中国至少要吃 ¥200 一位的自助餐才可能提供。

5/25

第二天去 Facebook,进门后见到室内的涂鸦就觉得很有创意。据说是请了知名涂鸦画家 David Choe 来画的,当然也有 Facebook 员工的作品。Facebook 现在用的园区以前是 Sun 的,Facebook 重新装修后只启用了其中的几栋楼,例如说工程师都在 Building 16,其余的楼房都还是空的。园区内只有一条主干道,因此不会迷路也不需要地图来导航。

Facebook 内部有趣的东西很多,例如 Building 16 里面的开放区域有几台街机,甚至还有一台带笔记本电脑支架的跑步机——你可以随时把电脑放上去接好线,然后就可以一边跑步一边工作。墙壁上的大屏幕是室内导航图,而且还带有会议室查询功能,点两下就能知道哪些会议室在用,或者你要进行会议的会议室在哪里。此外,为了降低 IT 服务成本,如果员工想要领取外设硬件,连 IT 也不需要去麻烦,在贩卖机上刷一下卡就能拿到。

Facebook 园区内最显眼的地标是 Hacker Square,据说在广场上写一个大大的「HACK」字,就是为了确保 Google Maps 的卫星图(或航拍图)能够拍到。每次 Hackathon 举行前人们都会聚集到这个广场上,然后 Zuck 以敲钟的形式宣布这场 Hackathon 的开始。

从 Facebook 回来后,我约了在 Stanford 读书的中学师弟出来吃饭,然后我先步行去 Stanford Shopping CenterApple Store 买 Gift Card。晚饭是在 The Counter 吃的,汉堡的牛肉和炸洋葱圈都很美味。晚饭后去了一趟师弟的宿舍,帮他解答作业上的一些 JavaScript 和 Rails 问题,结果一直折腾到凌晨两点才结束(他很悲剧地又要消耗 late day 了)。

5/26

离开 Sheraton Palo Alto 后,我搬到了三藩市朋友家住,在 Balboa Park 附近,搭乘 BART(地铁)和 MUNI(轻轨)都很方便。感谢当天 @liuxi 开车过来帮忙搬东西,在南湾这样乡下的地方没有车真的很不方便。那天还去了 Hong Kong Saigon Seafood Harbor Restaurant(西贡渔港)喝下午茶,见到了 @iveney。西贡渔港的出品很不错,据说中大有些同学很喜欢来这里吃,没时间在这里吃也打包带走。

喝完下午茶,去参观了一下 Google。在做 Android 的 Building 44 门外能看到一堆代表不同 Android 版本的大公仔,里面有大屏幕的 Android 设备(类似 Apple Store 的大 iPhone)可以用来试玩。我跟 @liuxi 想要试试如果对着展示设备的 Gmail 发邮件会怎么样,结果发现发不进去,而在展示设备上的 Gmail 中写邮件也发布出来。难道是机器没有联网?但网页和 YouTube 都能正常打开。估计早就有人想到我们会这样子搞,所以提前封锁了 Gmail 的访问,使得展示设备的 Gmail 处于离线状态。

Google 的访客中心还有一些有趣的展示品,例如一台由 8 块大屏幕组成的全景 Google Earth。由于 Google 的园区比较大,所以不像 Facebook 那样是封闭的,建筑物之间会有公路,跟其他公司的建筑物之间也没有围墙分隔。

5/27

住朋友家的第二天,跟着他们一家去了 South Sea Seafood Village(南海渔村)喝茶,虽然感觉没有西贡渔港那么正,不过至少做得还算正中。随后跟着他们去购物,买了几条廉价的 Levi’s(相对国内廉价),然后在 GameStop 买了 Forze 4,在 BestBuy 买了 Xbox 360 用的 Wireless Speed Wheel。虽说这样买比在 Amazon 贵大概 10% 到 20%,而且还要多交近 10% 的税,但既然出来购物了那就买吧。见识过这样的定价,才明白为什么实体连锁店都快要倒闭了。

下午去了 Half Moon Bay,可惜有阳光的时间比较短,大多数时候都是阴天。看了一下港口的渔船,据说 11 月份的时候蟹只卖 $2 一磅,好像很便宜的样子。往返的路上还路过了 Crystal Springs Reservoir。据说水库里的水部分来自 Yosemite(优山美地),现在专门提供给三藩市作自来水用。

晚饭是去 Mountain View 跟 Changhao 吃的,回三藩市时有了人生第一次搭 Caltrain(火车)和 BART 的经历。感觉 Caltrain 比 BART 要干净,而且因为搭乘时间比较长所以列车上还有洗手间(整列车只有一个)。BART 作为地铁,有闸机验票,而 Caltrain 则只有随机检票。一般情况下,Caltrain 只要在站台买票然后上车就是了,我搭的这一次 Caltrain 没遇上检票的。这一次从 Mountain View 搭 Caltrain 到 Millbrae 用了近一个小时,而 Millbrea 搭 BART 到 Balboa Park 又用了半个小时,加上等待和步行时间差不多两个小时。这再次说明了在南湾没有车是很不方便的。

5/28

Memorial Day 长假期的最后一天,经过 @diamondtin 的介绍,受 @lordhong 邀请去他们家作客。去之前的先去了一趟 Gilroy 的 Premium Outlets,逛了一圈最终只买了两条牛仔裤和两瓶鱼油。其实我还看中了 Sony 的 Blu-ray 3D Player,只是考虑到电压不兼容,还可能有分区锁,想想还是算了。打完折的价钱(好像是 $90)换算成人民币也很划算,在中国支持 3D 的蓝光播放器都不止这个价钱。

去到 @lordhong 家,第一次见到美国的 house 里面是什么样子的,有花园有车库的感觉还是很爽的。我们整个下午就在花园里晒太阳聊天,感觉美国人家的午后生活真的很写意啊。随后是海鲜大餐,感谢 @lordhong 夫妇提供的生蚝和螃蟹,这一餐吃得真爽了。这次的聚会还见到了 @delphij 夫妇、@jinghuaz 夫妇以及 @jasonlai,聊了各种有趣的事情。

吃完一餐,又要赶场下一餐。@essej 开车过来接我,去 Chef Yu(岳阳楼)吃饭,顺便见见在 Sunnyvale 忙于准备面试的另一位中学同学。(其实还有第三位中学同学,据说因为在女生当中太过 popular 而总是约不到,当天晚上他也没时间出来。)吃完饭后去 @essej 在 Palo Alto 的住处看了一下他租的 house 是怎样的。$1400 能够有独立的卧室和浴室洗手间,共享的客厅和厨房,我觉得不错啦。类似的空间在北京没有 ¥4000 你都不用想了,但你在北京的收入数字部分真的能做到 Palo Alto 的 3 倍吗?

5/29

在三藩市最后完整的一天,我选择了去参观 GREE,顺便体验三藩市室内公共交通。由于出门就有 MUNI 的 KT 线,所以上了 K 车后我也没怎么考虑 K 车和 KT 线到底是什么关系。话说 MUNI 的这些有轨电车很可爱,在路面上看就是有轨电车,但是进入隧道后感觉又好像地铁。中午跟 @jasonlai 到 Polo Grounds 吃汉堡(我又是冲着大块牛肉来的),下午到 GREE 看了一下,然后就准备搭 MUNI 往回走了。结果发现在我下车的站台上,反方向来的只有 T 车,那我到底要上不要上呢?一犹豫就错过了一辆车,接着下一辆车要 20 分钟后。好吧……无论下一辆是 K 车还是 T 车我都上,搭错了再说,反正我有 Google Maps,不信我找不到路回去。结果下一辆来的还是 T 车,走的路线则跟来时正好相反。(到现在我还没明白 KT 线和 K 车、T 车是什么关系。)

到倒数第二个站停车后,司机就不关门也不往前走了。看到前面轨道上停着一辆有轨电车,司机说估计 5 分钟内不可能解决问题,想要下车的人可以在这里下车。想想我只需要步行一个站,迅速选择下车往前走,发现前面堵了好几辆有轨电车,最后发现某个路口出事故了,警察在处理,两个方向的有轨电车都停下来等待。看来三藩市的公共交通系统还是挺脆弱的。

5/30

最后一天自己搭 BART 去机场,再经东京飞回广州,一路都很顺利。比较搞笑的是,到东京后被告知我们的飞机早到了,分配给我们的廊桥还被上一个航班占用着,机场正在跟他们沟通,希望他们能够准点离开。此外,在三藩市机场的时候我又忍不住花钱了,买了一本 PC Gamer 和一本叫做 The Zombie Survival Guide 的书。买前者是因为它的封面(有 Company of Heroes 2 的内容哦),买后者是为了在飞机上打发时间(结果在飞机上只看了一章,然后看了 3 部电影)。

总结一下,尽管这次湾区的路行安排有点匆忙,很多朋友也是临时约的,但抵达后能够见到那么多的朋友也实在是幸运。尽管没有车确实有点不方便,但朋友们提供了不少帮助,结果还是去了不少地方长了不少见识。这次唯一的小遗憾是没去看金门桥 75 周年庆典,不过根据去看了烟花的 @diamondtin 所说,看完烟花后人流散去公共交通资源实在稀缺,或许不去确实是正确的选择。

P.S. 在写这篇文章时,一开始地点我都是用 Google Maps 链接的,后来想想还是用 Foursquare 链接比较方便,因为地点我都 check-in 过,去 History 页把链接复制过来就可以了。此外,大家也可以通过 Foursquare 链接看到更多的照片,不仅仅是我发的,也有其他人发的。

2012年6月1日星期五

理想的技术面试过程

作为面试官

从在大学里面试社团大一新生,到加入百度后帮公司面试候选人,我觉得我对面试这件事一直不得要领。百度提供面试培训,也允许参考或使用题库,但我还是觉得不知道如何判断给不给一名候选人通过我这关。偶尔我会遇到非常优秀的实习生候选人,我能十分确定我要给他过,甚至想方设法确保他能来。其它时候,我觉得我的判断随机性太大,或许还不如一枚硬币做得好。

在百度做二面的时候,我往往会问一些组合问题,就是候选人需要有扎实的基础加上一定的解题能力才能做出来的。我假设一面的面试官已经问过基础问题,所以我不会再问基础问题。结果通常是,候选人的基础不够扎实,会作出一些错误的假设,甚至面对组合问题就无从下手,不知道如何分解为更小的问题然后再一步一步来解决。我不知道是否应该期望候选人全部答对,但答对小部分的状况让我无从判断。

为此我开始跟其它人讨论面试经验。Acumon 说应该针对候选人说他擅长的领域来提问,而且使用开放性问题以便了解候选人的思考方式,但我发现我遇见的大多数候选人都不清楚自己擅长什么,或者是他们自认为的强项无法达到我的预期。后来在上海跟 Winter 和 Hax 聊天时发现一个可怕的现实:大多数前来应聘的前端工程师都无法回答「position 属性取值都有哪些」以及「display 属性取值都有哪些」。随后我尝试在我的面试中先问这两个问题,发现确实有些人回答不出来取值,更多人则是无法准备描述常见取值的作用。(我甚至不把 inline-block 和 fixed 归类到常见取值里面。)遇到如此基础的问题都回答不出来的候选人,我通常会跟他告诉他正确答案,再问一些基础问题然后给他一些学习建议,最后很礼貌地送他走。

状况在我到达豌豆荚后有所改善。不是来应聘的人都非常优秀,而是我开始有感觉了。关键原因我觉得是我们不着急招人。现在我们还能应付手头上的工作,也没有快速扩张的需求,所以我们只有在遇到最合适的人选时才邀请他加入。我相信我前面的两位面试官做得足够好,然后我可以放胆地去考量「懂不懂」之外的其它方面。准确来说,这甚至不是一种考量,我只想知道我跟这个人一起工作是否会很愉快。我会以工作上遇到问题时同事跟同事间讨论的方式去跟他进行讨论,有可能是工作上实际遇到的问题,也有可能是刻意设计出来的有趣问题。(我觉得在一个充满活力的工作环境中,同事之间互相找些稀奇古怪的问题来讨论是很正常的,大家也会享受这类挑战。)如果我感觉跟他讨论问题是很愉快的过程,他能够提出有趣的想法,甚至能告诉我一些原本我不知道的事情,我肯定会给他很正面的面试评价。

作为面试者

换个角度来说,如果你作为面试者发现自己在面试的过程中能够进入这种状态,感觉如同跟同事讨论问题一样放松,那么你应该对面试结果充满信心。至少根据我个人的经验来说,感觉如同轻松愉快讨论的面试我都能得到面试官不错的评价。我明白要做到这一点很不容易,很多人在面试时都会很紧张,甚至会假设面试官一定会用各种难题来考自己,这种心态其实会把自己放在不利的位置上。我过去的经验是,我越不在乎的面试,往往我越能放松地跟面试官随便聊,结果反而越好。我很在乎的面试,反而有时候会高估面试官问题的难度,结果简单的问题没给出简单的答案,最后得到的评价反而不好。

在百度大厦的时候,我喜欢穿越二层平台,因为那里有很多小圆桌,也有很多人会选择在那里进行讨论或者面试。如果你对肢体语言有点最基础的了解,或者你就是在这方面有感觉,你会发现在那里很容易看得出哪些桌是同事间的讨论,哪些桌是在面试,以及谁是候选人。同事之间的讨论,往往大家都会很放松;面试的话,面试官也会很放松,但面试者通常处于比较紧张的状态,身体会略为前倾,就算不需要写字也会把手放在桌子上,用于支撑上半身的重量。对于有经验或者有感觉的面试官来说,这种肢体语言会传递一种不好的信号,那就是你太想要这份工作了,就算更深入的沟通后可能你会发现这份工作不适合你,但你还是会追求这个机会。

类似的场景也会出现在异性之间,下次去餐厅吃饭时你可以观察一下隔壁桌的异性交互。就算你完全不知道谈话内容,你也可以尝试从面部表情和肢体语言去判断谁在追谁。为什么有时候你追的人各种虐待你,还有充足的信心你不会放弃?因为他们看得出你把自己摆在什么位置上。在统计学的角度来说,我觉得女人在这方面比男人有优势,因为女人看的电视剧都不是白看的,她们在逛街吃饭时还无时无刻地在观察身边发生的各种剧情。就如同资深前端工程师可以只看 CSS 片段就猜出遇到了什么浏览器 bug 以及作者尝试如何解决一样,有经验的女人(又或者是面试官)要看明白你的立场太容易了。

因此,首先你要把面试看作一个平等的双向选择过程,不仅仅公司在选择你,你也在选择公司。如果面试官问的问题显得他很没有品味,或者是 HR 的某些安排让你觉得这家公司的文化很有问题,你随时可以提出说你没兴趣聊下去。这时候你反而可以比较好的发挥出来你真实的实力。对于你真的不知道的问题,你就直接说出哪部分你不知道或者不确定。同事间讨论也会遇到你不懂的问题,你平时是怎么处理的在面试时就怎么处理,面试官不会因为你直率地表达不懂某个点而鄙视你的,他应该帮你把这个点绕过去然后继续。

我觉得最好的面试建议就是 Changhao 跟我说的那句,「Be yourself」。(其实这也是很好的追女建议。)

改善面试流程

既然技术面试应该平等地讨论问题,通过感觉这个人是否能成为一名你喜欢的新同事来做出判断,那又应该如何衡量他的技术能力呢?这其实应该放在面试流程尽可能靠前的部分来做。很多外企的标准做法是,通过首轮(或前两轮)电话面试来判断一个人的技术,随后通过多轮的面对面讨论来判断这个人是否适合这家公司这个团队。我相信这套方法在美国执行起来没什么问题,因为空缺和候选人的数量差不多。

当然,上述流程来到中国后就必然会发生变化。中国有能力来做这份工作的人太多,同时还有更多的人想要来浑水摸鱼,这就导致了招聘成本大大提升。如果在中国还是用首轮电话面试来根据候选人的技术能力做一个初步的筛选,我想面试官就要疯掉了,所以大多数大公司都会在此之前加上一轮笔试。(对应届生可能是多轮笔试,否则面试官就阅卷都足以疯掉。)

有些公司会选择通过 ACM/ICPC 竞赛经历做出筛选,尽管无法保证获得最优结果,但至少能获得次优结果。招聘本来就是个搜索问题,一个在美国能有效求解的算法到中国来发现搜索空间大小增长了几个数量级,自然要想办法调整算法提高性能。这是非常理性的做法。当然它会带来一些外部性,例如说使得更多中国学生在可能不适合或不喜欢参加竞赛的前提下选择参加竞赛。

这些方法确实能把想要浑水摸鱼的人筛掉,不过能通过的人有时候还是太多了,所以接纳候选人过多的大公司往往会无端增加面试难度,问一些纯粹刁难人的题目。我觉得这没什么意思,也没在应届生面试以外的场合遇到过。既然应届生及格的太多,我觉得就直接随机抽签好了,这是最公平的做法。抽签可以通过公证过的算法来做,这比问一些候选人可能随机地知道或不知道的问题可靠多了。

最后说一下,为什么我觉得平等讨论的面试方式比问一堆问题要好。不同的面试方式,取决于你把候选人看做一个人力资源单位,还是一个有个性的人。如果你把候选人看做一个人力资源单位,他只要能完成你给出的任务就可以了,那确实是能力测试更重要。如果你把候选人看做一个有个性的人,你就需要知道他的个性能否很好地融入团队当中,这使得你在面试过程中必须把他当做一名未来的同事来看待。

2012年5月9日星期三

怎么样才算是 RESTful?读 REST in Practice

最近 O’Reilly 搞活动,我就半价买了一本《REST in Practice》(Kindle 版链接)。对于 O’Reilly 的书,我通常会对比 O’Reilly 打折后的价钱和 Kindle 版的价格,通常是那家更便宜就在那家买,但图表或代码比较多的我就会坚持买 O’Reilly 的版本,因为 PDF 能够最好地保存这些格式。

回到 REST 的话题上。尽管这个概念 2000 年就被提出来了,2007 年成为了一个热词,随后越来越多的服务都宣称自己是 RESTful 的,但是到底真么做才是真正的 REST 我从来没有自习学习过。由于 2007 年的时候 Ruby on Rails 也十分热门,所以我以为 Rails 风格的 CRUD API 就是 REST 了,同时对于外界关于「什么算是 REST 什么不算是 REST」的争论没怎么关心过。

记得之前老赵提到过《REST in Practice》是本好书,所以我就收藏到 wish list 里面了,在 O’Reilly 打折时就下狠心买下来,然后看看争论得如此多的 REST 到底是什么东东。书中提到 Richardson 的 REST 成熟度模型,通过这个模型你可以理解什么是 REST 什么不是 REST。这个模型是这样子的:

第 0 级服务:只使用一个 URI 作为一个服务端口,也只使用一个 HTTP 方法传输数据。大多数 WS-* 服务都是这个级别的,XML-RPC 和 POX 也是。这种做法相当于把 HTTP 这个应用层协议降级为传输层协议用。HTTP 头和有效载荷是完全隔离的,HTTP 头只用于保证传输,不涉及业务逻辑;有效载荷包含全部业务逻辑,因此 API 可以无视 HTTP 头中的任何信息。

第 1 级服务:使用多个 URI,不同的 URI 代表不同的调用入口,但只使用同一个 HTTP 方法传输数据。

第 2 级服务:使用多个 URI,不同的 URI 代表不同的资源,同时使用多个 HTTP 方法操作这些资源,例如使用 POST/GET/PUT/DELET 分别进行 CRUD 操作。这时候 HTTP 头和有效载荷都包含业务逻辑,例如 HTTP 方法对应 CRUD 操作,HTTP 状态码对应操作结果的状态。我们现在看到的大多数所谓 RESTful API 做到的也就是这个级别。

第 3 级服务:使用超媒体(hypermedia)作为应用状态引擎。要解释这个概念先要解释什么是超媒体:

我们已经知道什么是多媒体(multimedia),以及什么是超文本(hypertext)。其中超文本特有的优势是拥有超链接(hyperlink)。如果我们把超链接引入到多媒体当中去,那就得到了超媒体,因此关键角色还是超链接。使用超媒体作为应用引擎状态,意思是应用引擎的状态变更由客户端访问不同的超媒体资源驱动。

举个例子来说,用户在论坛的帖子列表点击超链接进入某个帖子,这时候浏览器作为一个客户端就会去 GET 这个帖子的链接 URI,应用状态就切换为显示某个帖子了。接着用户输入回复提交表单,浏览器就会根据 form 上面的 action 属性 POST 内容给目标 URI,应用状态就再一次发生切换,完成了回复的存储。

使用超媒体与前面第 1 级、第 2 级的显著区别是,客户端不再和 URI 紧耦合。在第 1 级或者第 2 级的应用里面,客户端都需要知道资源使用的 URI 模版(如 /orders/{id}),然后要操作什么样的资源就生成什么样的 URI。超媒体客户端只知道入口 URI,之后的每一个 URI 都是通过超链接获得的。

还是用上述论坛例子来解释,假若这个论坛通过 Atom 协议支持非浏览器的客户端访问。客户端是不需要知道论坛帖子的 URI 模版的,因为客户端可以通过帖子列表的 Atom 获得帖子的超链接,然后在用户选择浏览帖子时获取对应 URI 的内容。获取回来的结果不会带有 form,但会带有 <link rel="reply" />,通过这个 link 客户端又知道了用户提交的回复应该发往哪个 URI。

说完 Richardson 的成熟度模型,说说 REST 这篇论文作者 Roy Fielding 的回应。Roy Fielding 说「只有使用了超媒体的才能算是 REST」。简单来说,他认为第 3 级成熟度以外的都不算 REST。我个人的看法是,我支持 Roy Fielding 对 REST 的严格定义,我也认为一个真正使用了超媒体实现应用状态引擎的服务非常了不起,但是在普通 CRUD API 能满足需求的情况下我觉得使用 CRUD API 也可以。

这本书后面还讲到了如何使用缓存来增加系统健壮性,如何使用 Atom 和 Atom 发布协议,如何使用 OpenID 和 OAuth 等认证授权技术。因为这本书我还没看完,所以这些内容我也不好做评价。

这本书总体来说写得不错,但就是废话稍微多了一点点。如果你跳着章节地去看,可能就不会觉得那是废话了,因为你挑选出来阅读的部分都包含所有的信息。但如果你像我这样从头到尾地阅读,就会觉得作者怎么前面提到过的事情后面还要再提及。

此外这本书提供的大量 .NET 和 Java 代码对我来说没什么意义。我觉得这本书更多是写给做企业服务的人看的,希望他们的思维模式能够从企业环境里常见的 WS-* 跳出来,同时希望证明给他们看实现 REST 是多么简单的事情。这对于天天研究互联网服务的我来说没什么必要,况且我也很久没写过 .NET 代码了,所以各式各样的示例代码给我无视掉了。我觉得我知道这件事情 WCF 能做也就足够了。

2012年4月24日星期二

把 Blogger 服务的博客都迁移到 Disqus 评论了

一直使用 Blogger 的服务,因为我觉得使用起来比较放心,相信 Blogger(实际上是 Google)能够保证数据不会丢失。我的第一需求是要能专心写文章,第二需求是前端在有需要的情况下能够定制,这两点 Blogger 都能很好地满足到,所以在大家都忙于搭建 WordPress 时我还是坚守在 Blogger 上。因此,我不需要面对因为自己的失误,或者宿主环境的问题,而导致 WordPress 数据丢失的风险。

尽管对 Blogger 没什么好抱怨的,不过看到 Disqus 提供的评论服务还是觉得挺有吸引力的。看到越来越多的大网站使用 Disqus,相信越来越多的用户会拥有 Disqus 账号或能登录到 Disqus 的第三方账号,我就开始考虑迁移到 Disqus。背后的主要原因是,Blogger 评论支持的账号系统不多,不是很 geek 的人通常不会拥有它所支持的账号,而不登录的话就难以形成对话。(谁会去看自己匿名发表的评论是否有回复?)我希望我的评论区域能够形成对话,就好像在博客园那样子,大家都使用博客园账号互相回复讨论问题,因此我决定换成 Disqus 试试。

将 Disqus 部署到 Blogger 比我想象中的要容易。尽管 Blogger 不像 WordPress 那样允许你随意改后端代码,但 Disqus 早就准备好了前端解决方案——只要是有评论的页面,Disqus 就会将 Blogger 原有的评论隐藏掉,然后把自己显示出来。轻松跟着 Disqus 的 Blogger 迁移向导做,点几下就能把 Blogger 的评论替换为 Disqus。

完成功能迁移后,接着就可以进行内容迁移了。Disqus 也知道 Blogger 用户原本可能已经拥有评论了,所以它可以帮你把内容都迁移过来,只不过账号是不可能对应上了(就算该评论作者在 Disqus 有对应账号)。尽管 Disqus 写的是 24 小时内能够完成内容迁移,但实际上我等了几天也没见它有动静,后来不再在意这件事了,却发现迁移完成了。因此,如果你要做类似的迁移,我建议你先迁移内容,然后再迁移功能,这样能够做到无缝切换。

2012年2月8日星期三

如何帮助别人改变坏习惯

最近为了写一个 slides 而重新看了一遍 The Mystery Method,然后又重新学习了一遍我已经学习过并且忘记掉的知识。其中让我觉得比较有意义的一条是关于如何训练别人的行为模式的,尽管背后的道理源自对动物行为模式的训练。

如果你的狗总喜欢跳到你身上来,你该怎么办?你可以打它骂它,但它无法理解你的情绪,所以它只会把这理解为关注,也就是你在乎它,因此它会继续跳到你身上来,以便获取更多的关注。你可以把硬币放在一个罐子里,然后摇晃那个罐子让狗因为恐惧而走开,但久而久之它会理解到你和罐子存在关系,因此把你和厌恶联系在一起,这显然也不是你想要的结果。正确的做法是,先通过摇晃罐子让它从你身上下来,然后让它做一个简单容易做到的动作,例如跟它说「sit」,等它坐下来后在拍拍它给它一些奖励。这样子它就会把厌恶和错误的行为联系起来,但不会把厌恶跟你联系起来,因为你还是会奖励它的。

尽管 The Mystery Method 把这个方法用于训练别人的服从性,不过如果你想帮助身边的朋友改掉坏习惯这也是适用的。可能你觉得,既然大家都是朋友那么平等理性地沟通就可以了,只要对方知道这是个缺点他就可以改掉。然而事实不是这样的,可能沟通并不一定能够很好地传达你的意思,让对方有足够的决心去改变;可能对方一早就知道这个缺点的存在,但是他的意识无法扭转无意识的行为习惯,久而久之他就会合理化自己的行为,为自己无意识的行为辩论。

关于意识和无意识,我喜欢用 The Happiness Hypothesis 里面的那个比喻——无意识是大象,有力量,能够做重复性劳动,但是智慧不是很高;意识是骑象人,力量比不过大象,而且还懒惰,但智商足够高。这个比喻的一个要点就是,人永远不能跟象较劲。象看到了香蕉就要冲过去,而人看到了香蕉就在悬崖边上,这时候人想拉住象是不可能的。人应该使用自己的聪明才智防止这种情况的发生,例如训练象看到香蕉不要那么冲动。

显然你不希望在帮助对方改掉坏习惯的过程中让对方讨厌你,尤其是无意识讨厌你同时意识到应该感激你。前面已经说了,人是不能跟象较劲的,所以如果你有幸让一个人无意识讨厌你,那么无论他多么明白事理,他也无法让自己喜欢上你。为此,当你需要帮助别人改变习惯的时候,不一定需要跟对方的象骑者沟通。如果对方足够聪明,也足够信任你,他会允许你直接训练他的象,并且他也能理解你在做什么。

2012年1月31日星期二

Google Search Plus 把用户选择放到了内容质量的前面

Google 发布了 Search, plus Your World,简称 Google Search+,带来最大的改变包括 3 点:

  1. 跟个人相关的搜索结果:你跟 Google+ 上好友发表过的链接和照片都会出现在搜索结果内,包括通过 Circle 限制为非公开的。
  2. Google+ Profile:如果搜索人名,自动完成和搜索结果都会出现对应的 Google+ Profile。
  3. 相关 Google+ Profile:如果 Google+ 上有 Profile 跟你搜索的关键字相关,会出现在搜索结果页右侧。

其中最直接影响搜索结果排名的是第 1 点,后面两点都只是往搜索结果里插入少数 Google+ Profile 条目而已。这第 1 点可以说是把用户选择放到了内容质量前面来,使得过去完全针对内容的搜索引擎优化手段变得需要同时考虑针对用户做优化,也就是引入营销手段。

在过去,搜索引擎优化考虑的是内容质量。理论上来说,如果内容质量足够高,别人就会频繁引用你的页面,这样你的页面自然应当排到前面去。但是有了 Google Search+ 之后游戏规则彻底改变了,就算你的页面内容十分小众,只要它被某人发布到 Google+ 上去了,它就会出现在此人好友的搜索结果上。这时候,被多少人 +1 就显得比内容如何更重要。

这个变化可以说是由作者投票变成了读者投票。过去 PageRank 是以对待论文的方式来对待页面,被引用的次数越多,引用来源越重要,这篇论文也就越重要。然而,能够影响论文引用的只有论文作者,有多少读者读过一篇论文后受益是不知道的,除非他为此再写一篇论文。现在 Google Search+ 的做法可以说是把页面对读者的影响力也计算在内了。

跟作者投票不一样的是,读者投票的影响是跟个人相关的。只有离你近的读者投票才会影响到你的搜索结果,离你远的读者投票对你影响不明显(除非你指定搜索他的信息)。因此,如果要让你的内容通过搜索引擎展示给特定的受众看,你不一定再需要把内容优化为其他作者会认同的样子,只要你有营销手段让受众中的一部分先认同你的内容,搜索引擎就会让这种认同扩散出去。

尽管已经有很多社会化营销手段涉及到 Facebook 和 Twitter,Google Search+ 暂时还不支持这两者,所有社会化信息仅来自 Google+。无论这是 Google 有意的恶性竞争行为,还是技术上和商务上暂时没把对 Facebook 和 Twitter 的支持做好,现在你要影响 Google 搜索结果中的社会化成分就必须通过 Google+。估计很快网站就会重视页面上嵌入的 +1 按钮,并且使用各种方式去鼓励用户点击那个按钮。

2012年1月18日星期三

如何设计大规模 JavaScript 应用 (Part 1 - 概览)

背景

我一直很关注如何设计大规模的 JavaScript 应用,因为我一直在做的都是大规模的 JavaScript 应用。从百度 Hi 网页版到百度地图,从 Yahoo Search Direct 到豌豆荚客户端。好吧……Yahoo Search Direct 本身的规模不大,但作为一个网页插件它要能跑在任何宿主页 面上,其复杂度也不低。在大规模 JavaScript 应用开发和维护的过程中,有两个问题尤其值得关注:设计和性能。前者是必须在开发阶段之前做好的,开发开始后就来不及改了,只能事后重构;后者则更多发生在开发的中后期,等 profiling 结果出来了,再针对瓶颈做优化。在这个文章系列中,我们的关注点是设计。

尽管我一直在写跟设计相关的文章,不过现在看来我过去写下的文章都有点山寨了,也不够 update。在我看到 Large-scale JavaScript Application Architecture 这个幻灯片后,我决定重新写一个系列来说说我在大规模 JavaScript 应用方面的经验,也包括一些设想。

如果说大规模 JavaScript 应用设计方面有什么核心原则的话,我觉得核心原则就是这一条:永远不要尝试构建大规模应用。构建小应用,保证它们的可测性,然后把它们组装成大应用。因此,我觉得在讨论任何设计模式之前,我们先要讨论一下如何把大应用拆分成小应用。

“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application”

- Justin Meyer

模块化

CommonJS Modules

先说最理想的模块化方式,那就是 CommonJS Modules。一个模块系统至少要能解决两个问题:依赖项的加载、私有作用域和公有导出成员的区分。CommonJS Modules 通过 requireexports 很好地实现了上述两个功能。下面是一个 CommonJS Modules 模块定义和使用的例子:

util.js

var util = {
    extend: function(target, source) {
        /* implementation */
    }
};
util.extend(exports, util);

feature.js

var util = require('util');
var features = {
    core: {
        start: function() {
            /* implementation */
        }
    }
};
util.extend(exports,features);

app.js

var features = require('features');
features.core.start();
AMD

考虑到浏览器里面没有 CommonJS 的 Modules 接口,也不可能完整实现这样的 Modules 接口,所以就有了 AMD (Asynchronous Module Definition) 这样的解决方案。AMD 使用 define 函数定义模块,要求模块提前声明依赖项,然后通过回调加载模块,解决了浏览器无法同步加载模块的问题。下面是一个 AMD 模块定义和使用的例子:

util.js

define('util', [], function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
};

feature.js

define('features', ['util'], function(util) {
    return {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
});

app.js

define('app', ['feature'], function(feature) {
    features.core.start();
});
CommonJS Modules/Wrappings

考虑到 AMD 的写法跟 CommonJS Modules 的写法区别十分之大,要把已有的 CommonJS Module 写法模块改为兼容 AMD 不容易,所以又有人设计一些改动不那么大的写法,如 AMD 工厂函数的 function(require, exports, ...) {...} 签名,或 CommonJS Modules/Wrappings (意思是 Modules 的 Wrappings)。由于浏览器必须异步加载依赖项,所以这些写法只能通过对工厂函数源代码做静态分析提前找出依赖项,在异步加载好之后再执行工厂函数。这样做的坏处是工厂函数内部对 require 的调用缺乏灵活性。下面是一个 CommonJS Modules/Wrappings 模块定义和使用的例子:

util.js

define(function(require, exports, module) {
    var util = {
        extend: function(target, source) {
            /* implementation */
        }
    };
    util.extend(exports, util);
});

feature.js

define(function(require, exports, module) {
    var util = require('util');
    var features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

define(function(require, exports, module) {
    var features = require('features');
    features.core.start();
});
UMD

尽管 Node.js 写好的 CommonJS Modules 模块可以通过 CommonJS Modules/Wrapping 包装一下使得它能用在浏览器内,尽管包装过的模块通过 r.js 也能用于 Node.js 环境下,不过这不是完美的解决方案。因此,又有人提出 UMD (Universal Module Definition),希望提供跨平台的模块定义方案。

UMD 现在还没有定稿,不同的人提出了不同的解决方案。最全面的方案同时支持 AMD 和 Node.js,顺便还把传统的浏览器脚本顺序加载模式也兼容了。具体的做法就是判断环境中是否存在 AMD 所依赖的 define,如果存在的话就使用 AMD 加载,不存在的话就使用别的方式模拟 AMD 加载。

util.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define('util', [], factory);
    } else {
        root.util = factory();
    }
})(this, function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
});

feature.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('util'));
    } else if (typeof define === 'function' && define.amd) {
        define('feature', ['util'], factory);
    } else {
        root.feature = factory(root.util);
    }
})(this, function(util) {
    return features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('feature'));
    } else if (typeof define === 'function' && define.amd) {
        define('app', ['feature'], factory);
    } else {
        root.app = factory(root.feature);
    }
})(this, function(feature) {
    var features = require('features');
    features.core.start();
});

小结

在这篇文章里面,我们了解了大规模 JavaScript 应用设计的核心原则:使用模块化的方式把大应用分解为小应用来编写和维护。同时我们也看了不同的模块定义方式,我们可以针对平台来选择一种合适的方式,也可以选择通用的方式但需要维护更多的代码。

在有了模块的概念之后,我们就可以讨论具体的设计模式了。这个系列接下来将会深入讨论每一个对大规模 JavaScript 应用设计有用的设计模式。如果你对这个系列的文章感兴趣,可以选择订阅本博客。

2012年1月12日星期四

Covariant(协变)与 Contravariant(逆变)

今天为了解释某个问题而提到协变和逆变,发现每次解释这两个概念都会忘掉它们的本质,然后要重新看看定义,重新消化一下才能说明白。所以我决定把自己对协变和逆变的理解写下来,以免将来再次忘掉。

我知道 .NET 的用户喜欢用 delegate TResult Func<in T, out TResult>(T arg); 来解释协变逆变,我则喜欢把 Func 的签名简写为 Haskell 签名形式。也就是说,把 Func<T, TResult> 写成 f :: a -> b 的形式;把 Func<T1, T2, Result> 写成 f :: a -> b -> c 的形式。

其实无论是协变还是逆变,本质都是一样的:对于签名为 f :: A -> B 的函数,实际可接受的参数范围为 ASub,实际可返回的参数范围为 BSub。这个很容易理解吧?任何时候子类的实例都可以当做超类实例来使用,无论是接受还是返回。

协变和逆变用于描述高阶函数签名,如 f :: (X -> Y) -> Z。那上面的 f :: A -> B 做模版,我们可以把 (X -> Y) 看做 A,把 Z 看做 B。应用同样的逻辑,函数实际可接受的参数范围是 (X -> Y) 的子类,实际可返回的参数范围是 Z 的子类。对于后者我们没什么疑问,但 (X -> Y) 的子类到底是什么呢?它的所谓「子类」应该是 (XSuper -> YSub)

为什么说 (X -> Y) 的「子类」应该是 (XSuper -> YSub) 呢?因为子类在能力上应该完整覆盖超类的能力,因此如果对方要求你提供一个函数,这个函数接受 X 类型返回 Y 类型,你提供的函数至少要能接受 X 的超类而返回必须是 Y 的子类。这时候 X 是逆变参数(类型可以更宽松),而 Y 是协变参数(类型可以更严格)。

一般来说,如果把「类型可以更严格」看做协变的话,函数的返回类型一定可以协变,非高阶函数的参数也可以协变,高阶函数的非函数参数同样可以协变。把「类型可以更宽松」看做逆变的话,只有高阶函数中的函数参数中会出现逆变,也就是作为参数的参数出现。那么参数的参数的参数呢?也就是说高阶函数的参数仍然是高阶函数,那会怎么样呢?这个大家可以尝试自行分析,盯住 f :: ((X -> Y) -> Z) -> W 看一会儿,再不停类比上文的 f :: A -> B,或许你就明白了。

2012年1月6日星期五

世界顶级黑客自传:Ghost in the Wires

上个星期终于把 Ghost in the Wires 看完了,现在就抽时间来写写书评吧。尽管我一向懒得写书评,不过这本书真的是超级推荐!

最初我是在 Audible 上面买了这本书的有声读物,然后发现朗读得超级好,尤其是 Kevin 知道危险逼近时心中的独白——「Fuuuuuuuck!」朗读的语气掌握得非常好。后来我决定买 Kindle 版来仔细读一下,因为有声读物不会专心听,只能把我到大意。随后我边听边读把整本书看完了。

这本书整体上分为 4 大部分。第 1 部分讲述 Kevin Mitnick 是如何入行的——他父亲和叔叔都是商人,这使得他很小的时候就意识到通过言语可以驱使他人做一些对自己有利的事情。同时他又是个超级 geek,从小喜欢在书店里面看一些稀奇古怪的书,例如说如何伪造身份让自己凭空消失,或者是如何获取他人的驾驶记录、资产记录、信用报告、银行信息、非公开电话号码。

由于这些爱好的驱使,他接触到了电话玩家(phone phreak),也就是一群喜欢研究电话系统和拿电话系统做实验的人。然后他开始学习如何打免费电话(Steve Jobs 和 Steve Wozniak 也做个类似的事情),并且如同研究黑箱电路一样通过不停地打电话探索电话公司内部的组织形式。

后来由于同伴的出卖(这又是因为 Kevin 爱在同伴面前耍小聪明),Kevin 被捕并被起诉。在这个过程中,公诉人和媒体想尽办法抹黑 Kevin,给他安上各种奇怪的罪名,例如黑进 NSA、切断假释官电话、篡改法官信用报表等等,其中最莫名其妙的是说他由于喜欢明星 Kristy McNichol 而切断她的电话服务。(Kevin 独白:有见过因为喜欢一个人而切断她电话的吗?)正是由于这段经历,导致 Kevin 后来被 FBI 追捕时选择了逃亡,因为他认定政府是邪恶的,只会想往他头上加更多的罪名。

第 2 部分讲 Kevin 假释后的生活。尽管他的假释条款规定了他不能再进行违背道德的黑客行为,也不能再跟别的黑客有所接触,但为了调查弟弟死亡的真相,他还是忍不住黑进电话公司并且监听被怀疑对象的电话。随后他又回到了黑客道路上来。这时候他碰到了一位神秘黑客 Eric,看样子跟娱乐圈混得很近,不用工作也能过得很滋润,而且跟 Kevin 一样对电话系统有着深入的了解。最最重要的是,Eric 知道 Kevin 不知道的一种设备,叫做 SAS,可以用来全自动地监听任意线路。

Kevin 一方面对 Eric 有所怀疑,觉得他可能是卧底或者是线人;另一方面又受不住 SAS 的诱惑,希望搞清楚这套系统是怎么用的。最终他假装电话公司员工,向 SAS 制造商要到了完整的 SAS 设计手册,里面的资料比电话公司拥有的 SAS 用户手册还要丰富,包括用户用不到的调试指令。随后 Kevin 要想办法绕过 SAS 的安全策略来监听别人。

SAS 如此强大的设备自然有相对严谨的安全策略——你可以拨号给 SAS 让它监听一条线路,SAS 也能帮你监听这条线路,但监听的结果不会输出到你拨入 SAS 的线路,只会输出到你指定的回拨线路,问题就在于每一台 SAS 都限制了回拨线路,你不能让它回拨到任意一个号码上。Kevin 想了一个很聪明的方法来绕过这个限制:他先用线路 A 拨入到 SAS 的回拨线路上,然后用线路 B 向 SAS 发送指令。SAS 要回拨时它自然会接上回拨线路,实际上就接到了线路 A 上,这时候 Kevin 就对着话筒哼一个线路就绪音(就是我们平时拿起听筒听到的声音),SAS 以为线路就绪了就开始拨号,实际上线路早就被接通了。

电话公司发现有人入侵了,安全小组就联通 FBI 一起侦察,而 Kevin 发现自己被监听以后,就想办法监听电话公司安全小组以及 FBI 的通话,以便搞清楚到底对方都收集到多少证据了,自己是否还有机会脱身。在如此几个回合后,Kevin 决定是时候人间蒸发了,于是就开始了他的逃亡之旅。

第 3 部分讲 Kevin 逃亡的过程。他先换了一个临时假身份来到了丹佛,然后再换一个长期假身份。每一次他都先要把出生证明和驾驶证骗到手,然后再去办社会保障号码卡。由于他用的都是真是存在的同龄人身份,简历上印的学历也是真实的,所以他能够找到工作赚钱生活下去。在这个过程中,他还弄到了一叠盖好章的出生证明!这意味着将来他想换身份就更容易了,直接把想要克隆的身份填写到出生证明上面就可以了,然后去考驾照和申请补发社会保障号码卡。

第 4 部分讲 Kevin 最终被捕的过程。Kevin 一直做足安全措施,包括使用克隆的手机号码,拨号时利用先前已经黑下来的电话公司交换机设置多重呼叫转移使得拨号源头难以跟踪,为此 FBI 一直都抓不到他。最后他黑了安全专家 Tsutomu Shimomura。对方是个高傲的黑客,因为被黑而决定帮助 FBI 抓住 Kevin,FBI 为此也给了他很多平民在合法程序上不可能获得的特权,例如监听电话和网络。最终 Kevin 被捕,同时公诉人再次想往他头上套上最严重的罪名,结果反而导致了 Free Kevin 活动的活跃。

媒体最初一直把 Kevin Mitnick 渲染为大黑客,说他只要对着电话吹口哨就能发射核导弹。现在由于政府玩大了,风向就发生了变化,人们开始支持 Free Kevin——不是说他无罪,而是说他应该在合理的范围内承担他的罪名。他盗窃了大量的源代码和信用卡信息,但是他没有利用这些信息赚钱,所以造成的损失也就是各大公司少赚了一份源代码授权费用而已,因此应该根据这部分损失来做出判决。

公诉人和法官当然不这样想,但 Kevin 竟然找到了一份极具参考价值的判例——一位税局官员曾经处于好奇心而获取了众多议员的税务信息,最终因为他没有因此获利而被轻判,理由是他只是好奇心驱使而已。这个判例使得 Kevin 获得了合理的对待,他只需要赔偿源代码授权费的损失就可以了,而且赔偿金额还要根据他未来几年的赚钱能力调整。考虑到假释条款上规定他不能使用电脑、不能接触电话、不能做技术咨询……基本上他就只能去麦当劳煎牛肉了,法官判他 $4000 多一些的赔偿。

假释期满后,Steve Wozniak 亲自给他送了一台最新款的 PowerBook G4,然后他又回到了他的黑客世界。只不过这次他是开咨询公司,在道德的范围内,在客户的要求下,去黑客户的公司,帮助客户找出他们的漏洞。

总的来说,如果你对社会工程学有点好奇心,或者想知道美国政府是如何不小心把 Kevin Mitnick 搞成头号黑客的,Ghost in the Wires 能够很好地满足你的好奇心。里面有技术细节,但非常少,就算你完全看不懂也不影响对情节的理解。

P.S. 这本书里面的每一章开头都有一段密文,有兴趣的人可以自己尝试去破解,懒得破解的可以搜索相关的文章。

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 的理解。