First of all, go to the message page of your personal website and you can see the effect: Message Board
To save trouble, the front-end is written using jQuery, and the back-end uses PHP to simply read and write the MySQL database.
Database design and implementation ideas
The database has created a table: comments, with the structure as shown below:
All comments (including article comment replies and message boards) are written in the same table, and different comment areas are distinguished by the field belong
In the same comment area, if parent is 0, it represents a comment. When parent is a certain value, it represents a reply to which comment. The idea is not complicated.
Note that we are not talking about CSS here. You can customize it according to your own needs. Let’s start packaging now:
Setting function
We set the functions according to our own needs. First of all, my website does not implement message reminder and instant messaging functions, so comment replies will not prompt the webmaster or users, but will only have an effect on the message area, so we only need Simply implement the following functions:
1. Display the comment list
2. Ability to submit comments
3. Reply
Comments
We encapsulate the comment function into a class, and we can create different comment areas through instantiation, so it is not difficult to think of,
The parameters we need to pass in when instantiating may include: the id of the comment area, the php address for obtaining comments, and the php address for submitting comments.
So we can guess that the code to instantiate the comment area may be:
var oCmt = new Comment({ parent: $('#box'), //你想要将这个评论放到页面哪个元素中 id: 0, getCmtUrl: './php/getcomment.php', setCmtUrl: './php/comment.php' })
Of course, I define a static method on the Comment class
Comment.allocate({ parent: $('#box'), id: 0, getCmtUrl: './php/getcomment.php', setCmtUrl: './php/comment.php' })
Mostly the same, just the initialization is different
Constructor
function Comment(options){ this.belong = options.id; this.getCmtUrl = options.getCmtUrl; this.setCmtUrl = options.setCmtUrl; this.lists = []; this.keys = {}; this.offset = 5; } var fn = Comment.prototype; Comment.allocate = function(options){ var oCmt = new Comment(options); if (oCmt.belong == undefined || !oCmt.getCmtUrl || !oCmt.setCmtUrl) { return null; }; oCmt.init(options); return oCmt; };
. If you do not define an allocate method, you can write it as:
function Comment(options){ this.belong = options.id; this.getCmtUrl = options.getCmtUrl; this.setCmtUrl = options.setCmtUrl; this.lists = []; this.keys = {}; this.offset = 5; if (this.belong == undefined || !this.getCmtUrl || !this.setCmtUrl) { return null; }; this.init(options) } var fn = Comment.prototype;
Let’s not talk about variables. Like me, I always write the function function first, and then need to add attribute variables and then add them later. We only need to see that the constructor is finally executed:
this.init(options)
As can be seen from the name, it is an initialization function.
init function
fn.init = function (options) { //初始化node this.initNode(options); //将内容放进容器 this.parent.html(this.body); //初始化事件 this.initEvent(); //获取列表 this.getList(); };
fn is Comment.prototype, which is only mentioned once and will not be mentioned again.
Initialization means there are 4 tasks to be done. As you can see from the code comments, we will explain them one by one now
initNode function
It can be seen from the name that the main initialization node or cache dom
fn.initNode = function(options){ //init wrapper box if (!!options.parent) { this.parent = options.parent[0].nodeType == 1 ? options.parent : $('#' + options.parent); }; if (!this.parent) { this.parent = $('div'); $('body').append(this.parent); } //init content this.body = (function(){ var strHTML = '<div class="m-comment">' + '<div class="cmt-form">' + '<textarea class="cmt-text" placeholder="欢迎建议,提问题,共同学习!"></textarea>' + '<button class="u-button u-login-btn">提交评论</button>' + '</div>' + '<div class="cmt-content">' + '<div class="u-loading1"></div>' + '<div class="no-cmt">暂时没有评论</div>' + '<ul class="cmt-list"></ul>' + '<div class="f-clear">' + '<div class="pager-box"></div>' + '</div>' + '</div>' + '</div>'; return $(strHTML); })(); //init other node this.text = this.body.find('.cmt-text').eq(0); this.cmtBtn = this.body.find('.u-button').eq(0); this.noCmt = this.body.find('.no-cmt').eq(0); this.cmtList = this.body.find('.cmt-list').eq(0); this.loading = this.body.find('.u-loading1').eq(0); this.pagerBox = this.body.find('.pager-box').eq(0); };
We can see from the code:
this.parent: Saves the container node
this.body: Saves the html of the comment area
this.text: Saves the textarea element of the comment
this.cmtBtn: Saves the submit button
this.noCmt: Saves the text reminder when there are no comments
this.cmtList: The container that holds the list
this.loading: Saves the loading GIF image when loading the list
this.pagerBox: The pager container when paging is required
There are no difficulties in js, they are all jQuery methods
Put content into container
this.parent.html(this.body)
There’s nothing to say about this, it’s very simple. At this time, our comment component should be displayed on the page, but the comment list is not loaded now, and comments cannot be made. Let’s first talk about loading the comment list
getList function
The first is to initialize the list, clear it, display the loading gif image, hide the reminder words without comments, and initiate an ajax request when ready.
The idea is to use php to get all the messages in the comment area and organize them on the front end. The ajax request is:
fn.resetList = function(){ this.loading.css('display', 'block') this.noCmt.css('display', 'none'); this.cmtList.html(''); }; fn.getList = function(){ var self = this; this.resetList(); $.ajax({ url: self.getCmtUrl, type: 'get', dataType: 'json', data: { id: self.belong }, success: function(data){ if(!data){ alert('获取评论列表失败'); return !1; }; //整理评论列表 self.initList(data); self.loading.css('display', 'none'); //显示评论列表 if(self.lists.length == 0){ //暂时没有评论 self.noCmt.css('display', 'block'); }else{ //设置分页器 var total = Math.ceil(self.lists.length / self.offset); self.pager = new Pager({ index: 1, total: total, parent: self.pagerBox[0], onchange: self.doChangePage.bind(self), label:{ prev: '<', next: '>' } }); } }, error: function(){ alert('获取评论列表失败'); } }); };
Get form, and then send the id to it. The data obtained is expected to be a list array.
I won’t talk about the content of php, but the sql statement is posted below:
$id = $_GET['id']; $query = "select * from comments where belong=$id order by time"; ... $str = '['; foreach ($result as $key => $value) { $id = $value['id']; $username = $value['username']; $time = $value['time']; $content = $value['content']; $parent = $value['parent']; $str .= <<<end { "id" : "{$id}", "parent" : "{$parent}", "username" : "{$username.'", "time" : "{$time}", "content" : "{$content}", "response" : [] } end; } $str = substr($str, 0, -1); $str .= ']'; echo $str;
What is obtained is a json string. jQuery's ajax can convert it into json data. The obtained data is as follows:
If the loading is successful, then what we get is a bunch of data. We are now in the success callback function. The data needs to be sorted before it can be displayed, because now all comment replies belong to the same layer.
initList function
fn.initList = function (data) { this.lists = []; //保存评论列表 this.keys = {}; //保存评论id和index对应表 var index = 0; //遍历处理 for(var i = 0, len = data.length; i < len; i++){ var t = data[i], id = t['id']; if(t['parent'] == 0){ this.keys[id] = index++; this.lists.push(t); }else{ var parentId = t['parent'], parentIndex = this.keys[parentId]; this.lists[parentIndex]['response'].push(t); } }; };
我的思路就是:this.lists放的都是评论(parent为0的留言),通过遍历获取的数据,如果parent为0,就push进this.lists;否则parent不为0表示这是个回复,就找到对应的评论,把该回复push进那条评论的response中。
但是还有个问题,就是因为id是不断增长的,可能中间有些评论被删除了,所以id和index并不一定匹配,所以借助this.keys保存id和index的对应关系。
遍历一遍就能将所有的数据整理好,并且全部存在了this.lists中,接下来剩下的事情就是将数据变成html放进页面就好了。
//显示评论列表 if(self.lists.length == 0){ //暂时没有评论 self.noCmt.css('display', 'block'); }else{ //设置分页器 var total = Math.ceil(self.lists.length / self.offset); self.pager = new Pager({ index: 1, total: total, parent: self.pagerBox[0], onchange: self.doChangePage.bind(self), label:{ prev: '<', next: '>' } }); }
这是刚才ajax,success回调函数的一部分,这是在整理完数据后,如果数据为空,那么就显示“暂时没有评论”。
否则,就设置分页器,分页器我直接用了之前封装的,如果有兴趣可以看看我之前的文章:
面向对象:分页器封装
简单说就是会执行一遍onchange函数,默认页数为1,保存在参数obj.index中
fn.doChangePage = function (obj) { this.showList(obj.index); };
showList函数
fn.showList = (function(){ /* 生成一条评论字符串 */ function oneLi(_obj){ var str1 = ''; //处理回复 for(var i = 0, len = _obj.response.length; i < len; i++){ var t = _obj.response[i]; t.content = t.content.replace(/\<\;/g, '<'); t.content = t.content.replace(/\>\;/g, '>'); str1 += '<li class="f-clear"><table><tbody><tr><td>' + '<span class="username">' + t.username + ':</span></td><td>' + '<span class="child-content">' + t.content + '</span></td></tr></tbody></table>' + '</li>' } //处理评论 var headImg = ''; if(_obj.username == "kang"){ headImg = 'kang_head.jpg'; }else{ var index = Math.floor(Math.random() * 6) + 1; headImg = 'head' + index + '.jpg' } _obj.content = _obj.content.replace(/\<\;/g, '<'); _obj.content = _obj.content.replace(/\>\;/g, '>'); var str2 = '<li class="f-clear">' + '<div class="head g-col-1">' + '<img src="./img/head/' + headImg + '" width="100%"/>' + '</div>' + '<div class="content g-col-19">' + '<div class="f-clear">' + '<span class="username f-float-left">' + _obj.username + '</span>' + '<span class="time f-float-left">' + _obj.time + '</span>' + '</div>' + '<span class="parent-content">' + _obj.content + '</span>' + '<ul class="child-comment">' + str1 + '</ul>' + '</div>' + '<div class="respone-box g-col-2 f-float-right">' + '<a href="javascript:void(0);" class="f-show response" data-id="' + _obj.id + '">[回复]</a>' + '</div>' + '</li>'; return str2; }; return function (page) { var len = this.lists.length, end = len - (page - 1) * this.offset, start = end - this.offset < 0 ? 0 : end - this.offset, current = this.lists.slice(start, end); var cmtList = ''; for(var i = current.length - 1; i >= 0; i--){ var t = current[i], index = this.keys[t['id']]; current[i]['index'] = index; cmtList += oneLi(t); } this.cmtList.html(cmtList); }; })();
这个函数的参数为page,就是页数,我们根据页数,截取this.lists的数据,然后遍历生成html。
html模板我是用字符串连接起来的,看个人喜好。
生成后就 this.cmtList.html(cmtList);这样就显示列表了,效果图看最开始。
现在需要的功能还有评论回复,而init函数中也只剩下最后一个initEvent
initEvent 函数
fn.initEvent = function () { //提交按钮点击 this.cmtBtn.on('click', this.addCmt.bind(this, this.cmtBtn, this.text, 0)); //点击回复,点击取消回复,点击回复中的提交评论按钮 this.cmtList.on('click', this.doClickResponse.bind(this)); };
上面截图来自我的个人网站,当我们点击回复时,我们希望能有地方写回复,可以提交,可以取消,由于这几个元素都是后来添加的,所以我们将行为都托管到评论列表这个元素。
下面先将提交评论事件函数。
addCmt 函数
fn.addCmt = function (_btn, _text, _parent) { //防止多次点击 if(_btn.attr('data-disabled') == 'true') { return !1; } //处理提交空白 var value = _text.val().replace(/^\s+|\s+$/g, ''); value = value.replace(/[\r\n]/g,'<br >'); if(!value){ alert('内容不能为空'); return !1; } //禁止点击 _btn.attr('data-disabled','true'); _btn.html('评论提交中...'); //提交处理 var self = this, email, username; username = $.cookie('user'); if (!username) { username = '游客'; } email = $.cookie('email'); if (!email) { email = 'default@163.com'; } var now = new Date(); $.ajax({ type: 'get', dataType: 'json', url: this.setCmtUrl, data: { belong: self.belong, parent: _parent, email: email, username: username, content: value }, success: function(_data){ //解除禁止点击 _btn.attr('data-disabled', ''); _btn.html('提交评论'); if (!_data) { alert('评论失败,请重新评论'); return !1; } if (_data['result'] == 1) { //评论成功 alert('评论成功'); var id = _data['id'], time = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + ' ' + now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds(); if (_parent == 0) { var index = self.lists.length; if (!self.pager) { //设置分页器 self.noCmt.css('display', 'none'); var total = Math.ceil(self.lists.length / self.offset); self.pager = new Pager({ index: 1, total: total, parent: self.pagerBox[0], onchange: self.doChangePage.bind(self), label:{ prev: '<', next: '>' } }); } self.keys[id] = index; self.lists.push({ "id": id, "username": username, "time": time, "content": value, "response": [] }); self.showList(1); self.pager._$setIndex(1); }else { var index = self.keys[_parent], page = self.pager.__index; self.lists[index]['response'].push({ "id": id, "username": username, "time": time, "content": value }); self.showList(page); } self.text.val(''); } else { alert('评论失败,请重新评论'); } }, error: function () { alert('评论失败,请重新评论'); //解除禁止点击 _btn.attr('data-disabled', ''); _btn.html('提交评论'); } }); }
参数有3个:_btn, _text, _parent 之所以要有这三个参数是因为评论或者回复这样才能使用同一个函数,从而不用分开写。
点击后就是常见的防止多次提交,检查一下cookie中有没有username、email等用户信息,没有就使用游客身份,然后处理一下内容,去去掉空白啊,
换成
等等,检验过后发起ajax请求。
成功后把新的评论放到this.lists,然后执行this.showList(1)刷新显示
php部分仍然不讲,sql语句如下:
$parent = $_GET['parent']; $belong = $_GET['belong']; $content = htmlentities($_GET['content']); $username = $_GET['username']; $email = $_GET['email']; $query = "insert into comments (parent,belong,content,time,username,email) value ($parent,$belong,'$content',NOW(),'$username','$email')"; doClickResponse 函数 fn.doClickResponse = function(_event){ var target = $(_event.target); var id = target.attr('data-id'); if (target.hasClass('response') && target.attr('data-disabled') != 'true') { //点击回复 var oDiv = document.createElement('div'); oDiv.className = 'cmt-form'; oDiv.innerHTML = '<textarea class="cmt-text" placeholder="欢迎建议,提问题,共同学习!"></textarea>' + '<button class="u-button resBtn" data-id="' + id + '">提交评论</button>' + '<a href="javascript:void(0);" class="cancel">[取消回复]</a>'; target.parent().parent().append(oDiv); oDiv = null; target.attr('data-disabled', 'true'); } else if (target.hasClass('cancel')) { //点击取消回复 var ppNode = target.parent().parent(), oRes = ppNode.find('.response').eq(0); target.parent().remove(); oRes.attr('data-disabled', ''); } else if (target.hasClass('resBtn')) { //点击评论 var oText = target.parent().find('.cmt-text').eq(0), parent = target.attr('data-id'); this.addCmt(target, oText, parent); }else{ //其他情况 return !1; } };
根据target.class来判断点击的是哪个按钮。
如果点击回复,生成html,放到这条评论的后面
var oDiv = document.createElement('div'); oDiv.className = 'cmt-form'; oDiv.innerHTML = '<textarea class="cmt-text" placeholder="欢迎建议,提问题,共同学习!"></textarea>' + '<button class="u-button resBtn" data-id="' + id + '">提交评论</button>' + '<a href="javascript:void(0);" class="cancel">[取消回复]</a>'; target.parent().parent().append(oDiv); oDiv = null; target.attr('data-disabled', 'true'); //阻止重复生成html
点击取消,就把刚才生成的remove掉
var ppNode = target.parent().parent(), oRes = ppNode.find('.response').eq(0); target.parent().remove(); oRes.attr('data-disabled', ''); //让回复按钮重新可以点击
点击提交,获取一下该获取的元素,直接调用addCmt函数
var oText = target.parent().find('.cmt-text').eq(0), parent = target.attr('data-id'); this.addCmt(target, oText, parent);
注意: parent刚才生成html时我把它存在了提交按钮的data-id上了。
到此全部功能都实现了,希望对大家的学习有所启发。