更新: 本文原本的标题是“为何我们弃用AngularJS:……”,现在把它去掉了。因为这些痛点主要是针对单页JS应用框架的。有些人认为本文是专门批判AngularJS的,这可不是我的本意。-- Quinn
几个月前我们的Sourcegraph网站向公众开放,它是一个富AngularJS应用。服务器传输原始的HTML页面和JSON端点,剩下的就交给Angular来处理。这是一个创建Sourcegraph的简易方式,当时我们不知道Sourcegraph会变成什么样。
但是单页JavaScript框架并不适用于每一个站点。Sourcegraph是一个内容为主的站点,我们渐渐发现富js应用弊大于利。富js应用的好处众所周知,下面是我们体会到的一些意料之外的困难。希望对面临类似选择的开发人员能有一些帮助。
客户端JS框架的5个痛点
我们早知道会面临很多的困难,但是不知道会有这么难。
1. 糟糕的搜索排名和Twitter/Facebook预览
搜索引擎爬虫和社交网站的预览抓取器不能加载纯Javascript站点,提供替代版本又慢又复杂。
有两种方式可以允许爬虫阅读你的站点。你可以在服务器端运行一个浏览器实例来执行你的应用里的Javascript,然后从DOM中卸下HTML(使用PlantomJS或者WebLoop)。或者你可以创建一个服务端生成的专供爬虫的替代性HTML版本。
第一个方法需要你为每一个页面加载建立一个headless浏览器(或者tab),比起直接产出HTML,这样会花费很多的时间和系统资源。取决于你使用的框架,需要不少精力来决定什么时候页面已经准备好了。 你可以缓存页面,但是如果页面经常改变,那么缓存只能起到非常有限的优化作用,而且会增大复杂度。这个方法会将你的页面加载速度拖慢好几秒,对搜索引擎排名也不利。
第二个方法(创建一个替代性的服务器端站点)对简单站点而言足够了,但是如果页面很多,这将是一个噩梦。况且如果Google认为你的服务器版本站点跟你的主站版本有很大的不同,那他就会狠狠的惩罚你。糟糕的是,直到你的访问量直线下降的时候你才会意识到你已经过界了。
2. 不可靠的统计和监控
很多分析工具需要使用易于出错、手工集成的HTML5 history API(pushState)来导航。这是因为它们无法自动检测到你的应用使用pushState导航到了新的页面。即使可以做到,它们仍然需要等待你应用的信号来收集新页面的其他信息(例如页面标题和其他页面特定的指标)。
你如何解决这个问题?同时取决于你的客户端路由库和你集成的分析工具。用Google分析和Backbone.js?尝试一下backbone.analytics。用Heap(顺便说一下,Heap很棒)和UI-Router?设置你自己的$stateChangeSuccess
钩子然后调用heap.track
还没完!你想追踪起始页面加载?也许你重复跟踪了?你会跟踪失败的页面加载吗?如果你使用replaceState代替pushState呢?即使要获知你是否错误地配置了分析钩子——或者是否依赖升级搞乱了系统——也是相当困难的,除非交叉检查分析。当你发现问题后,很难去恢复你错过的分析数据(或者消除重复数据)。
3. 缓慢、复杂的构建工具
前端JavaScript构建工具,例如Grunt,需要复杂的配置而且会很慢。还好我们有像ng-boilerplate这样出色的项目来帮忙,但是它们很慢。并且如果你想添加一个自定义的步骤的话你还是无法避免复杂性。(我为什么说Grunt复杂,看看这个配置文件就知道了。)
一旦你配置好了你的应用,包括Gruntfiles等等。你仍然要忍受漫长的JavaScript构建时间。你可以把dev和production构建通道分开来提高开发速度,但是你终将深受其苦。用AngularJS尤其如此,他需要在压缩代码前使用ngmin(如果你用了特定功能)。事实上,我们有几次就是因为这些压缩的JavaScript和开发时的代码表现不同而把SourceGraph搞砸了。
事情正在改善,Gulp是一个巨大的提升。
4. 慢,不可靠的测试
测试JavaScript-only的站点需要使用基于浏览器的测试框架,比如Selenium,PhantomJS,或者WebLoop。安装这些(除了PhantomJS)通常意味着安装WebKit和Java依赖,配置Xvfb(虽然新版的PhantomJS移除了这些依赖),也许运行一个本地的VNC客户端和服务器来测试。最后,你还需要在持续集成服务器上配置这些东西。
相反,测试服务器端生成的页面通常只需要类库来获取URL和解析HTML,安装和配置要简单许多。
一旦你开始编写浏览器测试,你必须处理异步加载。你不能在页面还没有加载的时候就测试页面上的元素,但是如果在一个特定时间段里没有加载,你的测试就会失败。浏览器测试类库提供了一些帮助函数来处理这种情况,但是对于复杂页面它们只能帮上一点小忙。
你想组合很重的浏览器测试工具(Selenium,加上Firefox或者Webkit)和很大的测试复杂度(由于浏览器测试的异步本性)?你的测试需要很多配置,很长的时间来运行,而且很不可靠。
5. 被掩盖的未根除的缓慢
在富JavaScript应用中,页面转换几乎是瞬间发生,然后所有的特定元素异步加载。服务器端应用恰恰相反:页面在服务器端加载完成前不会发送到客户端。
听起来似乎是客户端应用胜利了,但是也许这不过是一个伪装的诅咒。
考虑客户端JS应用,当用户点击一个链接,页面会立刻加载并呈现。如果用户导航到一个侧边栏需要5秒钟才可以加载的页面,第一眼感觉很快,但是如果用户需要的信息在侧边栏里,对用户来说就太慢了。即使你需要的特定内容能立即加载,你仍需要忍受转动的加载指示器和页面填充时的抖动。
现在考虑一下这样的情况:如果开发人员想在那个页面添加新功能。很难确定这个功能是否必须快速加载——因为一切都是异步的,所以谁会在意页面底部过了几秒才加载呢?如此反复几次,整个站点就会让人觉察到迟缓和抖动。
在服务器端应用中,如果一个API调用很慢,整个页面就会阻塞直到页面完成。服务器端的缓慢不可能被忽视,因为这很容易被测量,并且会公平地影响每一个人。但是在客户端应用中这很容易被忽略。
你可以争论说,一个好的开发团队应该避免这些错误,并且客户端 JS 框架不是罪魁祸首。这是对的,但是总体上来说,客户端JS框架降低了迟缓的开销。这一点触动了开发团队的激励机制。
接下来怎么办?
上面说的问题,本身都不算大问题。我们可以做很多工作来减轻上述情况(事实上我们确实做了很多)。但是,这些问题加在一起就是另一回事了,可以说,客户端JS框架成为了我们开发工作的一大负担。
同时要牢记,每一个站点都是不同的。例如,Sourcegraph是一个内容站点,这意味着页面在加载后不会有太多的变化(和富应用相比)。我们依然喜爱这些技术,但是它们不是构建我们的主站的合适工具。