本文关于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
<!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 >
</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
<!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 >
<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 >
</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也是基于部件的框架。所有部件都是模块化,可组装的,组合性更胜于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
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
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 )。
1
2
3
4
5
const {div} = dc;
var commentBox = div({className : "commentBox" },
"Hello, world! I am a CommentBox."
);
commentBox.mount();
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
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 >
);
}
});
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
var CommentBox = React.createClass({
render : function ( ) {
return (
<div className ="commentBox" >
<h1 > Comments</h1 >
<CommentList />
<CommentForm />
</div >
);
}
});
注意我们是如何整合 HTML 标签和我们所创建的组件的。 HTML 组件就是普通的 React 组件,就和你定义的组件一样,只不过有一处不同。 JSX 编译器会自动重写 HTML 标签为 React.createElement(tagName) 表达式,其它什么都不做。这是为了避免全局命名空间污染。
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
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中可以直接通过函数参数传递数据。
对于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
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中只是简单的函数调用和传递参数,这在任何编程语言里面都是最自然不过的方法:
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
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 >
);
}
});
为了显示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
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 的安全性。
使用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
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
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
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 >
);
}
});
我们将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
ReactDOM.render(
<CommentBox url ="/api/comments" /> ,
document.getElementById('content')
);
现在我们需要改变产生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
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
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 >
);
}
});
这个组件和前面的组件是不一样的,因为它必须重新渲染自己。在服务器请求返回之前,该组件将不会有任何数据,请求返回之后,该组件或许要渲染一些新的评论。
根据现在的需求,我们把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
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')
);
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
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>
);
}
});
考虑到后面有传递回调函数参数的需求,现在就直接将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
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 事件上。无论事件什么时候触发,回调函数都会被调用:
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
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 .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>
);
}
});
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 : '' });
}
})
);
}
当用户提交表单的时候,在 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
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 ) {
},
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
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>
);
}
});
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
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>
);
}
});
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
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;
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>
);
}
});
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参考 。