导读:如果您正在看这篇文章,想必您正在开发一个多人交互的网站,而踌躇自己写一个评论组件?
纵观BAT以及各种电子商务或博客等网站,无一不使用了评论这个功能。那么你能自己实现吗?
一、先欣赏一下各大网站的评论区图片
【CSDN】
【百度】
【知乎】
暂时先列举三个网站的评论区设计,其它还有很多网站的评论区设计都很经典,比如:GitHub、YouTube等等。由于网络原因我就不一一截图列举了。
二、分析:主要从结构和源码入手
1、回复者与作者的关系
如图所示:我们把文章作者定义为(Root),评论者定义为(Leaf),而评论者具有多层关系,因此分为第一第二等层级。而每个层级又拥有多个子节点
2、应用
评论者与回复者的关系即可。A级为评论者,直接对文章进行评论,那么对A的评论进行回复或者对回复A的评论的人进行回复的所有层级都被称为回复者。搞清了这个关系,那么设计起来就简单多了。
CSDN的网站的评论模块就采用两层关系设计,一方面是直观,一方面是操作简单,不用一层一层点开回复内容,同时减少了开发者的负担,而且维护起来也容易。百度也是两层关系设计,但以前不是。特别是以前看过YouTube网站,那层级关系忒复杂,等你把七八层的回复内容挨个点开时,你会发现主要内容已经消失不见。
3、f12源码(CSDN为例)
两层关系的源码被一个div包裹,而里面有若干个ul标签,而ul标签里是n个li标签。让我们进一步看看这两个li标签分别渲染了评论区哪些部分。
第一个li标签渲染的是评论者模块,第二个li标签渲染的是回复者模块。(这里有一个小插曲,第二个li的class名字是不是有问题,确定是replay-box吗,是不是应该为reply-box?)
两个li标签其实属于同级关系,但却渲染了不同级别的内容。这种设计更利于维护和实现。第二个li标签相对于它的父节点也就是第一个li标签而言,只是将margin的左外边距右偏32px(相对于右边的外边距而言,源码大家自己可以再网页使用f12查看,不同的浏览器快捷键可能不一样)
三、
从源码来看,层级结构大致是div中嵌套多个ul,而ul的多少代表有多少个评论者。
每个ul中嵌套两个li标签,分别渲染评论内容和回复内容两部分,回复内容在渲染上只要不与评论内容处于一个纵线即可直观区分它们之间的关系。具体如何实现均可看源码学习。
那么这样一个数据结构应该是什么样的呢?
我指的是前端的。首先肯定不是KVP的Map,最外层是多个ul,应该选择用数组,而数组里是不是应该还要用数组呢?
当然不是,虽然ul中嵌套多个li标签,根据行业经验前端不做过多的逻辑处理,如果用数组,就无法表达嵌套数组元素与外层数组元素的关系了,且无法携带描述信息。因此我想到了使用对象。对象可以封装描述每个评论单位的描述信息,同时可以携带一个数组,用来封装回复内容。这里要注意一点,回复内容从实现角度来说没有层级结构,他们只有关系,这样更直观。所以回复内容的父节点只有一个——父级评论者。如果后续考虑到数据结构的变化,我们可以在数据库中为每条回复内容增设两个字段,一个是超父节点ID,一个是直属父级节点ID,这样后续无论是改回递归形式呈现还是两级关系呈现,都不影响数据库的数据修改。
四、实现
代码我已经写好了,有不妥的地方请指正(代码注释后续我会抽时间加上)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Comment component implementation</title>
<!-- <link type="text/css" rel="stylesheet" href="comment.css"> -->
<style>
body, html{
background-color: rgb(0, 0, 0, 0.05);
position: absolute;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
dt, dd, dl{
margin: 0;
padding: 0;
}
dt, dd{
font-weight: 500;
display: inline-block;
}
textarea{
width: 820px; height: 50px; resize: none
}
#button_submit_div > div > button{
border: none;
background: red;
color: white;
padding: 4px 10px;
}
#button_submit_div{
text-align: right;
}
.time{
padding: 0 10px;
color: lightgray;
}
ul{
list-style: none;
padding: 0;
}
ul > li:hover{
background: deepskyblue;
cursor: pointer;
}
ul > li{
padding: 10px 10px;
border-bottom: solid 0.5px lightgray;
display: block;
}
ul > li:last-child{
border-bottom: none;
}
.cmt_img, .right_box{
display: inline-block;
vertical-align: middle;
}
.avatar{
display: block;
}
aside, main{
background: white;
vertical-align: top;
margin-top: 50px;
box-shadow: 2px 0 2px 0 lightgray;
display: inline-block;
}
aside{
margin-left: 20px;
text-align: center;
width: 300px;
}
main{
padding: 20px;
text-align: left;
width: 900px;
}
.form_item{
padding: 10px 0;
}
.menu > div{
display: inline-block;
padding: 10px 10px;
}
.menu > div:hover{
background: deepskyblue;
cursor: pointer;
}
.menu{
margin: 0;
padding: 0;
background: white;
position: absolute;
width: 100%;
text-align: center
}
input, textarea{
border: solid 0.5px lightgray;
border-radius: 3px;
}
input{
padding: 4px 4px;
height: 30px;
}
textarea{
padding: 10px;
height: 60px;
}
</style>
</head>
<body>
<div class="menu">
<div>首页</div>
<div>博客</div>
<div>下载</div>
<div>视频</div>
<div>学习</div>
</div>
<aside>
<ul>
<li>所有</li>
<li>Java</li>
<li>Python</li>
<li>C++</li>
<li>JavaScript</li>
</ul>
</aside>
<main onclick="mainCLick(event)" style="text-align: left">
<div class="form_item">
<label>姓名:</label>
<input id="commentator" placeholder="请输入您希望展示的名称"/>
</div>
<div class="form_item">
<label class="label_img">
<img src="avatar.png" width="25px" style="border-radius: 100%">
</label>
<textarea id="comment_space"
onkeyup="readKey(event)"
onclick="commentFocus(event)"
placeholder="请输入您要评论的内容"></textarea>
</div>
<div id="button_submit_div">
</div>
<hr/>
<div id="comment_view">
</div>
</main>
<script type="text/javascript">
getRandomId = function () {
return 'id'.concat(Math.floor((Math.random(10) * 10000000000000000)).toString());
};
getCurrentTime = function () {
return new Date().toLocaleString();
};
var curReplyTargetTag = null;
var curArr = null;
var parentId = null;
var commentList = [];
var commentTree = {
id:'',
commentator: '',
author: '',
comment: '',
time: '',
reply: []
};
window.onload = function (ev) {
// the function will be called at first while page loads
initTreeComment();
};
initTreeComment = function () {
var commentView = document.getElementById('comment_view');
if (commentList.length === 0) {
noComment(commentView)
} else {
haveComment(commentView, commentList)
}
};
noComment = function (commentView) {
commentView.style.textAlign = 'center';
commentView.innerText = 'No comments'
};
commentFocus = function (ev) {
ev.stopPropagation();
document.getElementById('comment_space').focus();
renderReplyBar();
};
renderReplyBar = function () {
var commentSpace = document.getElementById('comment_space');
var textareaLen = commentSpace.value.length;
var htmlText = '';
htmlText +=
'<div>' +
' <span style="color: lightgray; margin-right: 20px">' +
' 还可以输入' +
' <span id="char_count">'+ (1000 - textareaLen) +'</span>' +
' 个字符' +
' </span>';
if(commentSpace.placeholder.charAt(0) === '回' || textareaLen > 0) {
htmlText += '<button id="cancel_reply" ' +
' onclick="cancelReply(event, this)"' +
' style="margin-right: 20px;">'
} else {
htmlText += '<button id="cancel_reply"' +
' onclick="cancelReply(event, this)"' +
' style="margin-right: 20px;display: none;">'
}
htmlText += '取消回复</button>' +
'<button id="submit_button">提交</button>' +
'</div>';
document.getElementById('button_submit_div').innerHTML = htmlText;
submitComment(commentSpace);
};
submitTree = function (submitButton, textareaEle) {
submitButton.onclick = function (ev) {
ev.stopPropagation();
console.log(parentId);
var text = textareaEle.value;
if (text.length > 0){
var plcHolder = textareaEle.placeholder;
if(plcHolder.charAt(0) === '回') {
plcHolder = plcHolder.substring(plcHolder.indexOf(' '), plcHolder.length)
} else {
curArr = commentList;
plcHolder = "anonymous";
}
var commentator = document.getElementById('commentator').value;
if(commentator.trim().length === 0) {
alarmIfEmpSpace();
} else {
commentTree = {
id: getRandomId(),
commentator: commentator,
author: plcHolder,
comment: text,
time: getCurrentTime(),
reply: []
};
curArr.push(commentTree);
initTreeComment();
}
} else {
alarmIfEmpSpace();
}
}
};
alarmIfEmpSpace = function () {
alert("Nothing worse than itself!")
};
submitComment = function (textarea) {
var submitButton = document.getElementById('submit_button');
submitTree(submitButton, textarea);
};
cancelReply = function (ev, th) {
ev.stopPropagation();
th.style.display = 'none';
parentId = null;
document.getElementById('comment_space').value = "";
countTextareaLength();
fillPlaceholderOfCommentSpace("请输入您要评论的内容");
};
countTextareaLength = function () {
var space = document.getElementById('comment_space');
document.getElementById('char_count').innerText = 1000 - space.value.length;
};
readKey = function (ev) {
var space = document.getElementById('comment_space');
document.getElementById('char_count').innerText = 1000 - space.value.length;
if(space.value.length > 0) {
handleCancelButton(space, 'inline-block')
} else {
handleCancelButton(space, 'none')
}
};
handleCancelButton = function (textArea, status) {
document.getElementById('cancel_reply').style.display = status;
};
commentBlur = function () {
document.getElementById('button_submit_div').innerHTML = ''
};
haveComment = function (commentView, arr) {
commentView.style.textAlign = 'left';
var htmlText = '';
arr.forEach(function (value) {
htmlText +=
'<ul id="'+ value.id +'" class="comment_ulist" style="">' +
' <li class="comment_line_box" id="'+ value.id.substring(0, 4) +'">' +
' <a class="cmt_img">' +
' <img class="avatar" src="avatar.png" width="30px" style="border-radius: 100%">' +
' </a>' +
' <div class="right_box">' +
' <a class="commentator">'+ value.commentator +'</a>';
if (value.author !== 'anonymous') {
htmlText +=
'<span style="margin: 0 10px;color: lightgray">回复</span>' +
'<a class="author">'+ value.author +'</a>';
}
htmlText += '<span class="time">'+ value.time +'</span>';
if (value.comment.length > 10){
htmlText += ' <span style="display: block; margin-top: 8px" class="comment">'+ value.comment.substring(0, 10) +'</span>';
} else {
htmlText += ' <span class="comment">'+ value.comment.substring(0, 10) +'</span>';
}
htmlText +=
' </div>' +
' <span style="float: right;" id="'+ value.id.substring(0, 7) +'">' +
'<a id="'+ value.id.substring(0, 5) +'"' +
' style="border: none;display: none;margin-right: 10px;">' +
'回复</a>';
if(value.reply.length > 0) {
htmlText += '<a id="'+ value.id.substring(0, 6) +'"' +
' style="border: none;">' +
' 查看回复('+ value.reply.length +')</a></span></li></ul>'
} else {
htmlText += '</span></li></ul>';
}
});
commentView.innerHTML = htmlText;
showButton(arr, 1)
};
showButton = function (arr, sign) {
arr.forEach(function (value) {
var parent = document.getElementById(value.id);
var broEle = document.getElementById(value.id.substring(0, 4));
var checkReply = document.getElementById(value.id.substring(0, 6));
var reply = document.getElementById(value.id.substring(0, 5));
broEle.onmouseover = function (ev) {
reply.style.display = 'inline-block';
};
reply.onclick = function (ev) {
ev.stopPropagation();
renderReplyBar();
curReplyTargetTag = parent;
if(sign === 1) {
parentId = value.id
curArr = value.reply;
} else {
curArr = arr;
}
document.getElementById('cancel_reply').style.display = 'inline-block';
var str = "回复 ".concat(value.commentator);
fillPlaceholderOfCommentSpace(str)
};
if (value.reply.length > 0) {
checkReply.onclick = function (ev) {
ev.stopPropagation();
if(checkReply.innerText.trim().charAt(0) === '查'){
ifHaveReply(parent, value.reply, broEle);
checkReply.innerText = "收起回复";
} else {
toggleBackReplies(parent);
checkReply.innerText = "查看回复("+ value.reply.length +")";
}
};
}
broEle.onmouseleave = function (ev) {
reply.style.display = 'none'
};
});
};
toggleBackReplies = function (parentTag) {
var nodes = parentTag.childNodes;
var len = nodes.length;
parentTag.removeChild(nodes[len - 1]);
};
ifHaveReply = function (parentTag, arr, broEle) {
var li = document.createElement("li");
li.className = "reply_list";
li.style.marginLeft = '42px';
li.style.borderLeft = 'solid 5px lightgray';
var htmlText = '<ul class="comment_ulist">';
arr.forEach(function (value) {
htmlText +=
'<li class="comment_line_box" id="'+ value.id.substring(0, 4) +'"><a class="cmt_img" style="margin-left: 10px">' +
' <img class="avatar" src="avatar.png" width="30px" style="border-radius: 100%">' +
'</a>' +
'<div class="right_box">' +
' <a class="commentator">'+ value.commentator +'</a>' +
' <span style="margin: 0 10px;color: lightgray">回复</span>' +
' <a class="author">'+ value.author +'</a>' +
' <span class="time">'+ value.time +'</span>';
if (value.comment.length > 10){
htmlText += ' <span style="display: block; margin-top: 8px" class="comment">'+ value.comment.substring(0, 10) +'</span>';
} else {
htmlText += ' <span class="comment">'+ value.comment.substring(0, 10) +'</span>';
}
htmlText +=
'</div>' +
'<span style="float: right;" id="'+ value.id.substring(0, 7) +'">' +
'<a id="'+ value.id.substring(0, 5) +'"' +
' style="display: none;border: none;margin-right: 10px;">' +
'回复</a>';
if(value.reply.length > 0) {
console.log(value.reply.length)
htmlText += '<a id="'+ value.id.substring(0, 6) +'"' +
' style="border: none;">' +
'查看回复('+ value.reply.length +')</a></span></li>'
} else {
htmlText += '</span></li>';
}
});
htmlText += '</ul>';
li.innerHTML = htmlText;
parentTag.insertBefore(li, broEle.nextSibling);
showButton(arr, 2)
};
fillPlaceholderOfCommentSpace = function (str) {
document.getElementById('comment_space').placeholder = str
};
mainCLick = function (ev) {
document.getElementById('button_submit_div').innerHTML = '';
};
</script>
</body>
</html>
五、代码运行截图
谢谢大家的欣赏,我会在编程和编辑这条路上越走越好,送给仍然青年的自己!