[关闭]
@atry 2016-09-24T16:22:07.000000Z 字数 6430 阅读 1704

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

前端 binding.scala data-binding scala.js


《More than React》系列的上一篇文章《为什么ReactJS不适合复杂的前端项目》列举了前端开发中的种种痛点。本篇文章中将详细探讨其中“复用性”痛点。我们将用原生 DHTML API 、 ReactJS 和 Binding.scala 实现同一个需要复用的标签编辑器,然后比较三个标签编辑器哪个实现难度更低,哪个更好用。

标签编辑器的功能需求

在InfoQ的许多文章都有标签。比如本文的标签是“binding.scala”、“data-binding”、“scala.js”。

假如你要开发一个博客系统,你也希望博客作者可以添加标签。所以你可能会提供标签编辑器供博客作者使用。

如图所示,标签编辑器在视觉上分为两行。

标签编辑器

第一行展示已经添加的所有标签,每个标签旁边有个“x”按钮可以删除标签。第二行是一个文本框和一个“Add”按钮可以把文本框的内容添加为新标签。每次点击“Add”按钮时,标签编辑器应该检查标签是否已经添加过,以免重复添加标签。而在成功添加标签后,还应清空文本框,以便用户输入新的标签。

除了用户界面以外,标签编辑器还应该提供 API 。标签编辑器所在的页面可以用 API 填入初始标签,也可以调用 API 随时增删查改标签。如果用户增删了标签,应该有某种机制通知页面的其他部分。

原生 DHTML 版

首先,我试着不用任何前端框架,直接调用原生的 DHTML API 来实现标签编辑器,代码如下:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <script>
  5. var tags = [];
  6. function hasTag(tag) {
  7. for (var i = 0; i < tags.length; i++) {
  8. if (tags[i].tag == tag) {
  9. return true;
  10. }
  11. }
  12. return false;
  13. }
  14. function removeTag(tag) {
  15. for (var i = 0; i < tags.length; i++) {
  16. if (tags[i].tag == tag) {
  17. document.getElementById("tags-parent").removeChild(tags[i].element);
  18. tags.splice(i, 1);
  19. return;
  20. }
  21. }
  22. }
  23. function addTag(tag) {
  24. var element = document.createElement("q");
  25. element.textContent = tag;
  26. var removeButton = document.createElement("button");
  27. removeButton.textContent = "x";
  28. removeButton.onclick = function (event) {
  29. removeTag(tag);
  30. }
  31. element.appendChild(removeButton);
  32. document.getElementById("tags-parent").appendChild(element);
  33. tags.push({
  34. tag: tag,
  35. element: element
  36. });
  37. }
  38. function addHandler() {
  39. var tagInput = document.getElementById("tag-input");
  40. var tag = tagInput.value;
  41. if (tag && !hasTag(tag)) {
  42. addTag(tag);
  43. tagInput.value = "";
  44. }
  45. }
  46. </script>
  47. </head>
  48. <body>
  49. <div id="tags-parent"></div>
  50. <div>
  51. <input id="tag-input" type="text"/>
  52. <button onclick="addHandler()">Add</button>
  53. </div>
  54. <script>
  55. addTag("initial-tag-1");
  56. addTag("initial-tag-2");
  57. </script>
  58. </body>
  59. </html>

为了实现标签编辑器的功能,我用了 45 行 JavaScript 代码来编写 UI 逻辑,外加若干的 HTML <div> 外加两行 JavaScript 代码填入初始化数据。

HTML 文件中硬编码了几个 <div>。这些<div> 本身并不是动态创建的,但可以作为容器,放置其他动态创建的元素。

代码中的函数来会把网页内容动态更新到这些 <div> 中。所以,如果要在同一个页面显示两个标签编辑器,id 就会冲突。因此,以上代码没有复用性。

就算用 jQuery 代替 DHTML API,代码复用仍然很难。为了复用 UI ,jQuery 开发者通常必须额外增加代码,在 onload 时扫描整个网页,找出具有特定 class 属性的元素,然后对这些元素进行修改。对于复杂的网页,这些 onload 时运行的函数很容易就会冲突,比如一个函数修改了一个 HTML 元素,常常导致另一处代码受影响而内部状态错乱。

ReactJS 实现的标签编辑器组件

ReactJS 提供了可以复用的组件,即 React.Component 。如果用 ReactJS 实现标签编辑器,大概可以这样写:

  1. class TagPicker extends React.Component {
  2. static defaultProps = {
  3. changeHandler: tags => {}
  4. }
  5. static propTypes = {
  6. tags: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
  7. changeHandler: React.PropTypes.func
  8. }
  9. state = {
  10. tags: this.props.tags
  11. }
  12. addHandler = event => {
  13. const tag = this.refs.input.value;
  14. if (tag && this.state.tags.indexOf(tag) == -1) {
  15. this.refs.input.value = "";
  16. const newTags = this.state.tags.concat(tag);
  17. this.setState({
  18. tags: newTags
  19. });
  20. this.props.changeHandler(newTags);
  21. }
  22. }
  23. render() {
  24. return (
  25. <section>
  26. <div>{
  27. this.state.tags.map(tag =>
  28. <q key={ tag }>
  29. { tag }
  30. <button onClick={ event => {
  31. const newTags = this.state.tags.filter(t => t != tag);
  32. this.setState({ tags: newTags });
  33. this.props.changeHandler(newTags);
  34. }}>x</button>
  35. </q>
  36. )
  37. }</div>
  38. <div>
  39. <input type="text" ref="input"/>
  40. <button onClick={ this.addHandler }>Add</button>
  41. </div>
  42. </section>
  43. );
  44. }
  45. }

以上 51 行 ECMAScript 2015 代码实现了一个标签编辑器组件,即TagPicker。虽然代码量比 DHTML 版长了一点点,但复用性大大提升了。

如果你不用 ECMAScript 2015 的话,那么代码还会长一些,而且需要处理一些 JavaScript 的坑,比如在回调函数中用不了 this

ReactJS 开发者可以随时用 ReactDOM.render 函数把 TagPicker 渲染到任何空白元素内。此外,ReactJS 框架可以在 stateprops 改变时触发 render ,从而避免了手动修改现存的 DOM。

如果不考虑冗余的 key 属性,单个组件内的交互 ReactJS 还算差强人意。但是,复杂的网页结构往往需要多个组件层层嵌套,这种父子组件之间的交互,ReactJS 就很费劲了。

比如,假如需要在 TagPicker 之外显示所有的标签,每当用户增删标签,这些标签也要自动更新。要实现这个功能,需要给 TagPicker 传入 changeHandler 回调函数,代码如下:

  1. class Page extends React.Component {
  2. state = {
  3. tags: [ "initial-tag-1", "initial-tag-2" ]
  4. };
  5. changeHandler = tags => {
  6. this.setState({ tags });
  7. };
  8. render() {
  9. return (
  10. <div>
  11. <TagPicker tags={ this.state.tags } changeHandler={ this.changeHandler }/>
  12. <h3>全部标签:</h3>
  13. <ol>{ this.state.tags.map(tag => <li>{ tag }</li> ) }</ol>
  14. </div>
  15. );
  16. }
  17. }

为了能触发页面其他部分更新,我被迫增加了一个 21 行代码的 Page 组件。

Page 组件必须实现 changeHandler 回调函数。每当回调函数触发,调用 Page 自己的 setState 来触发 Page 重绘。

从这个例子,我们可以看出, ReactJS 可以简单的解决简单的问题,但碰上层次复杂、交互频繁的网页,实现起来就很繁琐。使用 ReactJS 的前端项目充满了各种 xxxHandler 用来在组件中传递信息。我参与的某海外客户项目,平均每个组件大约需要传入五个回调函数。如果层次嵌套深,创建网页时,常常需要把回调函数从最顶层的组件一层层传入最底层的组件,而当事件触发时,又需要一层层把事件信息往外传。整个前端项目有超过一半代码都在这样绕圈子。

Binding.scala 的基本用法

在讲解 Binding.scala 如何实现标签编辑器以前,我先介绍一些 Binding.scala 的基础知识:

Binding.scala 中的最小复用单位是数据绑定表达式,即 @dom 方法。每个 @dom 方法是一段 HTML 模板。比如:

  1. // 两个 HTML 换行符
  2. @dom def twoBr = <br/><br/>
  1. // 一个 HTML 标题
  2. @dom def myHeading(content: String) = <h1>{content}</h1>

每个模板还可以使用bind语法包含其他子模板,比如:

  1. @dom def render = {
  2. <div>
  3. { myHeading("Binding.scala的特点").bind }
  4. <p>
  5. 代码短
  6. { twoBr.bind }
  7. 概念少
  8. { twoBr.bind }
  9. 功能多
  10. </p>
  11. </div>
  12. }

你可以参见附录:Binding.scala快速上手指南,学习上手Binding.scala开发的具体步骤。

此外,《More than React》系列第四篇文章《HTML也可以编译》还将列出Binding.scala所支持的完整HTML模板特性。

Binding.scala实现的标签编辑器模板

最后,下文将展示如何用Binding.scala实现标签编辑器。

标签编辑器要比刚才介绍的HTML模板复杂,因为它不只是静态模板,还包含交互。

  1. @dom def tagPicker(tags: Vars[String]) = {
  2. val input: Input = <input type="text"/>
  3. val addHandler = { event: Event =>
  4. if (input.value != "" && !tags.get.contains(input.value)) {
  5. tags.get += input.value
  6. input.value = ""
  7. }
  8. }
  9. <section>
  10. <div>{
  11. for (tag <- tags) yield <q>
  12. { tag }
  13. <button onclick={ event: Event => tags.get -= tag }>x</button>
  14. </q>
  15. }</div>
  16. <div>{ input } <button onclick={ addHandler }>Add</button></div>
  17. </section>
  18. }

这个标签编辑器的 HTML 模板一共用了 18 行代码就实现好了。

标签编辑器中需要显示当前所有标签,所以此处用tags: Vars[String]保存所有的标签数据,再用for/yield循环把tags中的每个标签渲染成UI元素。

Vars 是支持数据绑定的列表容器,每当容器中的数据发生改变,UI就会自动改变。所以,在x按钮中的onclick事件中删除tags中的数据时,页面上的标签就会自动随之消失。同样,在Add按钮的onclick中向tags中添加数据时,页面上也会自动产生对应的标签。

Binding.scala不但实现标签编辑器比 ReactJS 简单,而且用起来也比 ReactJS 简单:

  1. @dom def render() = {
  2. val tags = Vars("initial-tag-1", "initial-tag-2")
  3. <div>
  4. { tagPicker(tags).bind }
  5. <h3>全部标签:</h3>
  6. <ol>{ for (tag <- tags) yield <li>{ tag }</li> }</ol>
  7. </div>
  8. }

只要用 9 行代码另写一个 HTML 模板,在模板中调用刚才实现好的 tagPicker 就行了。

完整的 DEMO 请访问 ScalaFiddle

在 Binding.scala 不需要像 ReactJS 那样编写 changeHandler 之类的回调函数。每当用户在 tagPicker 输入新的标签时,tags 就会改变,网页也就会自动随之改变。

对比 ReactJS 和 Binding.scala 的代码,可以发现以下区别:

总的来说 Binding.scala 要比 ReactJS 精简不少。

如果你用过 ASP 、 PHP 、 JSP 之类的服务端网页模板语言,你会发现和 Binding.scala 的 HTML 模板很像。

使用 Binding.scala 一点也不需要函数式编程知识,只要把设计工具中生成的 HTML 原型复制到代码中,然后把会变的部分用花括号代替、把重复的部分用 for / yield 代替,网页就做好了。

结论

本文对比了不同技术栈中实现和使用可复用的标签编辑器的难度。

原生 HTMLReactJSBinding.scala
实现标签编辑器需要代码行数 45行 51行 17行
实现标签编辑器的难点 在代码中动态更新HTML页面太繁琐 实现组件的语法很笨重
使用标签编辑器并显示标签列表需要代码行数 难以复用 21行 8行
阻碍复用的难点 静态HTML元素难以模块化 交互组件之间层层传递回调函数过于复杂

Binding.scala 不发明“组件”之类的噱头,而以更轻巧的“方法”为最小复用单位,让编程体验更加顺畅,获得了更好的代码复用性。

《More than React》系列下一篇文章将比较 ReactJS 的虚拟 DOM 机制和 Binding.scala 的精确数据绑定机制,揭开 ReactJS 和 Binding.scala 相似用法背后隐藏的不同算法。

相关链接

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注