《More than React》系列的文章会一共分为五篇和一则附录。本文是第一篇,介绍用 ReactJS 开发时遇到的种种问题。后面四篇文章的每一篇将会分别详细讨论其中一个问题,以及 Binding.scala 如何解决这个问题。附录是一则指南,指引你从头一步步创建 Binding.scala 项目。
背景介绍
去年 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 函数,把 props 和 state 映射成网页元素。
这样的轻量级组件在渲染简单静态页面时很好用,但是如果页面有交互,就必须在组件间传递回调函数来处理事件。尤其是复杂的网页结构,往往需要多个组件层层嵌套,导致回调函数也必须在父子组件间层层传递,代码变成一团乱麻,维护就很难了。
我将在《More than React(二)组件对复用性有害?》中用原生DHTML API、ReactJS 和Binding.scala 实现同一个需要复用的页面,介绍Binding.scala 如何简单实现、简单复用复杂的交互逻辑。
问题二:ReactJS 的虚拟 DOM 算法又慢又不准
ReactJS 的页面渲染算法是虚拟 DOM 差量算法。
开发者需要提供 render 函数,根据 props 和 state 生成虚拟 DOM。然后 ReactJS 框架根据 render 返回的虚拟 DOM 创建相同结构的真实 DOM.
每当 state 更改时,ReacJS 框架重新调用 render 函数,获取新的虚拟 DOM 。然后,框架会比较上次生成的虚拟 DOM 和新的虚拟 DOM 有哪些差异,然后把差异应用到真实 DOM 上。
这样做有两大缺点:
- 每次 state 更改,render 函数都要生成完整的虚拟 DOM. 哪怕 state 改动很小,render 函数也会完整计算一遍。如果 render 函数很复杂,这个过程就白白浪费了很多计算资源。
- 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 的支持残缺不全。开发者必须手动把 class 和 for 属性替换成 className 和 htmlFor,还要把内联的 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 负责访问后端 API 并把数据设置到 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 无法编译。开发者必须手动把 class 和 for 属性替换成 className 和 htmlFor,还要把内联的 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 搭建新时代的前端技术栈。
相关链接
- Binding.scala 项目主页
- Binding.scala • TodoMVC 项目主页
- Binding.scala • TodoMVC DEMO
- Binding.scala • TodoMVC 以外的其他 DEMO
- JavaScript 到 Scala.js 移植指南
- Scala.js 项目主页
- Scala API 参考文档
- Scala.js API 参考文档
- Scala.js DOM API 参考文档
- Binding.scala 快速上手指南
- Binding.scala API 参考文档
- Binding.scala 的 Gitter 聊天室
鸣谢
感谢张凯峰和郑培真帮我检阅了这一系列文章,你看到的文章的结构和内容许多地方都来自他们提出的修改意见。
More than React 系列文章
《More than React(一)为什么ReactJS 不适合复杂交互的前端项目?》
《More than React(四)HTML 也可以静态编译?》
作者简介
杨博是 Haxe 和 Scala 社区的活跃贡献者,发起和维护的开源项目包括 protoc-gen-as3 、 Stateless Future 、 haxe-continuation 、 Fastring 、 Each 、 Microbuilder 、 Binding.scala 。杨博曾在网易任主程序和项目经理,开发过多款游戏。现在 ThoughtWorks 任 Lead Consultant,为客户提供移动、互联网、大数据、人工智能和深度学习领域的解决方案。
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论