日期:2014-05-16  浏览次数:20376 次

如果给JS代码发布正式使用前增加一个编译步骤,我们能做些什么.
最近看了Hedger Wang的"Coding Better Object-Oriented JavaScript with Closure Compiler"(中文),算是给D2预热.终于明白Google这工具为啥叫Compiler而不是Compressor.

相对于编译型语言,JavaScript缺少了编译这个环节.传统编译器把代码转换为可执行的机器指令的动作交由浏览器中的JS引擎在运行时执行.但现代的编译器除了代码翻译还有哪些功能?而JS引擎能在运行时Cover住这些任务么?

最常见的运行时编译无法解决的问题就是代码压缩.所以我们有各种Compressor让代码传输给浏览器的时是最小的.
而在这篇文章中Google Closure Complier告诉我们还可以在编译时统一OO风格,减小对象深度以提高访问速度,使用枚举,常量等等其他语言好用的特性.

但看了文章之后我还是没有马上使用GCC这些高级功能的打算.
预编译时只能做这么多么?
我觉得Google Closure Complier更有意义的是告诉我们,可以有这样一个发布前预编译的步骤,可以有这样的一个时间窗口,在这个时候结合我们自己的应用,结合我们自己遇到的问题是不是也可以做点什么.

最近我在做kissyLite,前一篇文章kissyLite的包管理和无需预先注册的带依赖关系模块异步加载,讲的是用较少的代码为JS引入具有良好扩展性的模块化.就这个应用场景,我们可以在预编译的环节做点什么.

场景一

mod开发者开发一个功能,将它封装在mod-a中.
然后我们发现mod-a中可以抽象出来mod-b,让mod-b其他地方也能使用到.
于是就有了mod-b,同时mod-a requires mod-b.

mod使用者的代码始终是KSLITE.use('mod-a',function(){});

由于原有mod-a被拆分,就需要串行的加载mod-a,mod-b.
因为以kissyLite的模块化模式,在mod-a加载进来之前是不清楚它require mod-b的.

为了颗粒化可重用,造成了性能的损失 -- 多了一个请求,而且是串行的.

通过预编译能解决这个问题么?可以.

模块使用者,使用Debug(带预编译相关功能)版本的kissylite,控制台就可以给出两个优化建议:
"为了让子模块能够平行加载,你应该修改此处代码为KSLITE.use('mod-a,mod-b');!"
可见至少我们解决了平行加载的问题.
而且我们可以在开发的时候完全不去考虑这个问题,当所有功能做好之后再来优化,这其实就是预编译时优化功能.

场景二

mod开发者还是发现mod-a中可以抽象出来mod-b,让mod-b其他地方也能使用到.
这次他非常清楚过细的颗粒化会引入更多的请求和串行加载等性能问题.
所以他在纠结到底要不要为了可能的重用单独先提出来mod-b.

通过预编译能解决这个问题么?如果预编译模块能结合自动化测试和自动构建,可以
首先回答:要将mod-b拆出来!
当我们把当前应用所有重要的测试用例都跑过一遍之后,发现当前的所有应用场景mod-b都是跟着mod-a进来的.
那么预编译可以给出优化建议
"为了减少请求数,可以将mod-b的内容追加到mod-a的后面".
因为mod-a必须要叫上mod-b,而当前mod-b还没有被单独使用,不如合并.
甚至可以结合自动构建工具,给出最适合当前应用的kissy版本kissy4U.
"为了减少请求数,这次构建已经吧mod-b的内容追加到mod-a的后面:)".
基准的kissy版本,mod-a和mod-b每个都是独立的.
而在为你应用构建的kissy4U中,mod-b在mod-a文件中.
不用纠结提出mod-b引入的性能问题了,写最优雅的代码吧.

场景三
还是mod-b要不要跟着mod-a一同输出.
现在问题又变化了,mod-b可能被单独使用,也可能被mod-a调用.
还要不要合并mod-a mod-b 到mod-a.js呢?
到底是先use mod-b 再 use mod-a的情况多呢?(这样显然不要合并文件)
还是直接就use-a的情况多呢?(这样还是合并了好点)

通过预编译能解决这个问题么?如果预编译模块能结合线上实际使用情况的统计反馈数据,可以.
我们参见Facebook的静态网页资源的管理和优化
我们有了抽样统计数据的反馈,就可以在预编译阶段根据反馈来决定要不要合并mod-a和mod-b.

场景四
add时到底要不要attach?
add的重要功能是注册一个模块,当这个模块不依赖其他模块或所依赖的内容都可用.那就具备attach的条件,要不要attach呢?
如果attach了,use的时候更快.如果不attach,页面加载更快.

回答问题:add的时候不要attach.
默认attach引入的代码运行消耗是不必要的.微软还有一个专门的工具Doloto来拆分加载时必要的代码和不必要的.

但这样做use的时候可能就会慢了.尤其use还可能涉及异步模块加载...
跟随微软的思路我们很容易想到办法,在domready之后,浏览器不忙的时候把可能用到的模块预先attach.
我称这个动作为预热.那该预热哪些?页面上有几十个功能都预热么?

通过预编译能解决这个问题么?依然可以.
在页面开发完之后把将会常用的功能使用一遍.然后编译器就可以给出提示.
"应该在domready的之后use('a,b,c,d'),进行预先attach".
即domready前按需加载,domready后选择性预热.

预编译打开一扇窗
可以看到针对kissyLite这个具体的应用:
        利用好这个环节,模块开发者的模块的划分,不必再因为性能问题而纠结.
        利用好这个环节,应用开发者可以配置出最优的代码打包方案,如Facebook做的那样.
        利用好这个环节,模块化架构可以不必纠结add时attach影响页面速度,不attach又拖慢功能使用.
但这不重要,可能没有一条适合你.

我们看到Hedger Wang在文章开头先抱怨了一通,然后使用GCC解决了这些问题.
而这里给出的几个场景完全不同,又何其相似.
我们也抱怨一通,然后经GCC启发,在其引入的预编译阶段也解决了这些问题.

每个应用都会遇到自己独特的问题被抱怨,也能在预编译这个时间窗口解决一些什么么?