优秀的JavaScript模块是怎样炼成的

如今的JavaScript已经是Web上最流行的语言,没有之一。从Github上的语言排行榜https://github.com/languages上 即可看出,也是如今最为活跃的开源社区。随着Node的加入,JavaScript开枝散叶进入服务器领域,为这个语言榜的占比,也贡献了几分热度。尽管 经历了Web2.0的洗礼 ,但在国内谈及开源,开源人士似乎都当这门语言并不存在,这也意味着国内的开发中坚阶层,并没有改变JavaScript以及前端过去二流形象的认识,也 没意识到JavaScript如今真正的价值。历史原因造就如今的局面,但是你并不能就此否认,一个出身不好,小时候还挺调皮的孩子,他长大后就是没有出 息。本文将介绍一个优秀的JavaScript模块应该是怎样炼成的,以期望未来国内的开源社区能够涌现出更多的优秀模块。
引起我写这篇文 章冲动的是近期接触到moment模块时的震惊。用JavaScript写程序,通常要经历两个阶段:第一个阶段是采用一些模块来擦除 JavaScript语言(跨平台)自身的问题;第二个阶段是自己写一些方法来擦除引入的模块的问题。第三个阶段才会真正进入业务开发中,前两个阶段俗称 “擦屁股”。时常在第三个阶段时,我会陷回到第二个阶段,和各种模块碰撞,往往会引起一些心情上的不舒爽。而在接触到moment时,这些烦恼瞬间消散, 我在心底知道为何会如此释怀,如碰到心仪的女神。这也让我回头反思过去在使用和写模块过程中遇到的挫折和收获,这落差让我产生了如此的冲动,此文算是一个 总结,期望能在汲取优秀模块的经验中,国内开源社区能够成长起来。

为什么是模块,而不是库或者框架

在过去几年做前端的同 学多半谈论的是库,或是框架,提及库则是Prototypejs和jQuery,框架则是YUI3,Dojo、Extjs等。关于库或者框架的,对应的比 喻则是大教堂和集市。在最早的时间,大教堂和集市的建设都在同步行进,具有代表意义的是YUI3和jQuery,前者Yahoo!投入许多精力,历时两代 完成,后者则是集市建造的典范,由John Resig创建,社区参与。最后看看后续的反馈:

大教堂未必优质

大教堂模式产出的作品,让享用者可以有一步到位的服务,无需其他。但是由于大教堂的建设过程中承担着解决所有问题的使命,这导致它逐渐变得庞大,尽管它有着宏大而美妙的结构,像一首长长的赞美诗,任何一段都可以被唱诗班演唱得动听。但是,弱点依旧暴露出来:

  1. 局部的优良程度未必是最好的;
  2. 庞大的结构导致灵活度下降,升级困难。
  3. 任何一条龙服务,都不能保证每个环节都是优异的。

小集市钻石闪烁,沙砾良多

小集市的特点是开放式建设、周期短、成本低。大多数创建出来的集市是功能简陋、品质平庸的。jQuery是一个值得玩味的现象,品质极高,但是它带来的插件市场,却体现了小集市的另一面,大多数的jQuery插件的质量却十分低下。
可 以说小集市模式创建出来的作品,在单点上,是能够超越教堂模式作品中对应的那部分。前者缺乏一个宏伟的结构,但是在微观上,它是完善的,可以随意挪移的。 后者虽然具备优良的结构,但在单点上并非优秀,这也容易让人怀疑是否其他点上也并不优秀,而且教堂式作品的一个特点则是,局部是不可移植的,非独立性的, 这在YUI3中表现十分明显。
上述对比很容易让人联想到,如果一个架构的特性具备单点可移植,可拆卸,自身极其轻量,汲取了大教堂和小集市 的优点。那必将是一个全新的时代,那是一个可以DIY的时代。原有的大教堂模式由于缺失了灵活性,在迭代迅速的开发中,必将淘汰。而小集市尽管良莠不齐, 好在预留了选择权利给用户,并且他的开放性也预示着它的可成长性,所以还有未来可期。
没错,如今这个时代已经到来。库通常是一个比框架小一 个粒度的单元,模块则是比库更小一个粒度的单元。一个库可能由几个模块组成,框架则可能是在几个库的基础上构建。不再谈论库和框架的原因是将库和框架的架 构部分还给开发者,以便开发者可以根据实际业务去构建合适的解决方案。任何框架或库都只具备解决某一方面的能力,不具备普适性。当粒度降低到模块级别的时 候,构建任何上层业务,可以实现按需使用。由于模块的粒度更小,所以相对库而言,更加专注,变优秀的成本更低。类比孩子在充满沙砾的海边奔跑,但很容易装 满一口袋的贝壳。对于稍具慧眼的开发者而言,挑选一堆适合的模块来解决业务的问题,比使用教堂式作品或者更高效。
这种方案因为CommonJS模块规范的影响,已经在既定事实中成型。在Node中,NPM平台(https://npmjs.org/) 上13000+的模块数量可以说明这个问题。前端中由于受到历史原因的影响,进展较慢,国内玉伯的SeaJS在推动此事,Arale是在SeaJS基础上 创建了一些模块,这类模块可以非常容易迁移到其他符合CommonJS模块规范的环境中。除此之外腾讯的JX也具备SeaJS的特性,可以轻松将别的库应 用到JX中,但是还不够规范。
如果非得给这种模块提供方式一个名称,我觉得该叫做小教堂模式,具备小集市的小,意味着这个教堂可能只提供祈 祷服务,如果注册结婚,则需要换另一家小教堂。但是这类小教堂的服务是最优质的。创建这类小教堂只比创建一个优异的小集市略费成本,换言之,优异的小集市 就是小教堂,它的创建方式是沿袭小集市的开放、透明的、有既定目标的成长性的。
另一个考虑是,在大部分的情况下,我们是不需要大教堂的,现 实中为了整体环境,我们宁愿在大教堂中祈祷,但是程序设计中,我们不会因为喜欢一棵树木,而买下整块山头,我们几乎不喜欢任何看起来多余的部分。同时我们 也不需要小集市,我们需要的是一个品质优良的商场。开发者是采购者,采购模块的过程既是架构的设计的过程。
前文我也提到这种小教堂的模式依 然存在不理想的功能重叠、功能多余、功能缺陷、功能冲撞等问题。可谓说完美的小教堂是可遇不可求的,但一旦它完美,将再难被替换。目前阶段的 JavaScript模块开发还存在着这些问题。是故,如果开发者了解如何去打造一个优秀的JavaScript模块,并乐于贡献到开源社区,这将大幅提 升社区JavaScript水平,后续的开发者在做业务架构时,将具备更优质的材料来DIY更优异的产品。

炼成优秀模块的最佳实践

其实这并不算是精确的最佳实践,只是从别的模块哪里学习到的和自己过去的一些经验,仅做一定的总结。欢迎补充和讨论。

模块自身的素质要求

要写出一个优秀的模块,模块自身的素质十分重要,如果自身条件太差,成为优秀模块的概率是极小的。这些自身素质包括美好的愿景、专注的定位、名字、API设计、文档、测试等。

合理的愿景:设定既定目标

如果你打算写作一个模块,并贡献到开源社区中,并期望它是优异的,并被许多人使用的,那么为它设定一个既定目标是首要的。如果这个目标是没有意义,没有趣味的,那多半没有人使用,甚至自己开发到一半都没有兴趣继续写下去。没有理想的屌丝,注定不能成为高富帅。
对 于具备众多坑爹问题的JavaScript语言而言,找到一个目标并非难事。典型的例子如:jQuery专注解决DOM操作和Ajax、 Underscore专注对象和集合的操作、QUnit和Jasmine专注BDD和TDD的单元测试、moment模块专注解决从Java那里学过来的 Date的问题;拿近一些的例子,玉伯的SeaJS专注模块加载,老赵的Wind.js专注异步编程同步化来解决流程控制问题;拿一个有趣的例 子,PNGDrivehttps://github.com/MadeInHaus/PNGDrive这个项目虽然没有什么实际用处,但是将文件编码为图片显示出来的方式足够有趣。
另 外这个目标必须是既定的。也就意味着饼不用画为无限的,这个目标一定是可以完成的。如果目标太大,也就意味着模块自身会变复杂。jQuery兼容各种浏览 器的DOM操作这个目标,在移动平台上变得没有意义,所以存在着Zepto.js这样的项目。更小的目标意味着模块自身简洁,且能够更专注,目标更容易抵 达。一旦抵达目标,该模块就是稳定的,未来被替换的机会极小。

为模块或项目起一个贴切的名字

模块需要一个贴切而好记的名字。这个名字何以帮助用户最直观地感受模块。SeaJS在起名上算是一个表率,让人很容易有海纳百川的联想,这也正是SeaJS的行为。一个好的名字可以使得模块的后期推广事半功倍,而且一旦开始推广,尽量不要换名字。

不做逾越的事

并非每个使用者都喜欢买一送一的感觉,因为后面这个一,对于使用者而言,并非是期望的,所以它的优良无法直观的判定好坏。无关的方法一定不要提供。评判一个模块是否完美,不是可以添加API,而是无法再减少API了。

不污染公共环境

每 个人都不喜欢公共环境被人污染。破窗效应揭示,如果一辆汽车的门窗稍有损坏,不立即修复,那么很快整辆车就会被破坏甚至偷走。 在JavaScript,公共环境包括全局变量,原型链等。jQuery和Underscore为了代码的写作方便,占用了$和_两个符号,尽管他们都提 供了noConflict方法来避免冲突,但是抱怨者还是大有人在。所幸这两个库太过于知名,几乎没有再有的库来使用这两个变量名。另一个例子是 Prototype.js库对对象进行扩展时,直接在Array、Object等原生对象的原型链上添加方法,尽管它看起来不影响,但是总有冲突的一天, 并且使用者并不一定知晓原型链被改动,在他的默认上下文中,难保他也不去修改原型链。相比Prototype.js的做法,Underscore提供的方 法则优雅许多,另行提供API来处理操作,而不是修改共有的原型链。
在Node和浏览器中,global和window是全局对象,如果随意放置变量到全局对象上,也容易遭到他人修改或者覆盖你变量的事情。 CommonJS提供的require、exports则十分优雅解决这个问题。谁使用,谁引入。而不是通过全局变量。在前端没有AMD或者CommonJS环境下,则是采用命名空间和闭包来解决这个问题。
不污染公共环境是模块与模块之间互相不影响的基本保证。如果引入你的模块,导致其他模块失效的事情,多半是不招人待见的。

抵制墨菲效应

有 可能变糟糕的事情,它变糟糕的可能性就会变大。模块在升级,迭代的过程中,如何避免这种糟糕的事情发生呢?答案是单元测试。当单元测试覆盖了你认为会出问 题的地方,可以避免相同的错误再次发生。这是模块稳定迭代的基本保证。当我发现一个仅仅为了解决日期操作的moment模块,它为数不多的API竟然具有 多达7000+的断言时,十分惊讶。
过去JavaScript只做简单的事情,地位低下,所以对于JavaScript的质量保证也极少。这个思维需要改变,一个用户在评估采用你的模块时,如果单元测试都无法看到,心里该是有多不踏实。