首先,上個人網站的留言頁面,大家可以看看效果:留言板
前端為了省事,使用jQuery編寫,後台使用php簡單讀寫MySQL資料庫。
資料庫設計和實現思路
資料庫建立了一個表:comments,結構如下圖:
全部評論(包括文章評論回複,留言板)都寫在同一張表中,不同的評論區用欄位belong區分
同一個評論區裡,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;};
裡面的變數和方法我們慢慢解釋,如果你不定義一個allocate方法,那麼可以寫成:
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(options)
從名字可以看出是初始化函數。
init函數
fn.init = function (options) { //初始化node this.initNode(options); //將內容放進容器 this.parent.html(this.body); //初始化事件 this.initEvent(); //擷取列表 this.getList();};
fn為Comment.prototype,只說一次,下面就不再說了。
初始化就是有4個工作要做,從代碼注釋可以看出,現在一個一個講解
initNode函數
從名字可以看出主要初始化節點或者緩衝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);};
代碼中我們可以看出:
this.parent : 儲存的是容器節點
this.body : 儲存的是評論區的html
this.text : 儲存的是評論的textarea元素
this.cmtBtn : 儲存的是提交按鈕
this.noCmt : 儲存的是沒有評論時的文字提醒
this.cmtList : 儲存的是列表的容器
this.loading : 儲存的是載入列表時的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('擷取評論列表失敗'); } });};
get形式,然後傳送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資料,獲得的資料如下:
如果載入成功,那麼我們得到的是一堆的資料,我們現在是在success回呼函數裡,資料需要整理,才能顯示,因為現在所有的評論回複都屬於同一層。
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等使用者資訊,沒有就使用遊客身份,然後處理一下內容,去去掉空白啊,\n換成 <br> 等等,檢驗過後發起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上了。
到此全部功能都實現了,希望對大家的學習有所啟發。