먼저 개인 홈페이지의 메시지 페이지에 가시면 효과를 보실 수 있습니다: 메시지 보드
문제를 해결하기 위해 프런트 엔드는 jQuery를 사용하여 작성하고 백엔드는 PHP를 사용하여 MySQL 데이터베이스를 읽고 쓰기만 하면 됩니다.
데이터베이스 설계 및 구현 아이디어
데이터베이스는 아래와 같은 구조를 갖는 comments 테이블을 생성했습니다.
모든 댓글(기사 댓글 답글, 게시판 포함)은 동일한 테이블에 작성되며, 소속 필드에 따라 댓글 영역이 구분됩니다.
동일 댓글 영역에서 parent가 0이면 댓글을 나타내고, parent가 특정 값이면 아이디어가 복잡하지 않은 댓글을 나타냅니다.
여기서 CSS에 대해 이야기하는 것이 아닙니다. 필요에 따라 맞춤설정할 수 있습니다. 이제 패키징을 시작해 보겠습니다.
설정 기능
저희는 필요에 따라 기능을 설정합니다. 우선 제 웹사이트에는 메시지 알림과 인스턴트 메시징 기능이 구현되어 있지 않기 때문에 댓글 답글은 웹마스터나 사용자에게 메시지를 표시하지 않고 메시지 영역에만 영향을 미칩니다. 이므로 다음 기능만 구현하면 됩니다.
1. 댓글 목록 표시
2. 댓글 제출 기능
3. 답장
댓글
댓글 기능을 클래스로 캡슐화하고 인스턴스화를 통해 다양한 댓글 영역을 생성할 수 있으므로 생각하기 어렵지 않습니다.
인스턴스화 시 전달해야 하는 매개변수에는 댓글 영역의 ID, 댓글을 받기 위한 PHP 주소, 댓글 제출을 위한 PHP 주소가 포함될 수 있습니다.
따라서 주석 영역을 인스턴스화하는 코드는 다음과 같을 것으로 추측할 수 있습니다.
var oCmt = new Comment({ parent: $('#box'), //你想要将这个评论放到页面哪个元素中 id: 0, getCmtUrl: './php/getcomment.php', setCmtUrl: './php/comment.php' })
물론 Comment 클래스에 정적 메소드를 정의합니다
Comment.allocate({ parent: $('#box'), id: 0, getCmtUrl: './php/getcomment.php', setCmtUrl: './php/comment.php' })
대부분 동일하고 초기화만 다릅니다
생성자
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; };
내부의 변수와 메소드에 대해 천천히 설명하겠습니다. 할당 메소드를 정의하지 않은 경우에는
과 같이 작성하시면 됩니다.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;
저처럼 항상 함수를 먼저 작성하고 속성 변수를 추가한 다음 나중에 추가해야 합니다. 생성자가 최종적으로 실행되는지 확인하면 됩니다.
this.init(옵션)
이름에서 알 수 있듯이 초기화 기능입니다.
초기화 기능
fn.init = function (options) { //初始化node this.initNode(options); //将内容放进容器 this.parent.html(this.body); //初始化事件 this.initEvent(); //获取列表 this.getList(); };
fn은 Comment.prototype으로 한 번만 언급되고 다시 언급되지 않습니다.
초기화는 4가지 작업을 수행해야 함을 의미합니다. 코드 주석에서 볼 수 있듯이 이제 하나씩 설명하겠습니다
initNode 함수
이름에서 알 수 있듯이 메인 초기화 노드나 캐시돔입니다
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); };
코드에서 확인할 수 있습니다:
this.parent: 컨테이너 노드를 저장합니다
this.body: 댓글 영역의 html을 저장합니다
this.text: 댓글의 텍스트 영역 요소를 저장합니다
this.cmtBtn: 제출 버튼을 저장합니다
this.noCmt: 댓글이 없을 때 문자 알림을 저장합니다
this.cmtList: 목록을 담는 컨테이너
this.loading : 목록 로딩 시 로딩 GIF 이미지를 저장합니다
this.pagerBox: 페이징이 필요할 때의 페이저 컨테이너
js에는 어려움이 없으며 모두 jQuery 메소드입니다
컨테이너에 콘텐츠 담기
this.parent.html(this.body)
이건 말할 것도 없고 아주 간단합니다. 이때 댓글 컴포넌트가 페이지에 표시되어야 하는데 지금은 댓글 목록이 로드되지 않아 댓글을 작성할 수 없습니다. 목록
getList 함수
첫 번째는 목록을 초기화하고, 지우고, 로딩 gif 이미지를 표시하고, 주석 없이 알림 단어를 숨기고, 준비되면 ajax 요청을 시작하는 것입니다.
PHP를 사용하여 댓글 영역의 모든 메시지를 가져와 프런트 엔드에서 정리하는 것이 아이디어입니다. Ajax 요청은 다음과 같습니다.
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('获取评论列表失败'); } }); };
양식을 가져온 다음 ID를 보내면 얻은 데이터가 목록 배열이 될 것으로 예상됩니다.
php 내용에 대해서는 언급하지 않겠지만, sql문은 아래와 같습니다.
$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;
얻은 것은 json 문자열입니다. jQuery의 ajax는 이를 json 데이터로 변환할 수 있습니다.
로드가 성공하면 많은 데이터를 얻게 됩니다. 이제 모든 댓글 답변이 동일한 레이어에 속하므로 데이터를 표시하기 전에 정렬해야 합니다. .
initList 함수
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上了。
到此全部功能都实现了,希望对大家的学习有所启发。