18 February 2014

JS模块化开发与自动化部署分两部分:上半部分讲述模块化开发,特别是使用模块化开发之后会碰到的问题。下半部分集中讨论JS代码的自动化部署。是否使用模块化开发以及模块化方案选择将直接影响JS代码的部署过程。


先列举一下JS模块化开发的坏处:

  1. 对源代码进行侵入式的更改,与非模块化的代码不能直接交互
  2. 增加合并环节的复杂度,按照模块化开发的小文件不能直接合并到一个大文件中
  3. 模块加载器本身也会带来开销,在加载其它其它代码之前要先加载模块加载器。在浏览器端进行路径/依赖计算也有开销,特别是模块数量庞大且依赖关系复杂的时候。

好处:

  1. 规避了命名冲突问题,模块都运行在匿名空间,不会污染window。
  2. 自动/半自动地解决了依赖管理的问题。半自动是因为一些模块化实现还需要手动声明依赖。
  3. 方便调试。

适用场景:代码库文件数量庞大,与外部代码库交互较少,代码复用频繁,多人开发。不符合这些情形的,使用模块化开发很可能得不偿失。

JavaScript的模块化解决方案比较流行的有AMD和CMD标准,requirejs和seajs分别是实现这两种标准的模块加载器。requirejs目标是能加载各种环境中的JavaScript,无论浏览器端还是服务端;seajs的追求是"A Module Loader for the Web",相当于requirejs针对浏览器环境下的简化版。

下面讨论针对上面的坏处,requirejs和seajs各自提供的解决方案。

1. 侵入式和与非模块化代码的兼容问题

JavaScript语法本身不提供模块化功能,模块加载器都是用DOM script标签加载和自动执行脚本原理来实现加载。但是为了解决依赖问题,AMD和CMD都提供了一套API和规范来实现依赖管理,保证模块代码的执行顺序。这些API和规范的特性是:如果要符合规范,就必须使用它的API,一旦使用了它的API,你的模块就不能脱离它的模块加载器而独立运行。凡是按照规范使用它的API开发的模块,互相之间都能正常工作。凡是不符合的,都不能保证。

现实情况是:大部分的第三方库/框架都在AMD/CMD规范提出之前就已经开始存在了!也就是说,即便你在自己新项目里面使用AMD/CMD标准组织你的代码树,但是只要碰到需要与旧项目或第三方代码交互时,你都会面临这个问题。

还好最流行的开源框架jQuery的代码已经支持AMD标准了。对于其他不支持的第三方库,要么在浏览器端使用requirejs/seajs的shim功能进行手动配置,要么把第三方库用transport工具/手动转成符合AMD/CMD标准的模块之后再使用。

2. 模块合并问题

部署上线时小模块要不要合并成一个大文件,是一个问题。合并的好处在于减少http请求数目。不合并(但仍然要压缩)的好处是可以充分利用浏览器的缓存,100个模块里面只要有1个发生变化,合并后的大文件就要重新请求。经验是:不经常发生改动的模块,比如公共库,把它们合并在一起,充分利用缓存;经常被迭代的业务模块也把它们合并在一起,以便减少HTTP请求数。

最初,JS代码合并只是文件内容的拼接问题,只要注意先后顺序就行了。在AMD/CMD标准之前,就有过模块化方案:把对外暴露的接口挂在一个命名空间里面,命名空间组织方式与目录结构保存一致,在源文件的前面几行通过注释的方式声明依赖。这种方案估计是写Java代码的人搞出来的。这种模块化方案在进行部署时,只要从入口模块开始跟踪依赖关系,在进行拓扑排序之后就可以直接合并多个小模块。

但是在采用AMD/CMD方案之后,模块合并就没这么简单了:你必须使用它提供的构建工具来进行合并!为什么呢?

因为合并之后的大文件中的各个小模块都必须符合define(moduleId, depArr, factory)格式才能被浏览器端的requirejs/seajs正确处理。

其实不使用官方的构建工具也行,只要我们自己动手搞定这些问题:给所有的匿名模块的define函数添加ID作为第一个参数,提取依赖列表并添加到define函数的第二个参数位(如果没有手动声明依赖数组)。上面的两步操作都需要使用JavaScript语法树工具/复杂的正则表达式来协助完成,然后才能进行模块的合并。(如果不删除模块的define调用,就不需要在合并时进行排序,define的调用顺序无关紧要,浏览器端的模块加载器会保证factory代码执行顺序)

总之,要么学习构建工具的API!要么自己费精力去做上面的事情。反正都不简单。

--------分割线-------- 未完待续...



blog comments powered by Disqus