oak

在2016年选择Ember,而不是React

oak · 2016-12-23翻译 · 1388阅读 原文链接 十年踪迹审校

一个月前, 我们启动了一个新的产品: Instant 2FA,要想不到一个小时就给一个网站添加双因子认证(two-factor authentication),这是最简便的方式。

目前,不论是通过 Google Authenticator,还是通过使用类似 Authy 这样的一个API服务,集成一次标准的双因子认证通常都需要花费数周的时间。

借助 Instant 2FA ,我们所构建的是一种将整个集成过程减少为3次API调用,并且提供所有开箱即用内容的解决方案: Google Authenticator,SMS,以及 Yubikey支持;记住当前电脑;速度限制与警报;借助我们第一批的少量客户,我们在不到一个小时的时间就完成了开发环境的整合。

这篇博客概括论述了我们为什么选择Ember(一项我们都未曾使用过的技术),而不是React(我们在Clef的实际生产项目中使用了2.5年),以及经过一个月的工作之后,我们对这个决定的看法。

我们的经验

每个新的项目都要面对无数的技术选型。幸运的是,构建Clef(一个被近数百万网站采用的双因子验证产品)这个项目的四年企业经验,使得我们大多数的技术选型意见都达成了一致。我们知道,我们的后端服务会使用Python,SQL以及Flask来进行构建,利用我们多年来一直采用的所有工具和库。

然而,前端的故事却完全不同:尽管我们在生产中使用了两年半的 React 和 flux (由 reflux 支持),但是当我们开始构思如何构建 Instant 2FA 的时候,我们却在考虑是否要使用一个全新的特色框架,比如, Ember 或者 Angular 会不会是一个更好的选择。

评测ember

当我们在考虑使用 Ember 取代 React 的时候,驱动我们好奇心的主要有两个假设:

  1. 约定先于配置(Convention over configuration)。鉴于Ember可以处理了很多开箱即用的事情,因此我们可以实现更多,更快的效果,而无需再疲于选择,而这些事情React需要整合其他库才可以实现。

  2. 是否是渐进式增强(Progressive enhancement),以及能否在技术成熟度曲线中存活下来。鉴于 Ember 拥有着强大的社区支持,社区致力于将 Ember 打造成一个构建现代网络应用的最佳方式,所以即使我们的应用稍稍落后于创新曲线(也可能处于生产的高原之上),我们依然能够使用最新且最棒的技术。

当我们深入了解了这两个假设之后,我们找到了极具说服力的理由,让我们最终选择Ember来构建我们的所有应用。

约定优先于配置

React非常棒的一点是,鉴于它仅仅是view层,因此它可以被轻易地添加到已有的JavaScript应用中。三年前,当Clef的前端在我们自己的面向对象的JavaScript框架之上构建时,我们开始探索React能否帮助我们,这一点就是React最棒的地方之一。我们并非完全转换到一个全新的框架之中,而是逐渐地使用React组件来替换不同的组件——只需要根据需要在React(例如使用redux来处理数据流)之上添加额外的逻辑层就可以了。

经过一段时间,这种逐渐库的结合固化成我们React生态系统中的一部分了。不同的库互相复制,但是功能可以实现。我们使用了:

  • coffeescript

  • React,用于view

  • reflux,用于数据流

  • react-router 2,用于路由

  • jQuery.ajax,用于网络请求

  • underscore,用于utilities

  • browserify,用于模块构建

  • gulp,用于处理任务

  • SASS,用于样式

  • 没有测试 😬

几个月前,我们做了个很小的副项目,在这个项目中,我们必须从杂乱无章中创建一个客户端应用。我们使用React来处理view层,但是由于相关生态环境在过去几年中的剧烈进化,我们最终形成了一个非常不同的组合:

  • ES6

  • React,处理view

  • reduxredux-saga,处理数据流

  • react-router 3,处理路由

  • fetch,处理网络请求

  • lodash,处理utilities

  • webpack,处理模块构建

  • post-css & css-modules,处理样式

  • karma, mocha, sinonchai,处理测试

挑选是一件很有意思的事情,但是我们前行中的每一步都在自我怀疑:在已有的可选库中我们是不是选错了呢?我们从React生态系统里挑出可能的工具配置我们的框架,但是却严重怀疑我们的配置到底是在帮助我们还是在伤害我们。

将高水平的架构技术决策托付给在这一领域拥有有限经验的工程师,通常会导致一些无关紧要的争执和糟糕的决策——这一点在我们所做的副项目中体现了出来。我们是构建Clef的四人团队,但是鉴于我们经常跨5个平台(IOS,Android,后端Python,客户端JavaScript,以及我们的的WordPress插件)维护代码,实际上只有两个工程师在真正地做React应用。作为一个团队,最终在做这个副项目的时候,我们陷入了一系列的被证明是不稳定的工具中不能自拔,而经常处于让我们的工作能正常运转的状态中。

使用Instant 2FA,每个工程师都将用他们的一大半时间来使用JavaScript,这意味着半数以上的代码书写者可能是对JavaScript比较生疏的工程师。这一结构——总的来说是高级的,但是对于前端工程来说相对陌生——使得约定先于配置变得尤为紧要。

当我们开始进行Instant 2FA项目的时候,Ember的这些想法对于我们做这些决策非常具有诱惑力。一些我们最不喜欢做的决定是下面这些:

测试

Ember内置测试功能,当新的单元(模型,路由,服务和串行等)被创建的时候,测试会自动生成。我们所创建的最后一个React应用,花费了数天的时间来搭建测试框架,验收测试,以及所有其他工具,这些工具对于拥有一个快速而易于使用的测试套件而言是必须的。并且我们甚至经常对自己的设置倍感沮丧。Ember将验收和单元测试构建到框架之中,我们希望这可以让我们的代码在任何一个层面都可以轻松的进行测试。

构建与部署

Ember由ember-cli提供技术支持,ember-cli会处理构建时的每个步骤:转译JavaScript,构建模块,生产代码,以及使用ember-cli-deploy进行部署。尽管这一系列工具默认就是开箱即用的(例如ES6和模块),但是构建这一标准的社区才是最强大的部分。

在我们之前的设置中,每当我们需要改变构建来执行一个具体目标的时候,我们都需要写一些自定义的代码。当我们的代码部署结束之后,上传sourcemaps到Sentry就是一个绝佳的例子。在React应用中,当webpack构建结束之后,我们会在shell脚本中进行这样的操作。

借助ember-cli,几乎我们需要添加的每一步在Ember插件(如 ember-cli-deploy-sentry)中都能找到。并且,由于Ember强化约定的方式,这些插件几乎都是都是开箱即用的。因此,我们的构建和部署逻辑从一堆杂乱的自定义JavaScript和shell脚本逻辑演变为整洁的不同社区维护的模块组合。

数据流和建模

在Ember中,数据流动和保存的默认执行依赖于ember-dataember-data(以及ember-cli和Ember核心)由核心团队来维护。依据我们最新的React的经验,当我们用redux-saga来使用redux的时候,尽管我们感觉自己已经处于最前沿了,但是我们似乎依然总是觉得不知所措,而试着找出如何做这些事情。当评测Ember时,让我们兴奋的是,借助ember-data,对于如何做事已经有一些现成的模式可用了——即使这些模式中的某些显得有些过时。

服务端渲染

Ember FastBoot 使得任何Ember应用都可以在服务端一键渲染。通过我们在之前的React应用中探索的服务端渲染,我们了解到,对于React相关库的每一项配置,都存在相同复杂程度的服务端渲染设置。服务端渲染并不是我们希望开箱即用的,并且我们依然没有激活这个功能,但是当我们知道Ember的服务端渲染距离我们只有一个cli命令,而不是一个巨大的项目时,还是让我们觉得如释重负。


如果我们使用React生态系统,我们需要做很多决定,经过大概了解和探索所有这些决定,显然,我们会选择放弃自己的决定权,而将这些决定交给一个已经构建客户端应用多年的核心团队和社区。

渐进式增强

当讨论渐进式增强相关的话题时,我们通常指的是网页的渐进式增强,即从纯HTML和CSS向更高级的,动态的JavaScript支持的应用的逐渐加强。是这样的:对于浏览网页应用的人们而言,他们的体验应当根据他们的浏览器所支持的(或者被激活的)技术而逐渐增强。一个浏览器不支持JS的用户,将会获得单纯的HTML和CSS体验——尽管这种体验可能会迟缓或者不爽,但是依然可以运行——而一个拥有现代浏览器的用户将会获得JavaScript的服务,这种服务使得他们的体验非常现代。换句话说,借助渐进式增强,我们会在用户所能到达的地方见到他们——并且,也许最重要的一点是,他们获得了可以获得的最佳体验,而无需做任何事情。

不论是在JavaScript框架还是库中,渐进式增强的最佳观点都应当很明显——我们指的是一个渐进式开发体验 (Progressive Developer Experience),这也是Ember框架和社区的决定性目标之一。

在React生态系统中,进步通常伴随着新的库的产生而发生。flux模式,以及单向数据流的概念就是很好的例子。当facebook刚发布flux架构的时候,一系列执行这一模式的小型库就都冒了出来(其中之一就是reflux,我们当时就开始使用了)。随着时间的推移,这些库中的很多就自己消亡了,而围绕其他库则形成了一些社区(例如redux),但是概念的发展和进化通常都发生在新库引入的时候。例如,redux处理异步操作有一个逐步深入的过程,先是只处理逻辑,然后更深一层加入redux-thunk,后来又通过redux-promise支持promise,最后加入redux-saga支持saga。对于概念的每一次螺旋式上升,一个新的库或者一个工具就需要添加到你的的工具集中:或者回到语言的渐进式增强中来进行尝试,开发者必须做一些事情,或者来修改他们的工具以获得最佳实践。

相反,Ember的目标之一就是为开发者提供当前可获取的最佳实践,而无需开发者做任何事情(或者做尽可能少的事情)。我们同样以单向数据流为例。在这个模式被广泛了解之前,Ember 1.0就已经存在了,随着概念的流行以及在React和flux生态系统中概念的逐渐澄清,Ember核心团队开始思考如何将这个概念融合进Ember中来。借助Ember2.0,以及从控制器逐渐转向组件的过程,单向数据流在Ember中找到了一席之地,而Ember开发者无需添加新的库或者采用主要的概念模式。实现这项增强功能可能需要花费稍微长一点的时间,但是一旦实现,它就是考虑非常周全的,构建到他们已经存在的框架中,并且会有一个非常明确的迁移路径。对于Ember是如何通过渐进式开发体验,向其开发者提供最新的模式和工具的入口,这是一个完美的例子 !

在测评阶段,通过阅读核心团队的工作内容,渐进式开发体验这个主题似乎是一直存在的:如Ember Fastboot(服务端渲染),glimmer(更快的渲染引擎)等这类特色,以及可路由组件(单项数据流),服务(独立于横切关注点(cross cutting concerns))等都是Ember提供的渐进式体验的例子,通常都是直接从其他框架引入的模式中直接借鉴而来,以给Ember用户最佳的网络体验。

如果一个项目会持续多年,而一开始我们就知道,有一个团队会致力于融合最新和最佳实践到我们已有工具中,而我们无需再自己去搜索这些内容,会让人心里倍感欣慰。

使用一个月之后的感触

入坑Ember一个月之后,我们对自己做出使用Ember的决定而感到高兴。尽管现在还高兴地太早,但总的来说,我们的两个猜想都不成问题——我们很享受遵从更高级的工程师所规定的约定,并且已经从Ember各式各样的渐进式开发体验方式中获益。

坚实的文档使得使用这个框架进行起步非常简单,同时拥有很多已经被规定好的惯例让我们的工作非常自由。

  • Grace Wong, 工程师

Ember非常贴心。上手非常容易,而且设计的非常棒。我真的非常喜欢使用css-modules。

  • Aayush Iyer,设计师

除了上面这些,还有很多类似的喜爱Ember的理由。

其他好的部分

ember generate

我们过去从来没有使用过一个提供用于生成新逻辑单元命令行工具的框架,所以很怀疑 ember-cliember generate可以提供的价值。使用这个框架一个多月多一点的时间之后,这就变成了我们最喜欢的一件事情:使用常规的方式来生成任何一个单元的逻辑(模块,路由,服务以及其他),这意味着书写很多小的,容易测试的单元将不再需要认知消耗(cognitive overhead)。这也意味着,不论何时一个新的单元被创建时,一个测试就会被自动为之生成,开发者测试代码将会变得更加简单。

ember-cli-mirage

Mirage是ember-cli的一个插件,该插件使得在开发中模拟AJAX请求和响应变得极其简单。对于要构建新功能的团队而言,这个插件被证明是弥足珍贵的。无需等待队友启动服务器上行路由,或者自己操作上下文切换(context switching),我们就可以通过几行代码来自建路由,提供我们需要的数据。与ember-data结合,生成复杂的关系设置也是非常简单的,而这些在过去在我们的开发工作流中却是一个复杂的任务。

载入和误差(error substates)

向view添加载入和误差(error substates)是一种每个前端工程师都必须多次执行的模式。借助Ember,对于路由载入,载入和错误状态会被自动处理:提供一个路由层级(例如 /organization/<id>/applications),Ember就会自动在每个模板文件夹中寻找loading.hbserror.hbs模板。当数据载入的时候,或者载入失败的时候,Ember会自动渲染一个对应的,与用户当前所处路由最近的载入模板或者错误模板。这个功能使得你可以轻松地添加一个高层次的载入状态,接着在APP任何需要的位置添加一个具体的载入状态。完美!

一些并不是那么美好的部分

当然,并非所有的情况都一帆风顺——学习一个新的框架是一种挑战,从一个框架迁移到另外一个框架也经常会让我们感到不同程度的沮丧。

没有热启动或者时光穿越调试(time travel debugging)

热重载和时光旅行调试(time travel debugging)是React生态系统中发展出来的两大有效工具。

借助热重载,当你修改代码的时候,逻辑重载就已经就绪了,而无需重新载入页面。作为开发者,这意味着你不再需要导航回到页面中的那个状态——拥有合适的状态——即每次你修改时所处的状态。取而代之的是,业务逻辑自动刷新,你可以继续工作。

借助时光穿越调试(time travel debugging),查看应用中状态随时间推移而发生的变化会很容易,而且跳转到这些状态也很容易。这使得诊断和修复来自于复杂状态控制的bug成为了一件很简单的事情。

尽管在Ember中也存在一个热重载的项目,但是这个项目还非常不成熟,而且对同一个Ember应用中不同的部分,这个项目的支持也非常有限。同样,时光穿越调试并不是在Ember社区中被推动的概念——部分原因是,Ember中的数据流并非像类似于redux这样的生态系统一样,适合这样。

不能拥有这些工具是一件很悲伤的事情,但是这并非世界末日。

给组件添加样式依然是一篇蛮荒之地

在React应用中,向组件添加样式总是一件很沮丧的事情。这归结于一下两个原因:

  1. 对于如何给组件添加样式还没有一个最佳实践的定论。css模块?还是在JS中使用CSS?还是纯SASS?

  2. 正是由于原因(1),关于如何给组件添加样式的方式的最佳实践也没有定论,方式包括发布到NPM,并且可以轻松地被其他项目所应用。

在Ember的生态系统中,问题1似乎也没有解决:我们采用的是css模块的方式给组件添加样式,但是也不得不采用一些非常规手段来让其对于基础样式系统(类似foundation)可以生效。我们还没有必须执行第二条原因,但是似乎使用Ember组件会更容易一些。

结论

本文并不是在讨论Ember和React哪个更好。本文讨论的只是,当Clef构建Instant 2FA时,在决定选择React还是Ember时,我们为什么选择了Ember。

构建网站有无数种方式,同时也有无数个理由可以证明,满足我们需求的决定并不一定适合你。这种丰富的选择正是促进web进化如此之快的原因,也是让web如此强大的原因。

我们希望你可以从我们思考的过程中得到一些启示——我们也乐于从你的思考过程中得到一些启发。如果你和我们做了同样的决定,我们希望可以知道你这么做的原因!如果你做了不同的决定,我们也愿意聆听其中的原因!

直接在博客上回复或者在tweeter上给我留言来继续这个讨论。


非常感谢 Chris Trek Glowacki 阅读本文的初稿,也感谢 mixonic 创造了渐进式开发体验(Progressive DX)这个术语。


如果2FA已经成为你的备胎好几个月了,那么请你试试Instant 2FA,一种在数分钟之内就可以给任何网站添加双因子认证的最简便的方式。

相关文章