基于jQuery与PHP的通用留言板组件封装实现

作者:小算盘时间:2026-10-30 05:56:31浏览:182

本文详细讲解如何使用jQuery封装一个通用的前端留言板组件,并结合PHP与MySQL实现后台数据读写。文章从数据库表结构设计入手,解析如何通过belong字段区分不同页面评论,利用parent字段实现嵌套回复。前端部分重点演示了Comment类的实例化、DOM节点初始化、Ajax异步获取与提交评论,以及列表渲染与分页逻辑,提供了一套完整的前后端交互方案。

首先,展示个人网站留言页面的实际效果,大家可以访问留言板查看运行状态。

数据库设计与实现思路

前端开发为简化流程采用jQuery编写,后端则通过PHP脚本对MySQL数据库进行简单的读写操作。

数据库设计与实现思路

接下来探讨数据库的设计方案及整体实现思路。

数据库中创建了一个名为comments的数据表,具体结构参考下图。

数据库设计与实现思路

所有类型的评论(包括文章下方的评论回复以及独立的留言板内容)均存储在同一张表中,通过belong字段来区分不同的评论区归属。

在同一个评论区内部,parent字段值为0时表示这是一条独立评论;若parent字段为其他数值,则代表这是对某条特定评论的回复,整体逻辑并不复杂。

请注意,本文不涉及CSS样式设计,读者可根据实际需求自行定制样式,现在开始进行代码封装。

明确功能需求。

根据实际业务需求确定功能范围,由于当前网站并未实现消息提醒或即时通讯功能,因此评论回复不会向站长或用户发送通知,仅在前端留言区产生视觉效果。基于此,我们只需实现以下核心功能:

1、显示评论列表

2、支持提交新评论

3、支持对已有评论进行回复

jQuery评论类封装实现

评论类设计。

我们将评论功能封装为一个JavaScript类,通过实例化该类即可创建不同的评论区实例,因此可以推测出以下实现方式:

在实例化对象时,我们需要传入的参数可能包括:评论区的唯一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,下文不再赘述。

初始化的工作包含四个方面,从代码注释中可以清晰看出,现在逐一进行讲解。

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:保存的是需要分页时的分页器容器。

JavaScript部分没有难点,主要调用了一些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);
  }

 };
};

我的思路是:将parent为0的留言(即独立评论)放入this.lists数组中;如果parent不为0,则表示这是一条回复,需要找到对应的父评论,并将该回复push进该父评论的response数组中。

但存在一个问题,由于id是递增生成的,中间可能存在被删除的评论,导致id与数组索引不匹配,因此借助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(/\&lt\;/g, '<');
   t.content = t.content.replace(/\&gt\;/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(/\&lt\;/g, '<');
  _obj.content = _obj.content.replace(/\&gt\;/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

点击取消,则移除刚才生成的元素。


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属性上。

至此,所有功能均已实现,希望对大家的学习有所启发。

作者最新文章