@atry
2016-09-25T00:22:07.000000Z
字数 6430
阅读 1902
前端
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 API 来实现标签编辑器,代码如下:
<!DOCTYPE html>
<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 <div>
外加两行 JavaScript 代码填入初始化数据。
HTML 文件中硬编码了几个 <div>
。这些<div>
本身并不是动态创建的,但可以作为容器,放置其他动态创建的元素。
代码中的函数来会把网页内容动态更新到这些 <div>
中。所以,如果要在同一个页面显示两个标签编辑器,id
就会冲突。因此,以上代码没有复用性。
就算用 jQuery 代替 DHTML API,代码复用仍然很难。为了复用 UI ,jQuery 开发者通常必须额外增加代码,在 onload
时扫描整个网页,找出具有特定 class
属性的元素,然后对这些元素进行修改。对于复杂的网页,这些 onload
时运行的函数很容易就会冲突,比如一个函数修改了一个 HTML 元素,常常导致另一处代码受影响而内部状态错乱。
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 中的最小复用单位是数据绑定表达式,即 @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开发的具体步骤。
此外,《More than React》系列第四篇文章《HTML也可以编译》还将列出Binding.scala所支持的完整HTML模板特性。
最后,下文将展示如何用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 请访问 ScalaFiddle。
在 Binding.scala 不需要像 ReactJS 那样编写 changeHandler
之类的回调函数。每当用户在 tagPicker
输入新的标签时,tags
就会改变,网页也就会自动随之改变。
对比 ReactJS 和 Binding.scala 的代码,可以发现以下区别:
tagPicker
这样的 @dom
方法表示 HTML 模板,而不需要组件概念。tags
这样的参数,而不需要 props
概念。state
概念。总的来说 Binding.scala 要比 ReactJS 精简不少。
如果你用过 ASP 、 PHP 、 JSP 之类的服务端网页模板语言,你会发现和 Binding.scala 的 HTML 模板很像。
使用 Binding.scala 一点也不需要函数式编程知识,只要把设计工具中生成的 HTML 原型复制到代码中,然后把会变的部分用花括号代替、把重复的部分用 for
/ yield
代替,网页就做好了。
本文对比了不同技术栈中实现和使用可复用的标签编辑器的难度。
原生 HTML | ReactJS | Binding.scala | |
---|---|---|---|
实现标签编辑器需要代码行数 | 45行 | 51行 | 17行 |
实现标签编辑器的难点 | 在代码中动态更新HTML页面太繁琐 | 实现组件的语法很笨重 | 无 |
使用标签编辑器并显示标签列表需要代码行数 | 难以复用 | 21行 | 8行 |
阻碍复用的难点 | 静态HTML元素难以模块化 | 交互组件之间层层传递回调函数过于复杂 | 无 |
Binding.scala 不发明“组件”之类的噱头,而以更轻巧的“方法”为最小复用单位,让编程体验更加顺畅,获得了更好的代码复用性。
《More than React》系列下一篇文章将比较 ReactJS 的虚拟 DOM 机制和 Binding.scala 的精确数据绑定机制,揭开 ReactJS 和 Binding.scala 相似用法背后隐藏的不同算法。