まず、あなたの個人ウェブサイトのメッセージページにアクセスすると、効果が確認できます: メッセージボード
トラブルを避けるために、フロントエンドは jQuery を使用して作成され、バックエンドは PHP を使用して単純に MySQL データベースの読み取りと書き込みを行います。
データベースの設計と実装のアイデア
データベースは、以下に示す構造を持つテーブル comments を作成しました:
すべてのコメント (記事コメントの返信や掲示板を含む) は同じテーブルに書き込まれ、異なるコメント領域は属するフィールドによって区別されます
同じコメント領域では、親が0の場合はコメントを表し、親が特定の値の場合はどのコメントに対する返信を表します。
ここでは CSS について話しているわけではないことに注意してください。必要に応じてカスタマイズできます。
設定機能
まず、私の Web サイトにはメッセージ リマインダーやインスタント メッセージング機能が実装されていないため、コメントの返信は Web マスターやユーザーにメッセージを表示することはなく、メッセージ領域にのみ影響します。したがって、次の関数を単純に実装するだけで済みます:
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; };
内の変数とメソッドについてゆっくり説明します。 assign メソッドを定義しない場合は、
のように記述できます。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 つのタスクを実行する必要があります。ここで 1 つずつ説明します。
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: コメント
の textarea 要素を保存します。
this.cmtBtn: 送信ボタンを保存します
this.noCmt: コメントがない場合にテキストリマインダーを保存します
this.cmtList: リストを保持するコンテナ
this.loading: リストをロードするときに、ロード中の GIF 画像を保存します
this.pagerBox: ページングが必要な場合のページャー コンテナ
js には難しいことはなく、すべて jQuery メソッドです
コンテンツをコンテナに入れる
this.parent.html(this.body)
これについては何も言うことはありません。非常に簡単です。この時点では、コメント コンポーネントがページに表示されるはずですが、コメント リストはまだ読み込まれていないため、コメントを作成することはできません。リスト
getList 関数
1 つ目は、リストを初期化してクリアし、読み込み中の 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上了。
到此全部功能都实现了,希望对大家的学习有所启发。