首先,上個人網站的留言頁面,大家可以看看效果:留言板

前端為了省事,使用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上了。
到此全部功能都實現了,希望對大家的學習有所啟發。