本文依据React的评论框教程 写成。案例本身以及不少中文解说文字出自React 中文社区对React Tutorial的翻译 ,略有改动。其余部分为本人原创。上述相关方以及各位读者如果觉得本文有侵权或其它不妥之处,敬请告知taijiweeb@gmail.com,本人将做适当修改,谢谢。
构造一个评论部件 我们将构建一个简单却真实的评论框,你可以将它放入你的博客,类似于 disqus 、 livefyre 、 facebook 中实时评论框,包含一些基础功能。我们将提供以下功能:
展示所有评论的界面
发布评论的表单
后端 API 接口地址,该接口需要自己实现
同时也包含一些简洁的特性:
发布评论的体验优化: 评论在保存到服务器之前就展现在评论列表,因此感觉起来很快。
实时更新: 其他用户的评论将会实时展示在评论框中。
Markdown格式: 用户可以使用Markdown语法来编辑评论文本。
运行一个服务器 为了开始本教程,我们将需要一个运行的服务器,提供一个 API 接口,仅用于获取和保存评论数据。为了尽量简便,我们已经准备好了几种脚本语言开发的简单服务器程序,完成了我们需要的功能。你可以查看源码 或者下载 zip 文件 ,里面包含了开始学习教程所需的所有东西。
为了简单,服务器将会使用一个 JSON 文件作为数据库。不能在生产环境中采用这种做法,但是这样做确实简化了模拟后端 API 的工作。一旦启动了这个服务器,它就能提供我们需要的 API 接口,同时也能生成并发送我们需要的页面。
设置html页面 本教程会尽量简单。上述讨论提及的页面是一个 HTML 文件,它包含在服务器源码包中,我们先看看这个文件。使用你最喜欢的编辑器打开 public/index.html ,然后会看见这些内容:
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 标签中编写我们的 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 。
第一个部件 Domcom Domcom是基于部件的框架。所有部件都是模块化,可组装的,组合性以及扩展性都非常好。另外还有一个显著特点,Domcom部件支持通过OOP方式进行扩展。
可以看到,本教程中Domcom表现得非常贴近函数式编程,没有体现出任何与OOP有关的特征,因此必须在这里特别地强调OOP的重要性。在GUI领域,OOP是非常重要的。只要熟悉GUI编程(比如MFC,WPF,Qt,WxWidgets, Android UI,cocoa等等)的人都应该理解这一点,不被某些错误的言论误导。
对于我们的评论框例子,我们将有如下的 Domcom 部件结构:
1
2
3
4
- CommentBox
- CommentList
- Comment
- CommentForm
第一步只是构造一个简单的div。
1
2
3
4
5
const {div} = dc;
var commentBox = div({className : "commentBox" },
"Hello, world! I am a CommentBox."
);
commentBox.mount();
制作部件 让我们为 commentList 和 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 部件代码:
1
2
3
4
5
const commentBox = div({className : "commentBox" },
h1("Comments" ),
commentList,
commentForm
);
传递数据 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
7
8
9
10
11
function makeComment(author, content) {
return div({className: "comment"},
h2({className: "commentAuthor"}, author),
content
);
}
const commentList = div({className: "commentList"},
makeComment("张三", "这是条评论"),
makeComment("李四“,”这是**另一条**评论“)
);
添加 Markdown 语法格式的评论 Markdown 是一种简单的格式化内联文本的方式。例如,用星号包裹文本将会使其强调突出。
为了显示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))
);
}
使用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 数据渲染到评论列表。到最后,数据将会来自服务器,但是现在直接写在源代码中:
1
2
3
4
var data = [
{id : 1 , author : "Pete Hunt" , text : "This is one comment" },
{id : 2 , author : "Jordan Walke" , text : "This is *another* comment" }
];
我们将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改成部件函数。本教程会在后面的步骤中做这样的改变。是否这样做完全可以视应用需求而定,不是框架的要求。
从服务器获取数据 现在我们需要改变产生commentBox的方法。为了传入url参数,我们用函数产生部件:
1
2
var commentBox = makeCommentBox("/api/comments" );
commentBox.mount()
根据现在的需求,我们把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中数据和部件解耦得更加彻底,同时传递和管理数据反而更加灵活便利了。
从服务器动态获取数据 现在实现从服务器动态获取数据的代码。
这里,我们用新的从服务器拿到的评论数组来替换掉老的评论数组,然后 UI 自动更新。有了这种反应机制,实现实时更新就仅需要一小点改动。在这里我们使用简单的轮询,但是你也可以很容易地改为使用 WebSockets 或者其他技术。
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 部件应该询问用户的名字和评论内容,然后发送一个请求到服务器,保存这条评论。
考虑到后面有传递回调函数参数的需求,现在就直接将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" })
);
}
处理输入事件 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$)
。这种写法和使用指令的写法都只是不同程度的语法糖而已,本质上和不用指令的代码是一样的。
表单交互 让我们使表单可交互。当用户提交表单的时候,我们应该清空表单,发送一个请求到服务器,然后刷新评论列表。首先,让我们监听表单的提交事件,然后清空表单。
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 : '' });
}
})
);
}
现在修改makeCommentBox,向makeCommentForm传递一个回调函数作为参数以便处理提交数据。
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
function makeCommentBox (url, pollInterval = 2000 ) {
const comments = [];
const commentBox = div({className : "commentBox" },
h1("Comments" ),
makeCommentList(comments),
makeCommentForm(function (newComment ) {
})
);
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;
}
调用回调并清除评论 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请求并更新状态 在传递给CommentForm的回调handleCommentSubmit中添加ajax代码,发起服务器POST请求,成功时调用comments.replaceAll。
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;
}
优化:提前更新 我们的应用现在已经完成了所有功能,但是在新添加的评论出现在列表之前,必须等待请求完成,所以感觉很慢。我们可以直接添加这条评论到列表中,从而使应用感觉更快。
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;
}
本文用Domcom通过一些步骤来构造评论框。我们看到Domcom的代码虽然没有是用JSX,但是依然很简洁(即使比带JSX的React代码也更简洁),涉及的新概念很少,学习曲线非常平坦,更加贴近javascript最普遍的语言特性。而且我可以告诉大家,Domcom的实现原理可以让它达到比React更好的运行性能。
考虑到当前web前端的主流习惯,本文以javascript作为Domcom演示语言。Domcom框架是以用coffee-script开发的,但从使用上可以良好地用在ES6或ES5环境中,并且以UMD风格支持模块化。如果使用coffee-script语言来开发Domcom的应用程序,代码将更为简洁优雅。
祝贺你你刚刚通过一些简单步骤构造了一个评论框。现在你可以去学习一个更全面的辅导教程 , 了解更多关于 Domcom 的概念和原理 ,或者去看看 Domcomm API参考 。