虚拟DOM已死?

More than React系列文章:

More than React(一)为什么ReactJS不适合复杂的前端项目?

More than React(二)React.Component损害了复用性?

More than React(三)虚拟DOM已死?

More than React(四)HTML也可以静态编译?


本系列的上一篇文章《React.Component损害了复用性?》探讨了如何在前端开发中编写可复用的界面元素。本篇文章将从性能和算法的角度比较 Binding.scala 和其他框架的渲染机制。

Binding.scala 实现了一套精确数据绑定机制,通过在模板中使用 bindfor/yield 来渲染页面。你可能用过一些其他 Web 框架,大多使用脏检查或者虚拟 DOM 机制。和它们相比,Binding.scala 的精确数据绑定机制使用更简单、代码更健壮、性能更高。

ReactJS虚拟DOM的缺点

比如, ReactJS 使用虚拟 DOM 机制,让前端开发者为每个组件提供一个 render 函数。render 函数把 propsstate 转换成 ReactJS 的虚拟 DOM,然后 ReactJS 框架根据 render 返回的虚拟 DOM 创建相同结构的真实 DOM。

每当 state 更改时,ReactJS 框架重新调用 render 函数,获取新的虚拟 DOM 。然后,框架会比较上次生成的虚拟 DOM 和新的虚拟 DOM 有哪些差异,进而把差异应用到真实 DOM 上。

这样做有两大缺点:

  1. 每次 state 更改,render 函数都要生成完整的虚拟 DOM,哪怕 state 改动很小,render函数也会完整计算一遍。如果 render 函数很复杂,这个过程就会白白浪费很多计算资源。
  2. ReactJS 框架比较虚拟 DOM 差异的过程,既慢又容易出错。比如,你想要在某个 <ul> 列表的顶部插入一项 <li> ,那么 ReactJS 框架会误以为你修改了 <ul> 的每一项 <li>,然后在尾部插入了一个 <li>

这是因为 ReactJS 收到的新旧两个虚拟 DOM 之间相互独立,ReactJS 并不知道数据源发生了什么操作,只能根据新旧两个虚拟 DOM 来猜测需要执行的操作。自动的猜测算法既不准又慢,必须要前端开发者手动提供 key 属性、shouldComponentUpdate 方法、componentDidUpdate 方法或者 componentWillUpdate 等方法才能帮助 ReactJS 框架猜对。

AngularJS的脏检查

除了类似 ReactJS 的虚拟 DOM 机制其他流行的框架比如 AngularJS 还会使用基于脏检查的定值算法来渲染页面。

脏检查算法和 ReactJS 有一样的缺点无法得知状态修改的意图这使得 AugularJS 必须反复执行`$digest`轮循、反复检查各个ng-controller中的各个变量。除此之外,AngularJS 更新 DOM 的范围往往会比实际所需大得多所以会比 ReactJS 还要慢。

Binding.scala的精确数据绑定

Binding.scala 使用精确数据绑定算法来渲染 DOM 。

在 Binding.scala 中,你可以用 @dom 注解声明数据绑定表达式。@dom 会自动把 = 之后的代码包装成 Binding 类型。

比如:

@dom val i: Binding[Int] = 1
@dom def f: Binding[Int] = 100
@dom val s: Binding[String] = "content"

@dom 既可用于 val 也可以用于 def ,可以表达包括 IntString 在内的任何数据类型。

除此之外,@dom 方法还可以直接编写 XHTML,比如:

@dom val comment: Binding[Comment] = <!-- This is a HTML Comment -->
@dom val br: Binding[HTMLBRElement] = <br/>
@dom val seq: Binding[BindingSeq[HTMLBRElement]] = <br/><br/>

这些 XHTML 生成的 CommentHTMLBRElement 是 HTML Node 的派生类。而不是 XML Node

每个 @dom 方法都可以依赖其他数据绑定表达式:

val i: Var[Int] = Var(0)
@dom val j: Binding[Int] = 2
@dom val k: Binding[Int] = i.bind * j.bind
@dom val div: Binding[HTMLDivElement] = <div>{ k.bind.toString }</div>

通过这种方式,你可以编写 XHTML 模板把数据源映射为 XHTML 页面。这种精确的映射关系,描述了数据之间的关系,而不是 ReactJS 的 render 函数那样描述运算过程。所以当数据发生改变时,只有受影响的部分代码才会重新计算,而不需要重新计算整个 @dom 方法。

比如:

val count = Var(0)

@dom def status: Binding[String] = {
  val startTime = new Date
  "本页面初始化的时间是" + startTime.toString + "。按钮被按过" + count.bind.toString + "次。按钮最后一次按下的时间是" + (new Date).toString
}

@dom def render = {
  <div>
    { status.bind }
    <button onclick={ event: Event => count := count.get + 1 }>更新状态</button>
  </div>
}

以上代码可以在ScalaFiddle实际运行一下试试。

注意,status 并不是一个普通的函数,而是描述变量之间关系的特殊表达式,每次渲染时只执行其中一部分代码。比如,当 count 改变时,只有位于 count.bind 以后的代码才会重新计算。由于 val startTime = new Date 位于 count.bind 之前,并不会重新计算,所以会一直保持为打开网页首次执行时的初始值。

有些人在学习 ReactJS 或者 AngularJS 时,需要学习 keyshouldComponentUpdate$apply$digest 等复杂概念。这些概念在 Binding.scala 中根本不存在。因为 Binding.scala 的 @dom 方法描述的是变量之间的关系。所以,Binding.scala 框架知道精确数据绑定关系,可以自动检测出需要更新的最小部分。

结论

本文比较了虚拟 DOM 、脏检查和精确数据绑定三种渲染机制。

AngularJS ReactJS Binding.scala
渲染机制 脏检查 虚拟DOM 精确数据绑定
数据变更时的运算步骤
  1. 重复检查数据是否更改
  2. 大范围更新页面,哪怕这部分页面根本没有修改
  1. 重新生成整个虚拟DOM
  2. 比较新旧虚拟DOM的差异
  3. 根据差异更新页面
  1. 直接根据数据映射关系,更新最小范围页面
检测页面更新范围的准确性 不准 默认情况下不准,需要人工提供keyshouldComponentUpdate才能准一点
需要前端工程师理解多少API和概念才能正确更新页面 很多 很多 只有@dombind两个概念
总体性能 非常差

这三种机制中,Binding.scala 的精确数据绑定机制概念更少,功能更强,性能更高。我将在下一篇文章中介绍 Binding.scala 如何在渲染 HTML 时静态检查语法错误和语义错误,从而避免 bug 。

相关链接

 


你想看到的洞见,都在这里

wechat

Share

为什么 ReactJS 不适合复杂的前端项目?

《More than React》系列的文章会一共分为五篇。本文是第一篇,介绍用ReactJS开发时遇到的种种问题。后面四篇文章的每一篇将会分别详细讨论其中一个问题,以及Binding.scala如何解决这个问题。

More than React系列文章:

More than React(一)为什么ReactJS不适合复杂的前端项目?

More than React(二)React.Component损害了复用性?

More than React(三)虚拟DOM已死?

More than React(四)HTML也可以静态编译?


背景介绍

去年 4 月,我第一次在某个客户的项目中接触到ReactJS 。

我发现ReactJS要比我以前用过的AngularJS简单很多,它提供了响应式的数据绑定功能,把数据映射到网页上,使我可以轻松实现交互简单的网站。

然而,随着我越来越深入的使用ReactJS,我发现用ReactJS编写交互复杂的网页很困难。 我希望有一种方式,能够像ReactJS一样简单解决简单问题。此外,还要能简单解决复杂问题。

于是我把ReactJS用Scala重新写了一个。代码量从近三万行降到了一千多行。

用这个框架实现的TodoMVC应用,只用了154行代码。而用ReactJS实现相同功能的TodoMVC,需要488行代码

下图是用Binding.scala实现的TodoMVC应用。

这个框架就是Binding.scala

问题一:ReactJS组件难以在复杂交互页面中复用

ReactJS中的最小复用单位是组件。ReactJS的组件比AngularJS的Controller和View 要轻量些。 每个组件只需要前端开发者提供一个 render 函数,把 propsstate 映射成网页元素。

这样的轻量级组件在渲染简单静态页面时很好用, 但是如果页面有交互,就必须在组件间传递回调函数来处理事件。

我将在《More than React(二)React.Component损害了复用性?》中用原生DHTML API、ReactJS和Binding.scala实现同一个需要复用的页面,介绍Binding.scala如何简单实现、简单复用复杂的交互逻辑。

问题二:ReactJS的虚拟DOM 算法又慢又不准

ReactJS的页面渲染算法是虚拟DOM差量算法。

开发者需要提供 render 函数,根据 propsstate 生成虚拟 DOM。 然后 ReactJS 框架根据 render 返回的虚拟 DOM 创建相同结构的真实 DOM.

每当 state 更改时,ReacJS 框架重新调用 render 函数,获取新的虚拟 DOM 。 然后,框架会比较上次生成的虚拟 DOM 和新的虚拟 DOM 有哪些差异,然后把差异应用到真实DOM上。

这样做有两大缺点:

  1. 每次 state 更改,render 函数都要生成完整的虚拟 DOM. 哪怕 state 改动很小,render函数也会完整计算一遍。如果 render 函数很复杂,这个过程就白白浪费了很多计算资源。
  2. ReactJS框架比较虚拟DOM差异的过程,既慢又容易出错。比如,假如你想要在某个 <ul> 列表的顶部插入一项 <li> ,那么ReactJS框架会误以为你修改了 <ul> 的每一项 <li>,然后在尾部插入了一个 <li>

这是因为 ReactJS收到的新旧两个虚拟DOM之间相互独立,ReactJS并不知道数据源发生了什么操作,只能根据新旧两个虚拟DOM来猜测需要执行的操作。 自动的猜测算法既不准又慢,必须要前端开发者手动提供 key 属性、shouldComponentUpdate 方法、componentDidUpdate 方法或者 componentWillUpdate 等方法才能帮助 ReactJS 框架猜对。

我将在《More than React(三)虚拟DOM已死?》中比较ReactJS、AngularJS和Binding.scala渲染机制,介绍简单性能高的Binding.scala精确数据绑定机制。

问题三:ReactJS的HTML模板功能既不完备、也不健壮

ReactJS支持用JSX编写HTML模板。

理论上,前端工程师只要把静态HTML原型复制到JSX源文件中, 增加一些变量替换代码, 就能改造成动态页面。 理论上这种做法要比Cycle.js、Widok、ScalaTags等框架更适合复用设计师提供的HTML原型。

不幸的是,ReactJS对HTML的支持残缺不全。开发者必须手动把classfor属性替换成classNamehtmlFor,还要把内联的style样式从CSS语法改成JSON语法,代码才能运行。 这种开发方式下,前端工程师虽然可以把HTML原型复制粘贴到代码中,但还需要大量改造才能实际运行。 比Cycle.js、Widok、或者、ScalaTags省不了太多事。

除此之外,ReactJS还提供了propTypes机制校验虚拟DOM的合法性。 然而,这一机制也漏洞百出。 即使指定了propTypes,ReactJS也不能在编译前提前发现错误。只有测试覆盖率很高的项目时才能在每个组件使用其他组件时进行校验。 即使测试覆盖率很高,propTypes仍旧不能检测出拼错的属性名,如果你把onClick写成了onclick, ReactJS就不会报错,往往导致开发者额外花费大量时间排查一个很简单的bug。

我将在《More than React(四)HTML也可以编译?》中比较ReactJS和Binding.scala的HTML模板,介绍Binding.scala如何在完整支持XHTML语法的同时静态检查语法错误和语义错误。

问题四:ReactJS与服务器通信时需要复杂的异步编程

ReactJS从服务器加载数据时的架构可以看成MVVM(Model–View–ViewModel)模式。 前端工程师需要编写一个数据库访问层作为Model,把ReactJS的state当做ViewModel,而render当做View。 Model负责访问数据库并把数据设置到state(即View Model)上,可以用Promise和fetch API实现。 然后,render,即View,负责把View Model渲染到页面上。

在这整套流程中,前端程序员需要编写大量闭包组成的异步流程, 设置、访问状态的代码五零四散, 一不小心就会bug丛生,就算小心翼翼的处理各种异步事件,也会导致程序变得复杂,既难调试,又难维护。

我将在《More than React(五)为什么别用异步编程?》中比较ReactJS和Binding.scala的数据同步模型,介绍Binding.scala如何自动同步服务器数据,避免手动异步编程。

结论

尽管Binding.scala初看上去很像ReactJS, 但隐藏在Binding.scala背后的机制更简单、更通用,与ReactJS和Widok截然不同。

所以,通过简化概念,Binding.scala灵活性更强,能用通用的方式解决ReactJS解决不了的复杂问题。

比如,除了上述四个方面以外,ReactJS的状态管理也是老大难问题,如果引入Redux或者react-router这样的第三方库来处理状态,会导致架构变复杂,分层变多,代码绕来绕去。而Binding.scala可以用和页面渲染一样的数据绑定机制描述复杂的状态,不需要任何第三方库,就能提供服务器通信、状态管理和网址分发的功能。

以下表格中列出了上述Binding.scala和ReactJS的功能差异:

Binding.scala ReactJS
复用性 最小复用单位 方法 组件
复用难度 不论交互内容还是静态内容都容易复用 容易复用静态内容组件,但难以复用交互组件
页面渲染算法 算法 精确的数据绑定 虚拟 DOM
性能
正确性 自动保证正确性 需要开发者手动设置 key 属性,不然复杂的页面会错乱。
HTML 模板 语法 Scala XML 字面量 JSX
是否支持 HTML 或 XHTML 语法 完整支持 XHTML 残缺支持。正常的 XHTML 无法编译。开发者必须手动把 classfor 属性替换成 classNamehtmlFor,还要把内联的 style 样式从 CSS 语法改成 JSON 语法。
如何校验模板语法 自动编译时校验 运行时通过 propTypes 校验但无法检测简单的拼写错误。
服务器通讯 机制 自动远程数据绑定 MVVM + 异步编程
实现难度 简单 复杂
其他 如何分派网址或者锚点链接 支持把网址当成普通的绑定变量来用,无需第三方库。 不支持,需要第三方库 react-router
功能完备性 完整的前端开发解决方案 本身只包含视图部分功能。需要额外掌握 react-router 、 Redux 等第三方库才能实现完整的前端项目。
学习曲线 API 简单,对没用过 Scala 的人来说也很好懂 上手快。但功能太弱导致后期学习第三方库时曲线陡峭。
Binding.scala ReactJS

两个多月前,我在Scala.js的论坛发布Binding.scala时,当时Scala.js社区最流行的响应式前端编程框架是Widok。Tim Nieradzik是Widok的作者。他在看到我发布的框架后,称赞这个框架是Scala.js社区最有前途的 HTML 5渲染框架。

他是对的,两个月后,现在Binding.scala已经成为Scala.js社区最流行的响应式前端编程框架。

Awesome Scala网站对比了Scala的响应式前端编程框架,Binding.scala的活跃程度和流行度都比Udash、Widok等其他框架要高。

我在最近的几个项目中,也逐渐放弃JavaScript和ReactJS,改用Scala.js和Binding.scala搭建新时代的前端技术栈。

相关链接

Share

关于前端的思考:AngularJS 2.0以及前后端边界

前端的学习曲线

每个人在学AngularJS的时候都会觉得Angular 1.x自创的概念实在太多,学习曲线也因此变得非常陡峭。但对于一个完整的前端项目来说,所需要的东西本来就不够简单,而AngularJS作为一款大而全框架,自带一揽子解决方案,只要学习上手之后还是会有一劳永逸的感觉。就像Python的web框架代表Django和Flask一样,萝卜白菜各有所爱,轻量级框架所带来的灵活性固然很棒,但对于新手来说依旧会很容易玩脱。就像当前所兴起的React大潮,暂且不讨论深度玩家所表现的态度和看法,就论一个前端新手所面临的问题,在没有主见的时候到底该师从何派?

对于前端刚入门的我来说,依旧会推荐从一个大而全的框架开始学起,一个好的框架不但会强制你不犯错误,由此带来的「配置大于约定」也会让一个还没有能力进行约定的能力去学习如何约定。当你学有所成的时候自然会似脱缰一般出去闯荡一番。就像当初青春期的我们,在蜕变之前我们安定得学习该有的技能,当有了一定资本之后就开始自我思考,决定去走自己的路。

反过来说,其实走自己的路,又何尝不是陡峭的呢?对于React来说,也许它所带来的概念非常简单给力。但与此同时,若是以完成整个前端项目为目标的话,你所需要绝对不仅仅只是一个View层的React所能办到的,你会发现前端还可能面临构建、路由、数据流处理等等一系列问题。所以就像当初遇见AngularJS一样,又开始接触眼花缭乱的第三方库所灌输的各种概念。这个时候,你还会认为组合拳的方式好于一揽子式的解决方案吗?

当我们站到一定高度之后再回过头来看问题,似乎问题就变得简单乃至问题都不复存在了。而如何能站到更高的高度呢?那就是开始同时尝试两种方案吧。只有积攒了一定的经验之后,才会认识到跟随永远不是最终的答案,只有亲身体验之后才会拥有自己的认识。那么,最终送上一句话:就是干!

AngularJS 1.x到2.0

从Angular 1.x官方文档的变迁中就可以看出,Google已经有意精简了核心Modules的内容,并且让其所引入的概念尽可能少。AngularJS拥有着诸多特性,人们津津乐道就是:依赖注入、模块化、自动化双向数据绑定、语义化标签等等。而如果你是一个习惯于写后端的软件工程师的话,所谓的DI和模块化都是常用的代码分层手段,而双向绑定也只是一种VM的简化形式,最核心也是最新颖的概念反而就是Directive,赋予了HTML更强大的能力,相当于让浏览器学习了新的语法。

但与此同时指令也变得过于复杂,赋予Template过多的功能之后只会让人想起原来的服务端脚本语言,比如JSP或者ASP,它们使用数据库的内容加上逻辑判断来直接填充HTML模板。而目前AngularJS中的赋予了类似JSP的过强能力,允许了,甚至鼓励了程序员把代码写得混乱的行为,模板再次成了灰色地带

AngularJS的创始人之一Misko Hevery:AngularJS弥补了HTML在构建应用方面的不足,其通过使用标识符(directives)结构,来扩展Web应用中的HTML词汇,使开发者可以使用HTML来声明动态内容,从而使得Web开发和测试工作变得更加容易。

当AngularJS刚创建出来的时候,它并不是给开发人员用的。它是一个工具,更倾向于给需要快速创建持久化HTML表单的设计人员用。随着时间推移,它作了改变以适应各种场景,开发人员也用它建造更多、更复杂的应用程序,而只是在原有基础之上直接进行「增量化地」改进是远远不够的。这就是Angular 2.0在较高层次上的动机。更详细的内容可以参考这篇[翻译]有关Angular 2.0的一切,我还特意去翻了一下原作者Rob Eisenberg的Blog和Twitter,结果就发现他是:

Creator of Caliburn.Micro & Durandal. Former Angular 2.0 team member. Currently building a new tech startup, Durandal Inc., whose first product is Aurelia.

Aurelia和Angular 2.0有诸多相似之处,详细的内容可以参考Introducing Aurelia,以及后Angular时代二三事这篇文章里面所提到的一些共同特性。

最后从这篇浴火重生的Angular中查看关于Angular 2.0最新的module、Web Components、observe、promise等特性吧,据说被诟病已久的性能也优化得不行不行的,总之我还是相当期待Angular 2.0的!

划分前后端边界?

在这篇来自关于[翻译]Angular的问题文章中,作者ppk乃至译者xufei自己也提到,Angular更多地是面向企业的IT部门,而不是前端人员,并且使用AngularJS的用户更多是有Java背景的人员。而在现在这个前端粥多僧少的阶段,必然有很大一部分Java开发人员要去写JavaScript,但与此同时由于JavaScript代码太过缺乏约束,也让Java开发人员更加无所适从。这时Angular的约束性以及依赖注入等特性的好处就彰显出来了,特别是对于传统后端开发者来说,当遵守AngularJS的约定时,生产力也会更高。

与此同时,AngularJS独特的编码风格,它那种更倾向服务端而不是浏览器端的对HTML模板系统的封装形式,以及严重而基础的性能问题也吓跑了不少原来写前端的开发者。对于很多前端人员而言,最大的问题就是,AngularJS强迫自己用一种指定的方式去干活。

xufei提到的另外一个关于前端代码写得烂的原因就在于:前端开发者缺乏架构意识,或者项目负责人和架构师(通常是后端)没有足够的前端知识,而这两点不解决,用什么框架都一定做成渣。这点需要反对一下的就是,这跟框架可用性以及易用性的关系还是挺大的,要是开发者都能够有清晰的编程架构意识,那岂不是纯靠原生的Java就可以把后端写得很漂亮,又或者是只靠JavaScript、CSS、HTML就可以把前端的脏活干得很漂亮?

然后,其实这儿也牵扯出一个更有趣的问题,在前后端分别都有相应的「模板」概念,那么HTML的动态内容究竟应该由谁来处理,也就是如何划分和界定前端后端?而评论中也有两位大神对模板应当归属于浏览器端还是服务器端吵得不可开交,而我个人还是比较赞同@calidion的观点,不应该去区分绝对的前端后端,更多内容在:Web开发的前端与后端的界线在那里?,最后的结论就在于「运行环境是唯一的前后端分界线」。

那么,在这个前后端分离趋势愈演愈烈的时期过渡之后呢?Web的未来是在哪里?Isomorphic/Universal JavaScript吗?其实对于一个更广泛概念的Application来说,前后端本来就是一家,最多分为界面(Application的界面可能是Web/iOS/Android/Desktop等等)和背后的数据处理而已。若是使用统一的数据格式(JSON)并且在浏览器内存和数据库间实现数据同步(个人很喜欢Meteor的概念),剩下的就只是编写业务逻辑,然后如何把数据显示到不同的「界面」上的问题而已。

Share

前、后端分离,谁值得拥有?

近两年来,前、后端分离的架构得到越来越多的认可,越来越多的团队在尝试、推广这种架构。但在团队采纳这种架构之前依然需要冷静思考,这是不是自己需要的?

什么是前、后端分离?

字面理解,前端与后端分离。以Web系统为例,浏览器一端的显示、交互、逻辑处理是系统的前端;前端需要获取数据、持久化数据、通知其他系统,这些无法在浏览器中单独完成,需要后端提供服务。很明显前端系统、后端系统已经分离,那为什么还要强调分离呢?此分离非彼分离,系统的实例是分离的,但系统的母体(代码)未必分离。所以,在这里前、后端分离指的是前、后端代码(在组织形式、调用结构等方面)分离。

有些团队会认为单页面应用(Single Page Application)就是前、后端分离,持这种说法的往往是后端背景较重的团队,因为单页面应用往往用到一些前端框架,把后端同学从输出前端元素的痛苦中解救出来。

还有人认为前端和后端通过API通讯就是前、后端分离,这只是分离前、后端代码的一种手段。

前、后端代码的组织形式

前、后端代码的组织形式有三种,分离、不分离、分离也不分离。第一种形式-分离,最基本的要求是前端代码和后端代码各自有独立的代码库,更好的做法是前端代库自带假后端,可以独立进行开发测试,而后端代码中包括前、后端交互的测试用例。第二种形式-不分离,前端代码、后端代码在同一个代码库,没有明显的前、后端概念,读取数据库和生成HTML、CSS、Javascript的代码混在同一个文件或目录中。可能使用了一些模板技术,但总体还是输出前端元素的思路。第三种形式-分离也不分离,这是介于第一和第二种之间的形式。可能有或没有独立的代码库,但前端代码和后端代码至少在不同的目录中。后端代码不关心或很少关心前端元素的输出。但不像第一种形式,前端代码往往不带假后端,不能独立进行开发测试,而后端往往没有前、后端交互的测试用例。

目前很多正在转换或刚刚转换到前、后端分离架构的系统往往采用第三种形式。比如,Rails背景的团队会分离出Rails API,把前、后端放在不同的代码库中,但开发过程中,往往会把前、后端代码放在同一个编辑环境中,因为前端代码目录中没有足够的信息进行独立开发,而后端代码目录也没有足够的信息确定是否会影响到前端。再比如,有的团队可能用到了AngularJS,EmberJS,ReactJS等前端框架,这些框架的独立性和前端为中心的思考方式导致很自然地分离出单独的前端目录,但是开发过程中依然需要前端代码和后端代码同时存在才能顺利进行。

前、后端代码分离的原因

产品是组织沟通结构的缩影 – 康威定律

前、后端代码的分离又一次验证了康威定律。前、后端代码分离表明前端团队和后端团队的越来越独立,以至于他们之间需要一个明确的界限。与其说前、后端团队的独立,不如说是前端团队的壮大。前端不再是人数很少的几个美工、设计角色的附属人员,前端也是有框架开发、组件开发、应用开发、UX设计等角色的独立团队了。

很容易理解前端团队的独立。一方面Web应用的前端越来越“重”,体现在交互越来越丰富、对页面操作的体验要求更快更炫,这使得前端的逻辑愈加复杂、页面渲染多样化。另一方面,多种端化的需求越来越多。这一切给前端融入了很多的技术,前端已今非昔比,其技术复杂性可以和后端相提并论,也就需要相应能力和数量的团队。

前、后端代码分离带来的好处有很多,比如通过分离关注点使代码职责单一产生更容易维护的代码,减少前端代码对后端开发人员的干扰,提高前端响应速度,提高后端性能,等等。这些好处都是结果,其原因还是技术复杂性带来的团队结构变化。

有些框架或平台花费很大精力做到前后端统一,比如Rails,Meteor,它们就不强调前后端,把前后端很好地融合在一起。这或许是一种方向,把技术复杂性降低,以至于开发人员可以同时掌握前、后端开发的所有技能。如果能做到这一点,也就不需要独立的前端团队,自然也就不需要强调前后端分离。不过就目前看,这类框架和平台还是有其适用范围,在系统的规模和性能需求上升到一定规模后还是不能掩盖技术的复杂性。

谁需要前、后端分离?

Web应用的需求和多终端化推动了前端技术的进步,但不意味着所有系统都有非常复杂的前端,因此不应该不假思索地采用前、后端分离。

我建议先区分系统的前端类型再考虑团队的人员结构,因为前端类型不同意味着不同的前端开发工作量。可以根据前端的轻重把系统分为三类,轻前端、重前端、不轻不重的前端。系统的类型没有严格的界限,取决于当时的技术水平以及决策人对技术的了解程度,对于一个非常熟悉Responsive的人可能不认为适应多终端是个难题,但放在两年前对于一个对CSS没有兴趣的人会认为适应多终端是个很重的需求。我根据自己仅有的一点点前端技术给出一个我认为的划分。

轻前端类型的系统具有以下特点:

  • 对页面布局、配色、字体没有具体要求,好看就行
  • 只有比较简单的特效
  • 只有简单的表单验证、表单提交
  • 几乎没有自定义的拖拽、滚动操作
  • 不需要Responsive,在不同终端布局能适应即可
  • 不需要Native App

重前端类型的系统具有以下特点:

  • 对页面布局、配色、字体有具体要求,甚至有一些创新性的设计
  • 有很多特效
  • 有复杂的业务逻辑
  • 有自定义的拖拽、滚动操作
  • 需要Responsive
  • 需要Native App(允许Hybird)

不轻不重的前端介于轻前端和重前端之间:

  • 对页面布局、配色、字体有一些指导性的要求
  • 有一些特效
  • 有简单的业务逻辑,后端愿意承担更多的业务逻辑以简化前端
  • 有或没有自定义的拖拽、滚动操作
  • 不需要Responsive,在不同终端布局能适应即可
  • 需要Native App(Hybird即可)

对于轻前端的系统,不必追求前、后端分离。一方面因为涉及的前端技术并不复杂,开发人员应该能够掌握大部分技能,不会需要花费很多时间学习新技能。另一方面,把前、后端代码组织在一起也便于开发过程中查找相关信息。当然了,并不提倡把前、后端的代码混杂在一起,还是要做到关注点分离(Separate of Concern)。前、后端代码还是应该在顶级目录做区分,只不过在设计决策时让后端承担更多的职责。比如,后端提供前端专用的API接口,使得前端少做或不做转换。

对于不轻不重的系统,建议综合考虑团队的人员结构和未来的发展方向。如果团队前端人员占比不高并且不认为未来会持续做下去,那么就不用过分追求前、后端分离。如果团队前端人员占比不高,但对未来有更高期望,那么建议立即转向前、后端分离,并注重建设前端团队,在设计决策时要避免后端过度承担职责。如果已经有相当规模的前端团队,建议立即转向前、后端分离,并且尽早做到独立代码库,前端有假后端,后端有针对前对的测试用例,使前、后端开发真正分离。

对于重前端的系统,建议采用前、后端分离架构,并且应该尽早完善前端团队建设,确保前端团队和后端团队的平等地位。

Share

我们真的缺前端工程师吗?

前言

这两天在好几个地方都看到了一篇关于为什么整个互联网行业都缺前端工程师?的文章,文章本身是去年的,中心思想是:其实我们并不缺前端工程师,我们缺的是优秀的前端工程师。我还是比较同意作者观点的,不过略有意犹未尽的感觉。于是我结合自己的经验,也来聊一下这个话题:我们真的缺前端工程师吗?

These walls are kind of funny like that. First you hate them, then you get used to them.Enough time passed, get so you depend on them. That’s institutionalising.

传统软件公司划分开发者的方式下,在前端部门的程序员永远不会去读缓存数据部分的代码,设计师也不太可能去和开发坐在一起,开发也不知道软件最终软件会以何种方式部署在服务器上。

什么是前端工程师

我在招聘广告和办公室的一些对话中,听到了一个新的角色:UI Dev,事实上我在知乎上还回答过一个关于ThoughtWorks的UI Dev的问题。简而言之,UI Dev可以快速的把设计师的作品实现为HTML/CSS/JavaScript代码。

01

如果按照这个标准,我觉得UI Dev对自己的要求太低了。毕竟要学会HTML/CSS实现mockup并不困难,但是成为一名前端工程师则需要掌握更多的知识:

  • 会用PS来进行图片的处理(比如切图,微调等)
  • 用HTML/CSS实现mockup(可能还有SASS/LESS等工具)
  • 熟悉JavaScript(比如前端的MVVM框架,客户端模板)
  • 前端开发的工作流程(代码检查,精简化,模块化CSS,LiveReload,调试)
  • 编写测试(静态检查,单元测试)
  • 跨浏览器、跨设备的解决方法(不同分辨率,不同厂商)
  • 会根据项目的特点选择不同的前端技术栈(移动端,Web站点,响应式设计等)

在有了基础的HTML/CSS/JS技能之后,你会尝试做的更好:

  • 如何更高效的操作DOM
  • 如何将CSS写的更加清晰易懂
  • 如何编写更加易于维护的代码(更有意义的单元测试)
  • 如何组织大型的项目结构,模块化,组件化等等

这些要求事实上已经不那么容易做到了。它可能会花费你2到3年时间来完全掌握。但是2到3年之后,即便你已经成为了一个“合格的”前端工程师,这也还远远不够。在现实世界中,一个软件产品除了前端,还有非常广阔的空间,还有很多有趣的东西值得学习:

  • HTTP协议本身(缓存,鉴权)
  • Web容器/HTTP服务器如何工作
  • 无状态的Web应用的工作原理(如何让网站正确地运行在集群上)
  • 动态,静态内容如何分离部署(反向代理配置)
  • 安全机制如何配置
  • 监控机制如何配置

有了这些,也算是有点端到端的意思了。这时你也已经不是一个纯前端工程师了,系统中的大部分问题你都可以搞定,不过日常工作中可能更多的职责还是做前端的开发。但是这些还不够,软件除了交付之外,还有一些非功能性的需求:

  • 端到端测试(UI测试,比如selenium server/web driver)
  • devops(比如数据库环境,测试服务器,CI服务器的自动化provision)
  • 基本的UI设计原则(在某些页面确实的情况下,根据系统的已有UI做设计)
  • 数据库性能优化
  • 性能测试

这时候,你才能算是一个严格意义上的“前端”工程师。不从系统的角度来思考,不真正做一些后端开发/配置,并不能算是前端工程师,或者可以被称为偏前端工程师(partial frontend developer)。但是即使称为上边这样的“前端工程师”,我想这离一个优秀的工程师还是有很大差距的。

我跟一位设计师同事聊过这个问题:

Dev眼中的世界是这样的,从墙上(物理的或者电子的)上找到一些卡片(story卡或者需求文档说明书),然后撸袖子开干,干的过程中有很多自以为是的理解,同样有一些自以为是的牛逼实践(TDD啊,自动化啊),最后功能做完,大功告成,然后接着做下一个卡片。传统的Dev,或者苦逼屌丝程序员的世界就是这样的:需求从哪儿来,不知道;做完之后谁来负责质量,不知道;最终上线的时候怎么发布,不知道;线上有问题了怎么办,不知道。

在ThoughtWorks,Dev的工作有了很大的变化,一个最明显的变化是边界的模糊。比如很多项目都不设QA角色,所有人都对质量负责,都做测试,也有OPs角色,但是大部分非生产环境都是Dev自己发布。也就是说,软件/项目生命周期中的大部分实践我们都能涉足,而且可以带来改进,提升效率。但是这只是往下游(从开发,到测试,到部署,到运维),反过来看上游,比如需求从哪儿来,Dev还是不知道。这毫无疑问是一个令人沮丧的事实,因为这需求的产生才是核心,也就是我昨天跟你聊的:一个idea如何变成一个可视化的原型,然后进一步演进为项目原型?

开发工作不应该仅仅局限在编码上,作为开发者/工程师,应该尽可能的多了解一些上下文:比如我们的项目最终是给谁用的,需求从何而来,项目是如何部署在线上的等等。

02

software life cycle

简而言之,开发者视野应该放开开阔一些。不要将自己局限在某种角色上,不但不要局限在前端/后端开发上,压根就不要局限在开发这种角色本身上,你在系统中,可以是设计师,还可以是业务分析师。虽然不一定最终要你去转行做BA,或者UX,但是更广阔的视野可以使你更加高效的发挥自己的作用,也可以在和别的角色互动式,快速的了解上下文。

我所理解的,前端不一定要熟知所有这些知识和技能,但是一定不要认为自己做好了前端的一亩三分地就足够了,不要给自己设限。跨界会给你带来难以估量的好处,一个角色做久了,难免会产生一些盲点。这时候,换个视角,从其他角色的角度来看待你的工作,又会有很多新的发现。而且不仅如此,很可能你会发现之前很麻烦,很难搞定的事情,在新的方法/视角下变得很容易。

我的故事

其实,我是一名后端开发

工作之后,我在很长一段时间是专注于“非前端”的领域。和很多刚入行的新人一样,我对计算机能触及的几乎一切领域都感兴趣:语言解释器,人工智能(遗传算法,隐式马尔科夫模型,自动纠错,模式识别),嵌入式开发,图形处理,操作系统的进程调度,进程间通信,多线程模型,各种脚本语言(python,ruby,JavaScript等等),另外,日常开发流程中的一些工具的定制化也会花去我很多的时间,比如如何配置vim,写几个小脚本来和编辑器做集成等等。更别说那些令人一听就觉得激动的编程范式:面向对象,基于消息总线,函数式编程等等。如果你感兴趣,可以看看我几年前的博客

我的上一家公司的产品是一个省级电网的收费/计费系统(电其实和我们在超市里购买的其他生活用品一样,也是一种商品)。我在那里工作了差不多两年,日常的开发方式就是ssh登陆到RHEL(Redhat Enterprise Linux)服务器上,用vim(当然有一堆的vim插件)开发C代码,调试器是gdb(对,就是那个很牛逼,但是对新手特别不友好的gdb)。

我们用C语言给Apache的httpd写了一个扩展module,大约相当于现在rack里的中间件,这个module要和后端的一个要复杂的多的模块通信,其中不但涉及网络通信,还有*nix管道,缓冲,并发等等考虑。在这两年里,我几乎没有碰过任何的Web界面上的东西(处理用php写了一两百行的页面之外)在加入这家公司之前,我在一家用Java做报表的公司工作,技术栈为J2EE。其中有一些前端的工作,但是并不很多,而且说实话,我当时有些看不太上这些技术。HTML/CSS在我心目中的地位比线程池,语言解析等差远了,所以我也没有认真地去系统学习。

在加入ThoughtWorks之前,在“前端”方面,唯一算是比较擅长的也不过是写JavaScript,而且对于前端的MVVM框架,双向绑定,模块化等高级货都没听过。且不能论HTML/CSS的最佳实践,连根据设计稿做出一个静态页面的的能力也不具备。我之前有一点JSP/HTML经验,而CSS经验也并没有超出如何画一个细线表格的范畴。换句话说,我的前端(特别是HTML/CSS)是最近才学会的。

ThoughtWorks的开发

在ThoughtWorks,很多团队是按照feature团队来组建的。相对于传统的component团队(按部门划分,比如研发组,测试组,设计组等,每个组还有可能会再细分成如用户调研,流程设计,视觉设计等等),feature团队里配备了软件开发过程中需要的几乎所有角色:业务分析,测试工程师,开发工程师,设计师(设计师一般不会常驻),有的团队还有项目经理的角色。

在feature团队里,你可以很容易看到不同的角色是如何工作的,很多时候,开发会和设计师一起来调整颜色,排版,布局,也可能和测试一起编写自动化测试用例,showcase等。也就是说,角色之间的藩篱在淡化,而就开发这一种角色而言,对于前端/后端的区分也会显得非常模糊,因为需求划分之后的story(敏捷开发中的一个术语,其实就是需求的一种展现方式)是端到端的,比如一个商品列表展示的story,会包括

  • 数据库的表结构
  • 访问数据库的ORM部分,
  • 使用ORM的业务逻辑service
  • 响应客户端的controller(消费JSON或者XML的HTTP接口)
  • 发送请求,处理响应的JavaScript代码
  • 和设计稿一致的CSS样式

而且在这个过程中还会涉及到一些外围的工具

  • 虚拟机环境准备
  • 数据库连接
  • 自动化测试(单元测试,集成测试,可能还有UI测试)
  • 数据库迁移脚本

在这个过程中,开发者需要掌握和开发过程相关的一切实践中的一切工具.

在我的ThoughtWorks的第一个项目中,我是以Java开发工程师角色加入的,下项目的时候,我学会了自动化provision,cucumber测试工具,Rails,gradle(没错,我之前用Java都是用IDE构建的,在Linux世界我用make),jasmine测试工具,Backbone.js,haml.js。

第二个项目的时候,我是以前端工程师角色加入,下项目的时候,我学会了nginx配置缓存、负载均衡服务器,gatling测试工具,Hadoop/Spark等的集群配置,还有一些和项目相关的GIS(地理信息系统)的技术栈,前后端分离策略等。

第三个项目我是以Java开发工程师角色加入的,下项目的时候,我学会了如何做性能测试,如何建立一个漂亮的Dashboard(可以用来展现CI等),而且在业余时间系统的学习了CSS3和HTML5,将之前零敲碎打的那些知识串起来,这些总结做了几次内部培训后,还整理成了一本电子书

第四个项目我又变成了一个前端工程师,但是这个项目有意思的地方是跟mobile相关,于是页面性能,体验又变成了一个重点,下项目的时候,我对无状态的Web应用,session的持久化,CSS3的动画,用Backbone.js组织多页面的方式等等又有了新的理解。

如果这些经历造成了你觉得我很牛的错觉,那我应该道歉。我觉得自己勉强可以算是个合格的程序员:对学习保持着热情,对解决问题保持着热情,仅此而已。在项目上,如果我发现了问题,我就想办法解决,如果属于知识欠缺,那我就会去学习。我还远远没有到达精通这些技术的地步,但是在工程实践领域,根据80/20原则,这些粗浅的知识足以解决80%的问题,而另外的20%,我们才真正需要一个专家来帮忙。也就是说,团队里需要有一个能解决20%的问题的前端工程师,而其他的80%的前端工作,应该可以被其他所有的开发完成,对于后端开发也是一样。

在ThoughtWorks,经常可以遇到非常优秀的跨界人才:比如做出心声(一个帮助聋哑人打电话的软件)的朱晨,他的主要角色是用户体验设计师,为了开发心声这个软件专门学习了如何开发;比如西安办公室的大牛何飞,在学完了我上边提到Web领域的绝大部分技能之后,又开始学习各种底层算法;比如UX 王倩蕾唐婉莹,也在积极的学习前端开发;同样UX角色的刘海生,又在向BA角色进发。

总结

我们缺的从来都不是前端/后端工程师,而是工程师(或者那些会系统思考,并总是想着解决问题的人)。角色划分在大的机构内可能是有意义的,就像历史上工厂里,工人被分为车工,钳工,木工,电工。但是这种模式在软件开发中未必好用,具体而微的小团队可能更具竞争力。而在一个个的小团队中,再细分前端后端就显得比较滑稽了。团队中的每个成员都应该具备基本的端到端能力(不仅仅是开发,更应该是具有业务上下文,即每个人都清楚我们要交付的最终产品是什么,以及这个产品是如何帮助最终用户的),每个成员也都需要为最终的交付物负责,而不是为自己的职责负责。

感谢张霄翀唐婉莹为本文提供的feedback

Share