More than React(二)组件对复用性有害?

释放双眼,带上耳机,听听看~!

本文为『前端之巅』特供稿件,未经许可,拒绝任何形式的转载。申请入群请关注『前端之巅』公众号并发送“加群”。

本系列的上一篇文章《为什么ReactJS不适合复杂的前端项目》列举了前端开发中的种种痛点
(关注『前端之巅』公众号,发送“杨博”,查看More than React系列文章)

本篇文章中将详细探讨其中“复用性”痛点。我们将用原生 DHTML API 、 ReactJS 和 Binding.scala 实现同一个需要复用的标签编辑器,然后比较三个标签编辑器哪个实现难度更低,哪个更好用。

>>>> 1. 标签编辑器的功能需求

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

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

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

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

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

>>>> 2. 原生 DHTML 版

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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>
60

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

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

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

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

>>>> 3. ReactJS 实现的标签编辑器组件

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


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

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

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

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

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

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


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

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

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

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

>>>> 4. Binding.scala 的基本用法

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

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


1
2
3
1// 两个 HTML 换行符
2@dom def twoBr = &lt;br/&gt;&lt;br/&gt;
3

1
2
3
1// 一个 HTML 标题
2@dom def myHeading(content: String) = &lt;h1&gt;{content}&lt;/h1&gt;
3

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


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

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

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

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

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

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


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

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

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

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

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


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

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

完整的 DEMO 请访问 https://thoughtworksinc.github.io/Binding.scala/\#4。

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

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

  • Binding.scala 的开发者可以用类似 tagPicker 这样的 @dom 方法表示 HTML 模板,而不需要组件概念。

  • Binding.scala 的开发者可以在方法之间传递 tags 这样的参数,而不需要 props 概念。

  • Binding.scala 的开发者可以在方法内定义局部变量表示状态,而不需要 state 概念。

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

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

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

>>>> 6. 结论

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

实现标签编辑器需要代码行数
45行
51行
17行
实现标签编辑器的难点
在代码中动态更新HTML页面太繁琐
实现组件的语法很笨重

使用标签编辑器并显示标签列表需要代码行数
难以复用
21行
8行
阻碍复用的难点
静态HTML元素难以模块化
交互组件之间层层传递回调函数过于复杂

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

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

>>>> 7. 相关链接

More than React(二)组件对复用性有害?

More than React(二)组件对复用性有害?

长按二维码关注

**        
前  端  之  巅**


        紧 跟 前 端 发 展

        共 享 一 线 技 术        不 断 学 习 进 步        攀 登 前 端 之 巅

给TA打赏
共{{data.count}}人
人已打赏
安全技术

base64加密解密

2021-8-18 16:36:11

安全技术

C++ 高性能服务器网络框架设计细节

2022-1-11 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索