现代JavaScript并不一定意味着客户端MVC单页Apps。
在Basecamp,我们写了许多JavaScript,但是我们并没有用它来创造当代意义上的“JavaScript应用”。我们的所有应用在核心部分都有服务端渲染的HTML,然后添加少量JavaScript来让页面变得炫酷。
这是一种构建庞大的一体化应用的方式。Basecamp运行在多个平台上,包括原生移动Apps,只有单独一套使用Ruby on Rails创建的controllers、views和models。拥有一套唯一的共享的可以在一个地方更新的接口,是一个小型团队能够支撑多个平台的关键。
这使得我们能够像过去一样凝聚生产力。这可以让我们回到过去那种高生产力的时期,那时一个程序员无需陷入层层间接的或分布式的系统,就可以完成大量任务进度。那时候,还不是每个人都认为,服务端就是给客户端JS喂JSON的。
这并不是说,大型JS应用没有价值。这只是在说,作为一种对很多应用程序都通用的方案,特别是那些像Basecamp的应用,这是一种简单性和生产力的整体倒退。
而且这也不是说,JavaScript单页应用的发展没有带来切实的好处。其中最主要的好处是界面从全页刷新中解放出来,变得更快、更流畅。
我们过去也想让Basecamp变得那样。因此,我们曾跟随大众,用客户端渲染重写了所有东西,或者在移动端采用纯原生的实现。
我们因为这种需求而创建了一种双重方案:Turbolinks和Stimulus。
Turbolinks高屋建瓴,Stimulus贴近细节
在我介绍新的JavaScript框架——Stimulus之前,让我先大致介绍一下Turbolinks的细节。
Turbolinks是从一种叫做pjax的方案简化而来,托管在GitHub上。基本的概念保持相同。全页刷新通常感觉起来慢的主要原因并不是因为浏览器需要处理服务器发送的一堆HTML。浏览器都非常棒,处理这种事情都非常快。在大多数情况下,HTML负载比JSON负载大,对于页面响应速度也并没有太大的影响(特别是启用gzip压缩的情况下)。全页刷新感觉慢的主要原因是CSS和JavaScript必须重新初始化并重新应用到页面。无论这些文件本身是否被缓存。如果你有相当多数量的CSS和JavaScript,全页刷新就会非常慢。
为了绕过这种重新初始化,Turbolinks维护了一种持久化过程,就像单页应用做的那样。但很大程度上看不出来。它拦截了链接,然后通过Ajax加载新页面。服务器仍然返回完整格式的HTML文档。
这种策略可以让大多数应用中的大部分动作感觉起来非常快(如果服务器能在100-200ms时间内返回响应,而这可能非常需要使用缓存)。对于Basecamp,这样使得页面到页面的转换速度加快了3倍左右。这使得应用程序感觉起来响应灵敏而流畅,而这也是单页应用最吸引人的部分。
但是Turbolinks只是故事的一半。粗粒度的那一半。在全页改变的级别之下,存在一个单页的所有细粒度的行为。包括显示和隐藏元素、将内容复制到粘贴板、在列表中增加一个新的todo以及所有其它我们与一个现代Web应用发生的交互行为。
在Stimulus之前,Basecamp使用了几种不同风格和模式来实现这些零碎的功能。一些代码只是一撮jQuery,一些代码也是差不多大小的普通JavaScript,而一些代码又是比较大型的面向对象的子系统。它们通常使用data-behavior属性来避免显式事件处理。
尽管像这样通过新增代码来解决问题非常容易,但这不是一个全面的解决方案,而且我们混杂了太多内部风格和模式。这使得复用代码非常困难,并且这使得新开发者很难学习一种一致的方案。
Stimulus的三个核心概念
Stimulus将那些模式中最好的部分糅合到一个适度的小型框架中,只围绕三个主要概念:Controllers、actions和targets。
当你查看它所展示的HTML时,它看起来是一种渐进的增强。因此,你可以查看一个template,然后就知道哪些行为发生在它上面。这里是一个例子:
<div data-controller="clipboard">
PIN: <input data-target="clipboard.source" type="text" value="1234" readonly>
<button data-action="clipboard#copy">Copy to Clipboard</button>
</div>
你可以看到这些内容,然后对会发生什么事情有一个比较好的概念。即使完全不知道任何关于Stimulus的东西或者查看controller本身的代码。这很像伪代码。这和阅读一份HTML片段,然后这段HTML中有一个外链的JavaScript文件将事件处理器应用到其中元素的情况有很大不同。这还维护了关注分离,而这点在许多现代JavaScript框架中已经遗失了。
正如你所看到的,Stimulus并不纠结于创建HTML。相反,它将自身绑定到一个已存在的HTML文档上。在大多数情况下,HTML是在服务器端渲染的,无论是在页面加载的时候(首次点击或者通过Turbolinks)或者是通过一个Ajax请求改变DOM的时候。
Stimulus关注的是对这种已有的HTML进行操作。有时这意味着增加一个CSS class来隐藏一个元素,或者使它有动画效果,或者让它高亮显示。有时这意味着在组里对元素重新布置。有时这意味着操作一个元素的内容,例如我们将UTC时间转变成可以显示的当地时间。
有一些场景下,你会想要Stimulus创建新的DOM元素,那么你完全可以随意那么做。将来,我们甚至会增加一些语法糖来让创建DOM元素这件事更简单。但是,这只是比较罕见的应用场景。重点是操纵元素,而不是创建元素。
Stimulus和主流JavaScript框架相比有哪些不同
重点是操纵元素,而不是创建元素。这一点让Stimulus和大部分现代JavaScript框架有很大不同。几乎所有的现代JavaScript框架都聚焦于通过某种形式的模板语言将JSON转变成DOM元素。许多人使用这些框架来创建一个空的页面,然后毫无例外地使用那些通过JSON-到-模板的渲染方式产生的元素来填充这个空页面。
Stimulus在状态管理问题上也有很大不同。大部分框架有通过JavaScript对象保持状态的方法,然后基于保持的状态渲染HTML。Stimulus正好相反。状态保存在HTML中,因此controllers可以在页面改变时丢弃,但是当缓存的HTML再出现的时候又可以重新初始化为原来的样子。
这是一种非常不同的模式。一种我确定许多已经习惯了用现代框架工作的经验丰富的JavaScript开发者会嘲笑的模式。随便吧,嘲笑就嘲笑吧。如果你享受复杂框架,例如React + Redux的复杂性和维护它们所需要投入的努力,那么你可能不会对Turbolinks+Stimulus感兴趣。
相反,如果你对正在使用的这些现代技术所带来的强烈复杂性和应用程序分离性感到非常不满,那么你应该会在我们的方案中找到解决之道。
Stimulus及相关的想法提炼于他山之石
在Basecamp,我们已经在多个不同版本的Basecamp和其它应用程序中使用这种架构很多年了。GitHub使用了一种类似的方案,起到了非常好的效果。关于“现代”Web应用程序看起来应该是什么样子,这不仅是相对于主流理解的另一种合理可选方案,还是一种非常有吸引力的方案。
事实上,这感觉和我们在Basecamp开发Ruby on Rails时非常相似。同一种秘方,同一种味道。感觉现代主流方案有一些不必要的复杂性,而我们可以用更少的投入更快地完成更多的事情。
另外,你甚至不一定非要进行选择。Stimulus和Turbolinks和其它比较重的方案可以很好的结合。如果你的应用80%都不需要那么复杂的架构,可以考虑使用我们的方案。然后在剩余的部分使用那些复杂的框架,从而可以真正地从中受益。
在Basecamp,我们一直都在场景需要的时候使用一些比较重的方案。我们的日历采用客户端渲染。我们的编辑器采用Trix,一款完全生成的文本处理器,完全不用作为一组Stimulus controllers。
这组框架旨在尽可能避免过于复杂的处理。使大部分处理保持请求-响应模式,大部分交互使用简单的模型就可以生效。然后在特别复杂的场景下使用那些代价昂贵的工具。
总之,如果你的团队规模较小,但又想要和那些使用比较费力的主流方案的较大团队竞争,那么这是一个比较适合的工具箱。
试试吧。
Stimulus是我在为期两周的深入研究过去一年中我采用的JavaScript的当前状态后想到的。我深挖了我们的Basecamp 3 代码库中的模式,提取出一个原型,然后使用最新的最伟大的JavaScript技术来实现它。这个深入研究最初产生了针对Rails的Webpacker,而如果没有Sam Stephenson和Javan Makhmali的杰出工作,就不会有这个框架。他们根据我粗糙的Stimulus原型,用TypeScript进行了重写。正如他们重写Turbolinks 5时一样。这是一段美丽的代码。
关于作者
DHH是Ruby on Rails的创作者、Basecamp(原名37signals)的创始人兼CTO、纽约时报畅销书《REWORK and REMOTE》的作者以及勒芒级别获奖赛车手。
查看英文原文:Stimulus 1.0: A modest JavaScript framework for the HTML you already have
转自 http://www.infoq.com/cn/articles/stimulus-modest-javascript-framework