本文关于React的所有代码出自React Tutorial。关于案例本身以及React的中文解说出自React 中文社区对React Tutorial的翻译,略有改动。其余部分为本人原创。上述相关方以及各位读者如果觉得本文有侵权或其它不妥之处,敬请告知taijiweeb@gmail.com,本人将做适当修改,谢谢。

构造一个评论组件

我们将构建一个简单却真实的评论框,你可以将它放入你的博客,类似于 disqus 、 livefyre 、 facebook 中实时评论框,包含一些基础功能。我们将提供以下功能:

  • 展示所有评论的界面
  • 发布评论的表单
  • 后端 API 接口地址,该接口需要自己实现

同时也包含一些简洁的特性:

  • 发布评论的体验优化: 评论在保存到服务器之前就展现在评论列表,因此感觉起来很快。
  • 实时更新: 其他用户的评论将会实时展示在评论框中。
  • Markdown格式: 用户可以使用Markdown语法来编辑评论文本。

运行一个服务器

为了开始本教程,我们将需要一个运行的服务器,提供一个 API 接口,仅用于获取和保存评论数据。为了尽量简便,我们已经准备好了几种脚本语言开发的简单服务器程序,完成了我们需要的功能。你可以查看源码或者下载 zip 文件,里面包含了开始学习教程所需的所有东西。

为了简单,服务器将会使用一个 JSON 文件作为数据库。不能在生产环境中采用这种做法,但是这样做确实简化了模拟后端 API 的工作。一旦启动了这个服务器,它就能提供我们需要的 API 接口,同时也能生成并发送我们需要的页面。


设置html页面

本教程会尽量简单。上述讨论提及的页面是一个 HTML 文件,它包含在服务器源码包中,我们先看看这个文件。使用你最喜欢的编辑器打开 public/index.html ,然后会看见这些内容:

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Tutorial</title>
<script src="https://npmcdn.com/react@15.3.0/dist/react.js"></script>
<script src="https://npmcdn.com/react-dom@15.3.0/dist/react-dom.js"></script>
<script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script>
<script src="https://npmcdn.com/jquery@3.1.0/dist/jquery.min.js"></script>
<script src="https://npmcdn.com/remarkable@1.6.2/dist/remarkable.min.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/babel" src="scripts/example.js"></script>
<script type="text/babel">
// 在本教程的剩下部分,我们会在这个 script 标签中编写我们的 JavaScript 代码。
// 我们没有任何高端的页面自动加载刷新工具,所以在修改了代码之后,需要手动刷新浏览器来查看变化。
// 下面,启动服务器,在浏览器中打开 http://localhost:3000 。
// 在没对源码进行修改的前提下,第一次打开这个 URL 会看到我们计划构建的应用的一个最终样子。
// 当你完成本教程,需要使用外部javascript文件的时候,只需要删掉这个 `<script>` 标签就可以了。
</script>
</body>
</html>

注意

因为我们想简化 ajax 请求代码,所以在这里引入 jQuery,但是 React 并不依赖于 jQuery 。

Domcom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Domcom Tutorial</title>
<script src="http://cdn.jsdelivr.net/domcom/0.5.2/domcom.min.js"></script>
<script src="https://npmcdn.com/jquery@3.1.0/dist/jquery.min.js"></script>
<!-- domcom 可以很方便地使用 ES5 原生代码来编写。因此并不需要包含babel -->
<!-- script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script -->
<script src="https://npmcdn.com/remarkable@1.6.2/dist/remarkable.min.js"></script>
</head>
<body>
<div id="content"></div>
<script src="scripts/example.js"></script>
<script>
// 在本教程的剩下部分,我们会在这个 script 标签中编写我们的 JavaScript 代码。
// 我们没有任何高端的页面自动加载刷新工具,所以在修改了代码之后,需要手动刷新浏览器来查看变化。
// 下面,启动服务器,在浏览器中打开 http://localhost:3000 。
// 在没对源码进行修改的前提下,第一次打开这个 URL 会看到我们计划构建的应用的一个最终样子。
// 当你完成本教程,需要使用外部javascript文件的时候,只需要删掉这个 `<script>` 标签就可以了。
</script>
</body>
</html>

因为Domcom所有的api都在dc名字空间上,因此使用dc本身并不要求使用babel之类的工具。本教程的Domcom代码仅使用了两项ES6特征:解构赋值和const关键词。因此只要把解构赋值语句用ES5语法重写,将const关键词用var替换,代码即已经符合ES5标准。当然,使用ES6会让代码更简洁一些,使用babel预编译还是鼓励的。

为行文方便,文中涉及的dc名字空间的API名称并没有一一声明和导入。这些名字包括div, span, h1, h2, input, text, html, $model, flow(flow.bind, flow.bindings)等。实际编码时需要根据ES6或ES5的不同语法加以处理。请留意。

注意

因为我们想简化 ajax 请求代码,所以在这里引入 jQuery,但是 Domcom 并不依赖于任何其它库,也包括 jQuery 。


第一个部件

React

React中全是模块化、可组装的组件。

React框架不支持OOP编程范式。虽然React有createClass,或者extends Comopnent,但是所有React组件都只能平坦地扩展Component,因为在React框架下无法按照类继承的方式扩展部件,不存在什么类继承层次体系。

Domcom

Domcom也是基于部件的框架。所有部件都是模块化,可组装的,组合性更胜于React的组件。另外还有一个显著不同,Domcom部件支持通过OOP方式进行扩展。

为了区别于React,Domcom将使用部件而不是组件这一概念。

可以看到,本教程中Domcom表现得比React更加贴近函数式编程,没有体现出任何与OOP有关的特征,因此必须在这里特别地强调OOP的重要性。在GUI领域,OOP是非常重要的。只要熟悉GUI编程(比如MFC,WPF,Qt,WxWidgets, Android UI,cocoa等等)的人都应该理解这一点,不被某些错误的言论误导。在web前端开发领域就有很多这方面的错误言论。不得不说,出于掩护自身设计缺陷和推广框架的原因,React社区也是推波助澜者之一。

对于我们的评论框例子,我们将有如下的React组件(或者Domcom部件)结构:

1
2
3
4
- CommentBox
- CommentList
- Comment
- CommentForm

React

让我们构造 CommentBox 组件,它只是一个简单的 <div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tutorial1.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);

注意,原生 HTML 元素名以小写字母开头,而自定义的 React 类名以大写字母开头。

不用JSX

首先你注意到 JavaScript 代码中 XML 式的语法语句。我们有一个简单的预编译器,用于将这种语法糖转换成纯 JavaScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
render: function() {
return (
React.createElement('div', {className: "commentBox"},
"Hello, world! I am a CommentBox."
)
);
}
});
ReactDOM.render(
React.createElement(CommentBox, null),
document.getElementById('content')
);

JSX 语法是可选的,但是我们发现 JSX 语句比纯 JavaScript 用起来更容易。更多内容请阅读《 JSX 语法介绍》。

  • 发生了什么

我们通过 JavaScript 对象传递一些方法到 React.createClass() 来创建一个新的 React 组件。其中最重要的方法是 render ,该方法返回一棵 React 组件树,这棵树最终将会渲染成 HTML 。
这里的 div 标签不是真正的 DOM 节点;他们是 React div 组件的实例。你可以认为这些标签就是一些标记或者数据, React 知道如何处理它们。React 是安全的。我们不生成 HTML 字符串,因此默认阻止了 XSS 攻击。
没必要返回基本的 HTML 结构,可以返回一个你(或者其他人)创建的组件树。而这就是让 React 变得组件化 的特性:一个前端可维护性的关键原则。

ReactDOM.render() 实例化根组件,启动框架,把标记注入到第二个参数指定的原生的 DOM 元素中。
ReactDOM 模块提供了一些 DOM 相关的方法,而 React 模块包含了 React 团队分享的不同平台上的核心工具(例如, React Native )。

Domcom

1
2
3
4
5
const {div} = dc;
var commentBox = div({className: "commentBox"},
"Hello, world! I am a CommentBox."
);
commentBox.mount();
  • Domcom无需JSX,代码更为简洁优雅。

Domcom框架在设计时致力于提供简洁优雅的Javascript语言API,以避免依赖于XML风格的语法。作为Domcom的框架作者,我认为,在框架已经以Javascript代码形式的部件完全掌控了几乎所有页面内容的情况下,以JSX而不是API的角度来解决问题,这是观念的落后和妥协,导致的是丑陋的代码、复杂的工具链以及诸多相关的问题,并不是完全必要的。本教程的代码对比印证了这一点。事实上,domcom的代码不管以coffee-script,ES6, ES5编写在简洁程度上都能胜过jsx。当然,domcom也是可以提供类xml风格的模板的。但是我认为这种观念的转变是更加重要的。


制作组件

让我们为 CommentList 和 CommentForm 搭建骨架,它们也是由一些简单的 <div> 组成。将这两个组件的代码添加到你的源码文件中,保留已有的 CommentBox 声明和 ReactDOM.render 调用:

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// tutorial2.js
var CommentList = React.createClass({
render: function() {
return (
<div className="commentList">
Hello, world! I am a CommentList.
</div>
);
}
});
var CommentForm = React.createClass({
render: function() {
return (
<div className="commentForm">
Hello, world! I am a CommentForm.
</div>
);
}
});

Domcom

1
2
3
4
5
6
7
const {div} = dc;
const commentList = div({className: "commentList"},
"Hello, world! I am a CommentList."
);
const CommentForm = div({className: "commentForm"},
"Hello, world! I am a commentForm."
);

接下来,使用新创建的组件更新 CommentBox 组件代码:

React

1
2
3
4
5
6
7
8
9
10
11
12
// tutorial3.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList />
<CommentForm />
</div>
);
}
});

注意我们是如何整合 HTML 标签和我们所创建的组件的。 HTML 组件就是普通的 React 组件,就和你定义的组件一样,只不过有一处不同。 JSX 编译器会自动重写 HTML 标签为 React.createElement(tagName) 表达式,其它什么都不做。这是为了避免全局命名空间污染。

Domcom

1
2
3
4
5
const commentBox = div({className: "commentBox"},
h1("Comments"),
commentList,
commentForm
);

传递数据

React

在React中数据是通过props和state来管理的。不变的数据用props,变化的数据用state。

1
2
3
4
5
6
7
8
9
10
11
12
13
// tutorial4.js
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
});

在 JSX 中,通过使用大括号包住一个 JavaScript 表达式(例如作为属性或者儿子节点),你可以在树结构中生成文本或者 React 组件。我们通过 this.props 来访问传入组件的数据,键名就是对应的命名属性,也可以通过 this.props.children 访问组件内嵌的任何元素。

Domcom

Domcom中可以直接通过函数参数传递数据。

对于AngularJS,ReactJS的数据传递机制(AngularJS是耦合于Scope的层次,ReactJS是耦合于组件的props和state层次),我联想到TJ Holowaychuk关于AngularJS模块设计机制的疑问:Why not just require?类似地,为什么不简单地利用函数及其参数来传递数据呢?这难道不是各种编程语言都提供的最普遍最自然的构造吗?Domcom正是充分利用这种方法实现了更好的数据传递与数据管理,实现了view和controllor以及model之间更充分的解耦,消除了层次化耦合数据这一重大的限制,从而实现了更灵活、更强大、解耦更彻底的数据传递和数据管理,能更好地支持MVC、MVVM或者flux等设计模式。

1
2
3
4
5
6
function makeComment(author, content) {
return div({className: "comment"},
h2({className: "commentAuthor"}, author),
content
);
}

组件属性

React

现在我们定义了 Comment 组件,我们想传递给它作者名字和评论文本,以便于我们能够对每一个独立的评论重用相同的代码。首先让我们添加一些评论到 CommentList :

1
2
3
4
5
6
7
8
9
10
11
// tutorial5.js
var CommentList = React.createClass({
render: function() {
return (
<div className="commentList">
<Comment author="Pete Hunt">This is one comment</Comment>
<Comment author="Jordan Walke">This is *another* comment</Comment>
</div>
);
}
});

请注意,我们从父 CommentList 组件传递给子 Comment 组件一些数据。例如,我们传递了 Pete Hunt (通过属性)和 This is one comment (通过类似于 XML 的子节点)给第一个 Comment 组件。正如前面说的那样, Comment 组件通过 this.props.author 和 this.props.children 来访问这些“属性”。

Domcom

在Domcom中只是简单的函数调用和传递参数,这在任何编程语言里面都是最自然不过的方法:

1
2
3
4
const commentList = div({className: "commentList"},
makeComment("张三", "这是条评论"),
makeComment("李四“,”这是**另一条**评论“)
);

添加 Markdown 语法格式的评论

Markdown 是一种简单的格式化内联文本的方式。例如,用星号包裹文本将会使其强调突出。

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tutorial6.js
var Comment = React.createClass({
render: function() {
var md = new Remarkable();
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{md.render(this.props.children.toString())}
</div>
);
}
});

Domcom

为了显示Html,Domcom提供了html部件。使用html部件时会将用户内容渲染为dom结构,面对这种需求务必对可能的安全风险保持警惕。

1
2
3
4
5
6
7
const md = Remarkable();
function makeComment(author, content) {
return div({className: "comment"},
h2({className: "commentAuthor"}, author),
html(md.render(content))
);
}

不象react的props.children是部件,Domcom中的函数参数可以是任何数据,因此content会保持是文本,无需react中的转为字符串的步骤。

React

但是这里有一个问题!我们渲染的评论内容在浏览器里面看起来像这样:<p>This is <em>another</em> comment</p>。我们希望这些标签能够真正地渲染成 HTML 。

那是 React 在保护你免受 XSS 攻击。这里有一种方法解决这个问题,但是框架会警告你别使用这种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// tutorial7.js
var Comment = React.createClass({
rawMarkup: function() {
var md = new Remarkable();
var rawMarkup = md.render(this.props.children.toString());
return { __html: rawMarkup };
},
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});

这是一个特殊的 API,故意让插入原始的 HTML 变得困难,但是对于 markdown ,我们将利用这个后门。记住: 使用这个功能,你的代码就要依赖于 remarkable 的安全性。

Domcom

使用html部件会将text渲染为dom结构。因此前一个步骤所使用的html部件的结果已经正好是我们想要的。当然这会存在安全风险,是编码时要注意的。Domcom并没有为此就特意将api搞得更丑,而是在文档中多加提醒。并且在api上更方便让程序员添加转义处理,方法是让html部件函数可以提供一个变换函数作为transform参数。上述代码在Domcom中可以这样实现:

1
2
3
4
5
6
7
8
9
10
const md = Remarkable();
function convertMarkdown(text) {
return md.render(text);
};
const comment = function (author, content) {
return div({className: "comment"},
h2({className: "commentAuthor"}, author),
html(content, convertMarkdown)
);
}

甚至可以更简单:

1
2
3
4
5
6
7
const md = Remarkable();
const comment = function (author, content) {
return div({className: "comment"},
h2({className: "commentAuthor"}, author),
html(content, md.render.bind(md))
);
}

接入数据模型

到目前为止,我们已经在源代码里面直接插入了评论数据。让我们将这些评论数据抽出来,放在一个 JSON 格式的变量中,然后将这个 JSON 数据渲染到评论列表。到最后,数据将会来自服务器,但是现在直接写在源代码中(这一步骤下React和Domcom都会采用这一段代码):

1
2
3
4
5
// tutorial8.js
var data = [
{id: 1, author: "Pete Hunt", text: "This is one comment"},
{id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

React

我们希望将数据以模块化的方式传递进CommentList,因此我们修改CommentBox和ReactDOM.render()调用通过props来传递数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// tutorial9.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById('content')
);

现在CommentList有了data,让我们用data来动态渲染comments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// tutorial10.js
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) {
return (
<Comment author={comment.author} key={comment.id}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});

Domcom

我们将commentList改成函数以便传递数据。利用each部件函数可以动态地监控数据。

1
2
3
4
5
function makeCommentList(comments) {
return each({className: "commentList"}, comments, function(item) {
return comment(item.author, item.text);
})
}

现在需要把commentList在commentBox的使用方式改为函数调用:

1
2
3
4
5
const commentBox = div({className: "commentBox"},
h1("Comments"),
makeCommentList(data),
commentForm
);

在Domcom中并不需要层次化传递数据。我们这里并不一定要把commentBox改成部件函数。本教程会在后面的步骤中做这样的改变。是否这样做完全可以视应用需求而定,不是框架的要求。

从服务器获取数据

React

让我们用从服务器动态获取的数据替换硬编码的数据。我们会删掉 data 属性,使用一个 URL 来获取数据:

1
2
3
4
5
// tutorial11.js
ReactDOM.render(
<CommentBox url="/api/comments" />,
document.getElementById('content')
);

Domcom

现在我们需要改变产生commentBox的方法。为了传入url参数,我们用函数产生部件:

1
2
var commentBox = makeCommentBox("/api/comments");
commentBox.mount()

响应式状态( Reactive state )

到目前为止,每一个组件都根据自己的 props 渲染了自己一次。 props 是不可变的:它们从父组件传递过来,“属于”父组件。为了实现交互,我们给组件引入了可变的 state 。this.state 是组件私有的,可以通过调用 this.setState() 来改变它。当 state 更新之后,组件就会重新渲染自己。

render() 方法依赖于 this.props 和 this.state ,框架会确保渲染出来的 UI 界面总是与输入( this.props 和 this.state )保持一致。

当服务器拿到评论数据的时候,我们将会用已知的评论数据改变评论。让我们给 CommentBox 组件添加一个评论数组作为它的 state :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tutorial12.js
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});

更新 state

当组件第一次创建的时候,我们想从服务器获取(使用 GET 方法)一些 JSON 数据来更新状态,以便展示最新的数据。我们将会使用 jQuery 发送一个异步请求到我们之前启动好的服务器,获取我们需要的数据。数据格式看起来会是这个样子:

1
2
3
4
[
{"author": "Pete Hunt", "text": "This is one comment"},
{"author": "Jordan Walke", "text": "This is *another* comment"}
]

相应代码看起来是这个样子

React

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
// tutorial13.js
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});

这个组件和前面的组件是不一样的,因为它必须重新渲染自己。在服务器请求返回之前,该组件将不会有任何数据,请求返回之后,该组件或许要渲染一些新的评论。

Domcom

根据现在的需求,我们把commentBox改成部件函数,把url作为参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function makeCommentBox(url) {
const comments = [];
const commentBox = div({className: "commentBox"},
h1("Comments"),
makeCommentList(comments),
commentForm
);
commentBox.on('didMount', function() {
$.ajax({
url: url,
dataType: 'json',
cache: false,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(url, status, err.toString());
}
});
});
return commentBox;
}

上面的代码中,didMount事件的回调函数并不需要象在React的代码中那样引用this.props.url, 也不需要调用this.setState,只是使用函数参数url和局部变量comments,因此不需要把它设计为commentBox的方法,也不需要使用bind绑定this,用闭包函数就好了。

可以看到,Domcom中数据和部件解耦得更加彻底,同时传递和管理数据反而更加灵活便利了。


从服务器动态获取数据

现在实现从服务器动态获取数据的代码。

这里, componentDidMount 是一个组件渲染的时候被 React 自动调用的方法。动态更新界面的关键点就是调用 this.setState() 。我们用新的从服务器拿到的评论数组来替换掉老的评论数组,然后 UI 自动更新。有了这种反应机制,实现实时更新就仅需要一小点改动。在这里我们使用简单的轮询,但是你也可以很容易地改为使用 WebSockets 或者其他技术。

React

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
// tutorial14.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
ReactDOM.render(
<CommentBox url="/api/comments" pollInterval={2000} />,
document.getElementById('content')
);

Domcom

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
function makeCommentBox(url = “api/comments”, pollInterval = 2000) {
const comments = [];
const commentBox = div({className: "commentBox"},
h1("Comments"),
makeCommentList(comments),
commentForm
);
function loadCommentsFromServer() {
$.ajax({
url: url,
dataType: 'json',
cache: false,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(url, status, err.toString());
}
});
},
commentBox.on('didMount', function(){
loadCommentsFromServer();
setInterval(loadCommentsFromServer, pollInterval)
});
return commentBox;
}

上面的代码中,我们将有关代码重构为loadCommentsFromServer,因为与前一个步骤类似的理由,依然只要用闭包函数就好,不必将其设定为commentBox的方法。


添加新的评论

现在是时候构造表单了。我们的 CommentForm 组件应该询问用户的名字和评论内容,然后发送一个请求到服务器,保存这条评论。

React

1
2
3
4
5
6
7
8
9
10
11
12
// tutorial15.js
var CommentForm = React.createClass({
render: function() {
return (
<form className="commentForm">
<input type="text" placeholder="Your name" />
<input type="text" placeholder="Say something..." />
<input type="submit" value="Post" />
</form>
);
}
});

Domcom

考虑到后面有传递回调函数参数的需求,现在就直接将commentForm改为用函数实现。

1
2
3
4
5
6
7
function makeCommentForm() {
return form({className: "commentForm"},
text({placeholder: "Your name"}),
text({placeholder: "Say something..."}),
input({type: "submit", value: "Post"})
);
}

处理输入事件

React

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
// tutorial16.js
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
render: function() {
return (
<form className="commentForm">
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
});

事件

React 使用驼峰命名规范的方式给组件绑定事件处理器。我们给表单绑定一个onSubmit处理器,用于当表单提交了合法的输入后清空表单字段。

用回调函数作为属性( props )
当用户提交评论的时候,我们需要刷新评论列表来加进这条新评论。在 CommentBox 中完成所有逻辑是合适的,因为 CommentBox 拥有代表评论列表的状态( state )。

我们需要从子组件传数据到它的父组件。我们在父组件的 render 方法中这样做:传递一个新的回调函数( handleCommentSubmit )到子组件,绑定它到子组件的 onCommentSubmit 事件上。无论事件什么时候触发,回调函数都会被调用:

Domcom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function makeCommentForm() {
var newComment = {author: '', text: ''};
return form({className: "commentForm"},
text({
placeholder: "Your name",
value: newComment.author,
onchange: function(event, node){
newComment.author = node.value;
}
}),
text({
placeholder: "Say something...",
value: newComment.text,
onchange: function(event, node){
newComment.text = node.value;
}
}),
input({type: "submit", value: "Post"})
);
}

上面代码中,除了text部件,没有别的地方会改变newComment的内容,text部件不需要依据newComment动态刷新。如果需要text部件自动响应newComment的数据变化,可以使用响应函数, 这里可以用flow.bind, 就象这样:text({..., value: flow.bind(newComment, 'author'), ...})

如果使用指令,上面的代码可以更加简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dc.directives('model', dc.$model);
function makeCommentForm() {
var newComment = {author: '', text: ''};
const {author$, text$} = dc.bindings(newComment);
return form({className: "commentForm"},
text({
placeholder: "Your name",
$model: author$
}),
text({
placeholder: "Say something...",
$model: text$
}),
input({type: "submit", value: "Post"})
);
}

text部件(也包括其它input部件)甚至可以这样写:text({ placeholder: "Your name"}, author$)。这种写法和使用指令的写法都只是不同程度的语法糖而已,本质上和不用指令的代码是一样的。


表单交互

让我们使表单可交互。当用户提交表单的时候,我们应该清空表单,发送一个请求到服务器,然后刷新评论列表。首先,让我们监听表单的提交事件,然后清空表单。

React

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
// tutorial17.js
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
// TODO: send request to the server
this.setState({author: '', text: ''});
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
});

Domcom

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
function makeCommentForm() {
var newComment = {author: '', text: ''};
return form({className: "commentForm"},
text({
placeholder: "Your name",
value: newComment.author,
onchange:function(event, node){
newComment.author = node.value;
}
}),
text({
placeholder: "Say something...",
value: newComment.text,
onchange: function(event, node){
newComment.text = node.value;
}
}),
input({
type: "submit",
value: "Post",
onsubmit: function(event, node) {
event.preventDefault = true;
var author = newComment.author.trim();
var text = newComment.text.trim();
if (!text || !author) {
return;
}
extend(newComment, {author: '', text: ''});
// TODO: send request to the server
}
})
);
}

当用户提交表单的时候,在 CommentForm 中调用这个回调函数:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// tutorial18.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) {
// TODO: submit to the server and refresh the list
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
```
#### Domcom
现在修改makeCommentBox,向makeCommentForm传递一个回调函数作为参数以便处理提交数据。
```js
function makeCommentBox(url, pollInterval = 2000) {
const comments = [];
const commentBox = div({className: "commentBox"},
h1("Comments"),
makeCommentList(comments),
makeCommentForm(function(newComment){
// TODO: submit to the server and refresh the list
})
);
function loadCommentsFromServer() {
$.ajax({
url: url,
dataType: 'json',
cache: false,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(url, status, err.toString());
}
});
},
commentBox.on('didMount', function(){
loadCommentsFromServer();
setInterval(loadCommentsFromServer, pollInterval)
});
return commentBox;
}

调用回调并清除评论

React

既然CommentBox已经通过onCommentSubmit特性将回调函数传递给CommentForm,CommentForm应该在用户提交form的时候调用这个回调函数。调用之后记得清除已经提交的评论。

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
// tutorial19.js
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.setState({author: '', text: ''});
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
});

Domcom

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
function makeCommentForm(submitComment) {
var newComment = {author: '', text: ''};
return form({className: "commentForm"},
text({
placeholder: "Your name",
value: newComment.author,
onchange:function(event, node){
newComment.author = node.value;
}
}),
text({
placeholder: "Say something...",
value: newComment.text,
onchange: function(event, node){
newComment.text = node.value;
}
}),
input({
type: "submit",
value: "Post",
onsubmit: function(event, node) {
event.preventDefault = true;
var author = newComment.author.trim();
var text = newComment.text.trim();
if (!text || !author) {
return;
}
submitComment(newComment);
extend(newComment, {author: '', text: ''});
}
})
);
}

发起POST请求并更新状态

React

在传递给CommentForm的回调handleCommentSubmit中添加ajax代码,发起服务器POST请求,成功时调用setState更新状态。

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
// tutorial20.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) {
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});

Domcom

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
function makeCommentBox(url, pollInterval = 2000) {
const comments = [];
const commentBox = div({className: "commentBox"},
h1("Comments"),
makeCommentList(comments),
makeCommentForm(function(newComment){
$.ajax({
url: url,
dataType: 'json',
type: 'POST',
data: newComment,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}
});
})
);
function loadCommentsFromServer() {
$.ajax({
url: url,
dataType: 'json',
cache: false,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(url, status, err.toString());
}
});
},
commentBox.on('didMount', function(){
loadCommentsFromServer();
setInterval(loadCommentsFromServer, pollInterval)
});
return commentBox;
}

优化:提前更新

我们的应用现在已经完成了所有功能,但是在新添加的评论出现在列表之前,必须等待请求完成,所以感觉很慢。我们可以直接添加这条评论到列表中,从而使应用感觉更快。

React

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
// tutorial21.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) {
var comments = this.state.data;
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// not use Date.now() for this and would have a more robust system in place.
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
this.setState({data: comments});
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});

Domcom

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
function makeCommentBox(url, pollInterval = 2000) {
const comments = [];
const commentBox = div({className: "commentBox"},
h1("Comments"),
makeCommentList(comments),
makeCommentForm(function(newComment){
$.ajax({
url: url,
dataType: 'json',
type: 'POST',
data: newComment,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(url, status, err.toString());
}
});
comments.push(newComment);
commentBox.render();
})
);
function loadCommentsFromServer() {
$.ajax({
url: url,
dataType: 'json',
cache: false,
success: function(data) {
comments.replaceAll(data);
dc.render();
},
error: function(xhr, status, err) {
console.error(url, status, err.toString());
}
});
},
commentBox.on('didMount', function(){
loadCommentsFromServer();
setInterval(loadCommentsFromServer, pollInterval);
});
return commentBox;
}

从上述分别用React和Domcom通过一些步骤构造评论框的过程,我们看到Domcom的代码比React要简洁很多(即使比带JSX的React代码也更简洁),涉及的新概念也更少,学习曲线更低,更加贴近javascript最普遍的语言特性。而且我可以告诉大家,Domcom的实现原理可以让它达到比React更好的运行性能。

考虑到当前web前端的主流习惯,本文以javascript作为Domcom演示语言。Domcom框架是以用coffee-script开发的,但从使用上可以良好地用在ES6或ES5环境中,并且以UMD风格支持模块化。如果使用coffee-script语言来开发Domcom的应用程序,代码将更为简洁优雅。

对于React框架而言,不用JSX的话代码会繁琐得多。以React如此繁琐的API,让程序员不用JSX显然是难以令人接受的。我想,React设计者不遗余力地实现JSX,应该有减少这种繁琐的意图。然而,即使用JSX写React依然是很繁琐的。比如说生命周期方法的名称为什么要那么长?明显可以用更简短的名字。还有类似this.props.data,this.props.children之类的长串访问路径, 以及refs的设计,事件处理方法需要bind this,等等。这些用起来都是很不方便的。

既然reactJS的设计者意识到了程序员是讨厌繁琐的,为什么要设计如此繁琐的API?我只能理解为React设计者的观念问题。设计者已经理解到了完全由javascript掌控页面,将页面组件化的优势和重要性,这是一个巨大的进步。然而,遗憾的是React设计者的观念没有能在API和语法习惯上转过弯来,还停留在实现框架初期的当时状态,不由自主地向html页面设计者做出了妥协。其实,为了实现这种妥协反而需要付出更大的努力去实现JSX的编译器,这个难度远远超过设计简洁API的难度。然而,既然React应用的页面构造已经完全在javascript代码和程序员的掌控之下,这样一种妥协就是完全不必要和不适当的,徒增学习难度、工具复杂度,降低开发效率,带来更多问题。

除了API的繁琐外,ReactJS在观念与设计哲学上与Javascript上存在很多不协调之处,比如:对副作用不够友好,为了粉饰这个缺陷而过分地拔高函数式编程,贬低OOP,用flux取代MVVM或者MVC等到,结果是导致了ReactJS全家桶的现象。

基于上述原因,虽然我最初就看到了ReactJS整体上带来的的进步,但是也看到了ReactJS将带来的巨大问题,可以认为,ReactJS有如此多的痛点,并不是一个非常完善的框架。这也是促使我设计Domcom的原因之一。

由此观之,开发者的整体观念和理念是多么重要啊。它不但影响了一个框架,又进一步影响到了一个应用领域。不象Java社区,Javascript社区是不太喜欢笨重的xml风格的,而是更加青睐类似JSON这种更简单轻便的规范。不过,作为web前端开发者,在以前的开发模式下,总是不可避免地要和Html打交道的。React本来很有希望能够进一步统一web前端开发的代码风格,逼近非web领域的主流GUI开发方式。因为观念的原因,很遗憾React错过了这个机会,让Domcom能有机会站在巨人的肩膀上。

关于React和Domcom的总体对比,这篇文章有更深入的阐述。


祝贺你你刚刚通过一些简单步骤构造了一个评论框。现在你可以去学习一个更全面的辅导教程, 了解更多关于 Domcom 的概念和原理,或者去看看 Domcomm API参考