低代码到底是不是行业毒瘤?一线大厂怎么做的?戳此了解>>> 了解详情
写点什么

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

2016 年 8 月 31 日

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

标签编辑器的功能需求

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

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

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

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

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

原生 DHTML 版

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

复制代码
<html>
<head>
<script>
var tags = [];
function hasTag(tag) {
for (var i = 0; i < tags.length; i++) {
if (tags[i].tag == tag) {
return true;
}
}
return false;
}
function removeTag(tag) {
for (var i = 0; i < tags.length; i++) {
if (tags[i].tag == tag) {
document.getElementById("tags-parent").removeChild(tags[i].element);
tags.splice(i, 1);
return;
}
}
}
function addTag(tag) {
var element = document.createElement("q");
element.textContent = tag;
var removeButton = document.createElement("button");
removeButton.textContent = "x";
removeButton.onclick = function (event) {
removeTag(tag);
}
element.appendChild(removeButton);
document.getElementById("tags-parent").appendChild(element);
tags.push({
tag: tag,
element: element
});
}
function addHandler() {
var tagInput = document.getElementById("tag-input");
var tag = tagInput.value;
if (tag && !hasTag(tag)) {
addTag(tag);
tagInput.value = "";
}
}
</script>
</head>
<body>
<div id="tags-parent"></div>
<div>
<input id="tag-input" type="text"/>
<button onclick="addHandler()">Add</button>
</div>
<script>
addTag("initial-tag-1");
addTag("initial-tag-2");
</script>
</body>
</html>

为了实现标签编辑器的功能,我用了 45 行 JavaScript 代码来编写 UI 逻辑,外加若干的 HTML

外加两行 JavaScript 代码填入初始化数据。

HTML 文件中硬编码了几个

。这些
本身并不是动态创建的,但可以作为容器,放置其他动态创建的元素。

代码中的函数来会把网页内容动态更新到这些

中。所以,如果要在同一个页面显示两个标签编辑器,id 就会冲突。因此,以上代码没有复用性。

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

ReactJS 实现的标签编辑器组件

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

复制代码
class TagPicker extends React.Component {
static defaultProps = {
changeHandler: tags => {}
}
static propTypes = {
tags: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
changeHandler: React.PropTypes.func
}
state = {
tags: this.props.tags
}
addHandler = event => {
const tag = this.refs.input.value;
if (tag && this.state.tags.indexOf(tag) == -1) {
this.refs.input.value = "";
const newTags = this.state.tags.concat(tag);
this.setState({
tags: newTags
});
this.props.changeHandler(newTags);
}
}
render() {
return (
<section>
<div>{
this.state.tags.map(tag =>
<q key={ tag }>
{ tag }
<button onClick={ event => {
const newTags = this.state.tags.filter(t => t != tag);
this.setState({ tags: newTags });
this.props.changeHandler(newTags);
}}>x</button>
</q>
)
}</div>
<div>
<input type="text" ref="input"/>
<button onClick={ this.addHandler }>Add</button>
</div>
</section>
);
}
}

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

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

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

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

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

复制代码
class Page extends React.Component {
state = {
tags: [ "initial-tag-1", "initial-tag-2" ]
};
changeHandler = tags => {
this.setState({ tags });
};
render() {
return (
<div>
<TagPicker tags={ this.state.tags } changeHandler={ this.changeHandler }/>
<h3> 全部标签:</h3>
<ol>{ this.state.tags.map(tag => <li>{ tag }</li> ) }</ol>
</div>
);
}
}

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

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

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

Binding.scala 的基本用法

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

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

复制代码
// 两个 HTML 换行符
@dom def twoBr = <br/><br/>
// 一个 HTML 标题
@dom def myHeading(content: String) = <h1>{content}</h1>

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

复制代码
@dom def render = {
<div>
{ myHeading("Binding.scala 的特点 ").bind }
<p>
代码短
{ twoBr.bind }
概念少
{ twoBr.bind }
功能多
</p>
</div>
}

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

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

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

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

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

复制代码
@dom def tagPicker(tags: Vars[String]) = {
val input: Input = <input type="text"/>
val addHandler = { event: Event =>
if (input.value != "" && !tags.get.contains(input.value)) {
tags.get += input.value
input.value = ""
}
}
<section>
<div>{
for (tag <- tags) yield <q>
{ tag }
<button onclick={ event: Event => tags.get -= tag }>x</button>
</q>
}</div>
<div>{ input } <button onclick={ addHandler }>Add</button></div>
</section>
}

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

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

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

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

复制代码
@dom def render() = {
val tags = Vars("initial-tag-1", "initial-tag-2")
<div>
{ tagPicker(tags).bind }
<h3> 全部标签:</h3>
<ol>{ for (tag <- tags) yield <li>{ tag }</li> }</ol>
</div>
}

只要用 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 代替,网页就做好了。

结论

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

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

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

相关链接

More than React 系列文章

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

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

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

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

《More than React(五)异步编程真的好吗?》

作者简介

杨博是 Haxe 和 Scala 社区的活跃贡献者,发起和维护的开源项目包括 protoc-gen-as3 Stateless Future haxe-continuation Fastring Each Microbuilder Binding.scala 。杨博曾在网易任主程序和项目经理,开发过多款游戏。现在 ThoughtWorks 任 Lead Consultant,为客户提供移动、互联网、大数据、人工智能和深度学习领域的解决方案。


感谢张凯峰对本文的策划,韩婷对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 8 月 31 日 17:175084

评论

发布
暂无评论
发现更多内容

有益思考一则:框架性思维

石君

学习 方法论

一起学MySQL性能优化

xcbeyond

MySQL 性能优化 MySQL性能优化

跨过语言银河,构筑智能鹊桥:百度NLP的十年、今夕与未来

脑极体

架构师第十一周作业及总结

傻傻的帅

Spring系列篇:Spring容器基本使用及原理

简爱W

微服务的基建工作

看山

微服务 基础设施

如何在面试中表现你所没有的能力

escray

学习 面试 面试现场

沟通是一门艺术

石云升

情绪控制 沟通艺术

架构师训练营第十一周作业

张明森

架构师训练营第11周作业

Bruce Xiong

开源流数据公司 StreamNative 推出 Pulsar 云服务,推进企业“流优先”进程

Apache Pulsar

Apache Pulsar 消息系统 消息中间件

分手快乐 祝你快乐 你可以找到更好的

escray

学习 面试 面试现场

区块链支付系统开发方案,usdt支付跑分系统搭建

WX13823153201

区块链支付系统开发

架构师训练营第十一周总结

张明森

游戏夜读 | 什么才值得纪念?

game1night

oeasy教您玩转linux010105详细手册man

o

性能相关,进程调度

Linuxer

ArCall 升级丨新增多项功能,可支持多人在线语音

anyRTC开发者

音视频 WebRTC 直播 RTC

第二周作业

Vincent

极客时间 作业

架构师训练营 -- 第11周作业

stardust20

Apache 软件基金会顶级项目 Pulsar 达成新里程碑:全球贡献者超 300 位!

Apache Pulsar

Apache Apache Pulsar 消息系统 消息中间件

薪水真的不是工作的全部

escray

学习 面试 面试现场

Flink状态管理-8

小知识点

大数据 flink scal

第二周学习总结

Vincent

极客时间 极客大学 作业

安全系列之——主流Hash散列算法介绍和使用

诸葛小猿

hash 散列函数 md5 sha1 murmurhash

大数据技术思想入门(五):分布式计算特点

抖码算法

Java 大数据 hadoop 分布式

week11 作业

Geek_196d0f

微服务编程范式

看山

微服务 范式

论商品促销代码的优雅性

架构师修行之路

week11 小结

Geek_196d0f

计算机网络基础(二十一)---传输层-TCP连接的四次挥手

书旅

TCP 四次挥手 TCP/IP 协议族

2021 ThoughtWorks 技术雷达峰会

2021 ThoughtWorks 技术雷达峰会

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