今日洞见
文章作者、部分图片来自ThoughtWorks:陈计节。
本文所有内容,包括文字、图片和音视频资料,版权均属ThoughtWorks公司所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。已经本网协议授权的媒体、网站,在使用时必须注明"内容来源:ThoughtWorks洞见",并指定原文链接,违者本网将依法追究责任。
当AngularJS应用程序变大时,很多问题就开始显现出来了,比如多层级视图的加载问题。如果在子视图显示之前没有预加载,则可能在需要展示时发生视觉闪烁的情况。这种问题在网络缓慢,或者服务器使用较慢的https连接时更容易出现。
本文将讨论更高效加载AngularJS视图的系统方法。
AngularJS 视图一般原理
AngularJS视图也并不是什么特别神奇的技术,在其内部就是按普通的directive来处理的。也就是说,当一个位置需要显示view时,AngularJS会尝试使用某种方法获得其HTML模板文件的具体内容,包装成directive,执行directive的标准流程,最后添加到页面上。
回想一下,directive本身是不是正好也支持templateUrl属性?这就与view技术衔接上了。
这样说来,是不是视图模板也可以使用行内DOM甚至是字符串字面量值了呢?答案是肯定的!我们本来就可以使用一段行内DOM来作为view的模板。例如:
当然,作为一个大型的AngularJS应用程序,将所有view都放在字符串值里,或者行内DOM里是不太现实的,我们希望可以使用多个小的HTML文件来作为子模板。这样,虽然整个应用很大,但每个子模板的文件并不大,一般都是几KB的小文件,当用户点击到指定位置,需要时使用对应界面的模板时再去加载,也就显著提高了效率。
我们可以用下图来表示“行内DOM”与“多个子模板文件”的性能对比:
AngularJS 对视图加载的优化
上面提到了“多个子模板文件”的模板组织方式,这本是一件很平常、很自然的工作方式而已。也正是因此,才让人们感觉AngularJS工作方式与自己的期望的一致:因为在没有使用AngularJS之前,人们在开发一个 Web应用时,页面就是这样一个个组织的。
即使在以前,我们在提到性能的时候,自然会想到“缓存”。在以前,页面与页面之间的跳转使得每个页面都是相互独立的单位,因此页面内容的缓存只能有赖于浏览器了。而今,AngularJS让所有页面子模板都在“单页应用”中加载,于是,我们在这个单页面应用内便获得了缓存页面内容的机会。
AngularJS中内建了缓存机制templateCache:只要已经加载过某个页面子模板,就会在templateCahce中缓存起来,下次从服务器加载页面模板之前,先检查templateCache,如果已有缓存则不需要从服务器上加载,直接使用。
AngularJS中内建了templateCache 机制之后,加载视图的过程变得高效而轻松,Web应用本身,以及开发者都不需要关心这一过程。不过,即使有页面内的templateCache,页面模板在初次使用时还是需要从服务器加载,因此偶尔能见到一些视觉闪烁的情况,比如标签切换、页面跳转等。
对AngularJS templateCache的优化
作为一种优化手段,我们很自然能想到,既然页面模板文件加载过一次之后,再次加载时直接从 templateCache 读取就能提高性能。如果在应用启动之初templateCache中就有了所有页面的缓存,也就根本不需要服务器了,那么在页面需要显示时,也就基本不需要加载时间了。图可以变成这样:
要实现这一目标,只需要在发布应用之前,构建额外的templates.js文件,在其中将所有的页面模板读取出来并提前put到templateCache中,再将形成的templates.js嵌入到应用中,即可在Web应用启动时就拥有所有页面模板内容的缓存版本了。
不过,对于大型AngularJS Web应用来说,我们很快发现一个问题:这个templates.js文件本身的体积迅速大了起来,又会引发新的性能问题。
为此,我们可以使用另一个已有的经验:“异步加载”。有了异步加载的支持,在加载templates.js的请求还没有完成之前,可以“降级”使用AngularJS内建的机制,而一旦templates.js加载完成,就立即拥有了所有模板的缓存。
理想中,templateCache最好能达到最佳的性能表现,但实际应用中,如果不加优化,templates.js文件本身的体积会令这种优化打折扣,而加上异步加载 templates.js和降级到逐个加载单个htm模板文件之后,又有了一些改善。
浏览器缓存
现在再来讨论一下浏览器缓存,可以结合上一节的templates.js一起来讨论了。浏览器缓存是浏览器里内置的一种缓存功能,当服务器正确配置了对htm和js文件的缓存支持时,浏览器将按指示缓存这些文件。无论是一个个htm模板,还是templates.js,都可能被缓存。
也就是说,只要在服务器上正确配置,那么上一节所述的“异步templates.js”,以及“降级的多个htm模板文件”都可以被浏览器缓存。这样,我们将加载htm模板文件和templates.js的需求都减少到第一次使用应用之时。
但在服务器上配置缓存也需要谨慎,如果配置不当,就会出现当服务器上文件已经更新,但客户端浏览器仍在使用老的缓存版本的问题。由于AngularJS应用使用绑定表达式显示界面,因此如果程序已经更新,而视图还是老版本,那么绑定表达式很可能失效。这种情况下,轻则局部界面错乱,重则整个Web应用完全无法使用。
浏览器缓存原本是一个“杀手锏”,不管是只使用单个模板文件,还是使用templateCache,浏览器缓存都可以极大地改善其性能效果。但一旦缓存配置不当致使客户端浏览器里使用了错误的版本,就直接导致应用错误,更不谈性能表现了。
要处理缓存问题也有成熟的经验可供借鉴:也就是在文件名上使用版本号,每次需要更新文件内容时,同时更改版本号,那么整个文件名也就发生变化,也就不会发生缓存版本错误问题。结合上面的论述,我们在templates.js上添加上版本号,另一方面配置AngularJS,在加载单个htm模板文件时,也会在请求上附上版本号,即可解决这一问题。当然,我们希望在开发时,标记要使用的视图模板时,不需要指定这个需要经常变化的版本号,从而最大程度地保障开发体验,并将维护成本降到最低。
总结
上面讨论了AngularJS视图各种可能的方式,分别实施的方法,以及其性能表现差异。主要值得关注的是经优化的templateCache机制,以及结合浏览器缓存的templateCache方法。总结来说,可以形成这样一个更直观的图形:
经过一番努力,最终我们能够达到这样的结果:
- 在应用里添加仅在生产环境才生效的策略:支持在加载视图模板文件时在文件名中添加版本号(从页面中templates.js的文件路径中分析版本号);
- 开发时不需要经过改变;
- 发布时预读取所有模板的内容,并生成带版本号的templates.js,嵌入应用页面中;
- 在服务器上配置所有htm模板文件及templates.js的缓存策略为“允许缓存”;
- 用户首次使用应用时,集中所有网络带宽加载AngularJS基础脚;本,以及应用程序业务逻辑系统,令应用程序尽早能够使用;此时应用使用htm模板文件作为视图模板;
- 异步加载templates.js;加载完成之后应用开始使用页面内模板缓存;
- 用户再次使用应用时,从浏览器缓存中加载templates.js;
- 再次发布应用时,修改templates.js 文件名中的版本号,嵌入页面中。
所以,在首次用户使用应用时,其网络加载图形就像这样:
最先加载的是应用程序AngularJS框架本身,以及业务逻辑,这时候应用已经可用;此时再异步去加载templates.js文件。事实上,上面的图形即是我们实际项目中的状况,具体实现在这里就不贴了,也欢迎读者一起探讨更多的可能性。
从本文的讨论中不难看出,只要通过各种方法,好好管理浏览器的加载行为,形成一个系统方法,便能令视图加载的性能表现变得更好。