重要要点
- 用本机HTML实现的组件几乎不可重用,并且该组件的实现可能很复杂。
- 与原生HTML相比,ReactJS具有更好的可重用性,尽管重用组件的语法不必要。
- Binding.Scala中的最小重用单元是一种普通方法。
- Binding.Scala在实现具有更高可重用性的组件方面是先进的。
- Binding.Scala中较轻的重用单元可带来更流畅的编程体验。
在“ 不仅仅是React”系列的最后一篇文章中, “不仅仅是React:为什么不应该在复杂的交互式前端项目中使用ReactJS”,第一部分 ,我们列出了前端开发中的所有痛点。
在本文中,我们将详细讨论其中一个难点,即可重用性。 为了进行比较,我们将使用本地DHTML API,ReactJS和Binding.scala来实现可重用的标签编辑器组件,从中我们将比较实现的难度和便利性。
标签编辑器的要求
假设我们正在计划实施博客系统。 像大多数博客系统一样,我们希望允许作者在其文章中添加标签。
在这种情况下,我们将为作者提供标签编辑器。
如图所示,标签编辑器需要两个UI行。
第一行显示了所有添加的标签,每个标签旁边都有一个“ x”按钮,用于删除。 第二行是带有“添加”按钮的文本编辑器,该按钮创建带有文本编辑器中内容的新标签。 每次单击“添加”时,标签编辑器应通过检查标签是否已存在来避免重复。 成功添加标签后,应清除文本编辑器以准备下一次输入。
除UI外,标签编辑器还应提供API。 包含标签编辑器的网页应该能够使用API填写初始标签。 如果用户添加或删除标签,则页面的其余部分应具有通知机制。
本机DHTML版本
首先,让我们使用本机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>
为了实现标签编辑器的功能,除了几个HTML <div>元素和两行JavaScript代码用于填充初始数据外,它还需要45行JavaScript代码用于UI逻辑。
HTML文件包含几个硬编码的<div>元素,这些元素用作其他动态创建的组件的容器。
该代码将动态更新这些<div>元素中的网站内容,因此,当我们在一个页面中需要两个或多个标签编辑器时,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代码和JSX实现了标签编辑器组件TagPicker。 尽管它比DHTML需要更多的代码行,但它具有更高的可重用性。
如果您不使用ECMAScript 2015,则代码行数会更大。此外,您还必须处理JavaScript中的一些陷阱,例如无法在回调函数中使用this
。
ReactJS开发人员可以随时使用ReactDOM.render函数将TagPicker渲染为任何空元素。 而且,每当状态和属性发生变化时,ReactJS框架都会触发render函数,从而节省了修改现有DOM的手动工作。
如果我们把重复的key属性放在一边,ReactJS可以处理单个组件的内部交互。 但是,复杂的网页体系结构往往需要多层嵌套组件。 在父子组件之间进行交互的情况下,对于ReactJS来说将更加糟糕。
例如,如果我们需要显示TagPicker之外的所有标签,并且当用户删除或添加标签时,这些标签应自动同步。 为了实现这一点,我们需要将changeHandler回调传递给TagPicker,如下所示:
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>All tags:</h3>
<ol>{ this.state.tags.map(tag => <li>{ tag }</li> ) }</ol>
</div>
);
}
}
在新创建的Page组件中,应该有一个changeHandler回调函数,该函数在内部调用Page的setState以便触发Page的重新渲染。
在上面的示例中,我们注意到ReactJS可以简单地解决简单的问题。 但是在具有复杂层和频繁交互的网页上,实现变得复杂。 一个使用ReactJS的前端项目在整个地方都有xxxHandler,仅用于传递消息。 根据我在海外客户项目中的经验,每个组件平均需要大约五个回调。 为了更深入地嵌套多层,我们需要在创建网页时将回调函数从根组件传递到叶子的每一层传递。 反之亦然,触发事件时,我们需要将事件消息逐层传递出去。 整个前端项目中至少有一半的代码是这样的简单样板代码。
Binding.scala的基本用法
在我们开始使用Binding.scala实现标签编辑器之前,我想介绍一些Binding.scala基础知识。
Binding.scala中最小的可重用单元是数据绑定表达式,即@dom方法。 每个@dom方法都是一块HTML模板,例如:
// Two HTML line breaks
@dom def twoBr = <br/><br/>
// An HTML heading
@dom def myHeading(content: String) = <h1>{content}</h1>
每个模板可以使用绑定语法包含其他子模板,例如:
@dom def render = {
<div>
{ myHeading("Feature of Binding.scala").bind }
<p>
Shorter code
{ twoBr.bind }
Less concepts
{ twoBr.bind }
More functionality
</p>
</div>
}
请查看附录:Binding.scala快速入门教程,以获取有关学习Binding.scala的详细信息。
另外,第四篇文章不仅仅是React IV:如何静态编译HTML? “ 不仅仅是React”系列文章中的,将列出Binding.scala支持HTML模板的所有功能。
使用Binding.scala实现标签编辑器模板
现在,我们将向您展示如何使用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行。
由于标签编辑器需要显示所有标签,因此我们使用tabs:Vars [String]保存标签数据,然后使用for / yield循环将标签中的每个标签呈现到UI组件中。
Vars是一个支持数据绑定的列表容器。 当列表容器中的数据更改时,UI也会自动更改。 因此,当x按钮上的onclick事件删除标签中的数据时,显示的标签将相应消失; 反之亦然。
Binding.scala不仅简化了标签编辑器的实现,而且简化了用法。
val tags = Vars("initial-tag-1", "initial-tag-2")
<div>
{ tagPicker(tags).bind }
<h3>All tags:</h3>
<ol>{ for (tag <- tags) yield <li>{ tag }</li> }</ol>
</div>
}
您需要做的就是在9行中编写另一个HTML模板,并在其中调用现有的tagPicker。
有关演示代码的完整版本,请参考ScalaFiddle
Binding.scala不需要像ReactJS中的changeHandler一样的回调。 当变量Var更改时,页面将自动反映更改。
比较ReactJS和Binding.scala之间的代码后,我们发现以下区别:
- 使用Binding.scala的开发人员可以使用@dom函数(例如tagPicker)来提供HTML模板,而不使用组件。
- 使用Binding.scala的开发人员可以在函数之间传递诸如标签之类的参数,而无需使用props概念。
- 使用Binding.scala的开发人员可以在函数内部使用局部变量,而不是使用状态。
通常,Binding.scala比ReactJS简洁得多。
如果您碰巧具有服务器端网页模板语言(例如ASP,PHP,JSP)的经验,那么您会发现Binding.scala和HTML模板与它们非常相似。
使用Binding.scala不需要任何有关函数编程的知识。 所需要做的就是将设计工具生成HTML原型复制到代码中,然后用大括号替换动态部分,将重复部分替换为for / yield,仅此而已。
结论
本文比较了实现和使用具有不同技术堆栈的可重用标签编辑器的难度。
本机HTML | ReactJS | 绑定标量 | |
实施标签编辑器所需的代码行 | 45个LOC | 51个 | 18位 |
实施过程中遇到的困难 | 动态更新HTML页面的实现太复杂了。 | 用于实现组件的语法太繁重 | 没有 |
重用标签编辑器并显示标签列表所需的代码行 | 难以重用 | 21个LOC | 8位 |
重用阻止器 | 难以模块化静态HTML元素 | 要通过多层传递回调函数太复杂了。 | 没有 |
Binding.scala不会尝试重新发明“组件”之类的概念。 相反,我们建议您将普通的“方法”创建为最小的重用单元,因为重量轻的重用单元将带来更流畅的编程体验,并增加可重用性。
在“ 不仅仅是React”系列的下一篇文章中,我们将把ReactJS的虚拟DOM机制与Binding.scala的精确数据绑定机制进行比较,在相似用途的单板下揭示不同的算法。
参考
- Binding.scala项目页面
- Binding.scala•TodoMVC项目页面
- Binding.scala•TodoMVC演示
- Binding.scala•TodoMVC其他演示
- 从JavaScript到Scala.js指南
- Scala.js项目页面
- Scala API参考
- Scala.js API参考
- Scala.js DOM API参考
- Binding.scala初学者指南
- Binding.scala API参考
- Binding.scala的Gitter聊天室