Nodejs之MEAN棧開發(九)---- 用戶評論的增加/刪除/修改


由於工作中做實時通信的項目,需要用到Nodejs做通訊轉接功能,剛開始接觸,很多都不懂,於是我和同事就准備去學習nodejs,結合nodejs之MEAN棧實戰書籍《Getting.MEAN.with.Mongo.Express.Angular.and.Node.2015.11》,我們完成了一個小型的ReadClubing項目,結合書中講解和步驟,我們完成了不同的功能,當然由於時間原因,還有很多不完善的地方,后續我們會繼續開發。

同事負責開發的內容為:

Nodejs之MEAN棧開發(一)---- 路由與控制器

Nodejs之MEAN棧開發(二)----視圖與模型

Nodejs之MEAN棧開發(三)---- 使用Mongoose創建模型及API

Nodejs之MEAN棧開發(四)---- form驗證及圖片上傳

Nodejs之MEAN棧開發(五)---- Angular入門與頁面改造

Nodejs之MEAN棧開發(六)---- 用Angular創建單頁應用(上)

Nodejs之MEAN棧開發(七)---- 用Angular創建單頁應用(下)

Nodejs之MEAN棧開發(八)---- 用戶認證與會話管理詳解

我開發的內容為:

Nodejs之MEAN棧開發(九)---- 用戶評論的增加/刪除/修改

針對這次Nodejs之MEAN棧開發ReadClubing項目,我主要負責是標題詳情頁面,包括頁面布局、數據展示、評論的增加/刪除/修改等工作。

1、標題詳情頁面布局和數據展示

 標題詳情頁操作如圖:

因為ReadClub的項目的結構是按照MVC的形式來開發的,首先找到程序入口,即路由app_client/app.js,app.js主要是按照Angular提供的內置模塊$routeProvider實現路由轉接功能,鏈接到詳情頁的代碼如下:

(function() {
    angular.module('readApp', ['ngRoute', 'ngSanitize'])
    .config(['$routeProvider', '$locationProvider', config]);
    function config($routeProvider, $locationProvider) {
        $routeProvider
          .when('/topicDetail/:topicid', {
              templateUrl: 'topicDetail/topicDetail.html',
              controller: 'topicDetailCtrl',
              caseInsensitiveMatch: true,
              controllerAs: 'vm'
          })
         .otherwise({ redirectTo: '/' });
         $locationProvider.html5Mode(true);
    }
)();

因此頁面被跳轉到詳情頁topicDetail.html,詳情頁的數據依賴於控制器topicDetailCtrl。

同樣是遵循Angular頁面獲取數據的方式,頁面中需要展示數據的地方都以{{value}}的形式表示,中間value的輸出是依靠控制器topicDetailCtrl從數據庫獲取到的數據。topicDetail.html代碼如下:

<navigation></navigation>
<div id="bodycontent" class="container">
    <div class="row">
        <div class="col-md-9">
            <div class="content">
                <div class="topic_top backcolor">
                     <div class="title">{{vm.topic.title}}</div>
                     <div class="topic_content">{{vm.topic.content}}</div>
                </div>
                <div class="comment backcolor">
                     <div class="commenttip">{{vm.topic.comments.length}} 回復</div>
                     <div id="commentcontent" ng-repeat="comments in vm.topic.comments">
                         <div class="cell reply_area reply_item ">
                                <div class="author_content">
                                    <a href="" class="user_avatar">
                                      <img src="https://avatars.githubusercontent.com/u/3088175?v=3&s=120"/>
                                    </a>
                                    <div class="user_info">
                                        <a class="dark reply_author" href="/user/{{vm.topic.author}}">{{vm.topic.author}}</a>
                                        <a class="reply_time">{{comments.createdOn | jsonDate:'yyyy-MM-dd HH:mm:ss'}}</a>
                                    </div>
                                    <div class="user_action">
                                       <a ng-href="#" ng-click="vm.editReply(comments._id)" id="{{comments._id}}">
                                            <i class="fa fa-pencil-square-o" title="編輯"></i>
                                       </a>
                                        <a ng-href="#" ng-click="vm.deleteReply(comments._id)">
                                            <i class="fa fa-trash" title="刪除"></i>
                                       </a>
                                    </div>
                                </div>
                                <div class="reply_content from-rochael">
                                    <div class="markdown-text" ng-bind-html="comments.content | trustHtml">
                                    </div>
                                </div>
                         </div>
                     </div>
                </div>
                <div class="commentarea backcolor">
                     <div class="header">添加回復</div>
                       <div class="inner">
                              <div id="summernote" ng-model="summernote" ng-summernote ></div>
                              <input id="topicid" type="hidden" value="{{vm.topic._id}}" />
                              <input id="username" type="hidden" value="Smartlin" />
                              <div class="editor_buttons">
                                     <span ng-show="vm.reply" class="submit" ng-click="vm.submitReply()">回復</span>
                                     <span ng-show="!vm.reply" class="submit"  ng-click="vm.updateReply()">回復</span>
                              </div>
                       </div>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="userinfo">
                <p>stoneniqiu</p>
            </div>
        </div>
</div>
<footer-nav></footer-nav>
View Code

理解控制器獲取數據:同理,在topicDetail文件下新建topicDetail.controller.js。遵循Angular的路由規則,由於實現的視圖和控制器的綁定關系,視圖加載時,控制器會立即調取數據給頁面輸出。

我們來看看topicDetail.controller.js的邏輯,代碼如下:

(function () {
    angular.module('readApp')
    .controller('topicDetailCtrl', topicDetailCtrl)
    .directive('ngSummernote', getSummernote)
    
    topicDetailCtrl.$inject = ['$scope','$http', '$routeParams', 'topicData'];
    function topicDetailCtrl($scope,$http, $routeParams, topicData) {
        var vm = this;
        vm.topicid = $routeParams.topicid;
        vm.reply = true;
        topicData.getTopicById(vm.topicid).success(function (data) {
            vm.topic = data;
        }).error(function (e) {  
            vm.message = "Sorry, something's gone wrong ";
        })
    }

    function getSummernote(){
        return {
            restrict : 'A',
            require : 'ngModel',
            link : function($scope, $element, $attrs, $ngModel){
                if (!$ngModel) {
                    return;
                }
                $($element).summernote({
                    height: 150,                 
                    minHeight: 150,             
                    maxHeight: 150,             
                    focus: true 
                })
            },
        };
    }
})();
topicData.getTopicById(vm.topicid),通過傳入topicid查詢數據庫,鏈接到app_client/common/services/ReadData.service.js,主要通過angular定義的service,請求數據模塊$http,實現路由轉接功能,代碼如下:
angular
.module('readApp')
.service('topicData', topicData)

topicData.$inject = ['$http'];
function topicData($http) {
    var getTopicById = function (topicid) {
        return $http.get('/api/topics/' + topicid);
    };
    return {
        getTopicById: getTopicById
    };
};
$http.get('/api/topics/' + topicid),根據路由跳轉到app_api/routes/index.js,通過node的router規則實現跳轉。代碼如下:
router.get('/topics/:topicid', topicCtrl.topicReadOne);

在app_api/controller/topic.js中實現路由方法,代碼如下:

module.exports.topicReadOne = function (req, res) {
    var topicid = req.params.topicid;
    console.log("topicid:"+topicid);
    if (!topicid) {
        sendJSONresponse(res, 404, {
            "message": "Not found, topicid is required"
        });
        return;
    }
    TopicModel.findById(topicid).exec(function (err, topic) {
        if (!topic) {
            sendJSONresponse(res, 404, {
                "message": "topicid not found"
            });
            return;
        } else if (err) {
            sendJSONresponse(res, 400, err);
            return;
        }
        sendJSONresponse(res, 200, topic);

    });
}

返回的數據topic在topicDetail.controller.js中通過vm.topic = data來接收,到這里,視圖中通過vm.topic依賴的數據就會展現出來。

詳情頁面展示需要注意的幾點細節:

(1)、富文本框summernote的顯示

沒有運用angular的html頁面,顯示summernote的方法:首先,在html中寫入標簽 <div id="summernote"></div>;其次,引入支持相應的summernote.css和summernote.js文件即可,代碼如下:

 <link rel='stylesheet' href='http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.1/summernote.css'/>
    <div id="summernote"></div>
 <script src='http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.1/summernote.js'></script>

但在使用angular框架加載html頁面時,會導致富文本框summernote無法顯示,解決方法為:

第一步:在html頁面中增加ng-model,ng-summernote,代碼如下:

 <div id="summernote" ng-model="summernote" ng-summernote ></div>

第二步:在控制器topicDetail.controller.js中,嵌入指令directive('ngSummernote', getSummernote),代碼如下:

angular.module('readApp',[])
.directive('ngSummernote', getSummernote)
 function getSummernote(){
    return {
        restrict : 'A',
        require : 'ngModel',
        link : function($scope, $element, $attrs, $ngModel){
            if (!$ngModel) {
                return;
            }
            $($element).summernote({//初始化方法
                height: 150,                 
                minHeight: 150,             
                maxHeight: 150,             
                focus: true 
            })
        },
    };
}

(2)、編輯和刪除圖標的顯示

編輯和刪除圖標的顯示,需要引用font-awesome.min.css,fontawesome是一套絕佳的圖標字體庫和CSS框架,為您提供可縮放的矢量圖標,您可以使用CSS所提供的所有特性對它們進行更改,包括:大小、顏色、陰影或者其它任何支持的效果。引入的方式如下:

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"/>

(3)、編輯事件editReply()和刪除事件deleteReply()傳參

在ng-repeat執行循環過程中,因為這兩個事件需要傳入評論的id,方法為:vm.editReply(comments._id)和vm.deleteReply(comments._id),直接傳入comments._id,不需要用{{}};而其他不需要作為參數傳遞給方法的值,顯示的方法是需要加{{}},如:{{comments.createdOn}},代碼如下:

(4)、關於angularJS綁定數據時自動轉義html標簽

因為我們追加的評論保存到數據庫時會有帶html標簽的情況,如加粗、設置顏色的字體,圖片等,當取出展示在評論區時會帶有html標簽,因此需要轉義,實現轉義的方法如下:

第一步:在html中綁定過濾器trustHtml,代碼如下:

 <div class="markdown-text" ng-bind-html="comments.content | trustHtml"></div>

第二步:在控制器topicDetail.controller.js中,設置過濾器,代碼如下:

angular.module('readApp',[])
.filter('trustHtml', getTrust)
getTrust.$inject = ['$sce'];

function getTrust($sce) {
    return function (input) {
        return $sce.trustAsHtml(input);
    }
}

2、追加評論

追加評論通過回復按鈕,但是當我們修改評論時,將要修改的評論內容放到富文本框中,也要點擊回復按鈕,因此會出現沖突,解決方法:

首先,在html頁面中定義兩個綁定不同事件的回復按鈕,通過angular內置指令ng-show來實現兩個按鈕的顯示和隱藏,從而來實現不同的功能,代碼如下:

<span ng-show="vm.reply" class="submit" ng-click="vm.submitReply()">回復</span>
<span ng-show="!vm.reply" class="submit"  ng-click="vm.updateReply()">回復</span>

其次,在初進詳情頁時,設置vm.reply = true來顯示增加評論的按鈕,當點擊編輯評論時,則設置vm.reply = false來顯示編輯評論的按鈕,最后修改的評論成功提交后,又設置vm.reply = true切換到增加評論的按鈕。

通過入口回復按鈕submitReply(),鏈接到控制器topicDetail.controller.js,代碼如下:

vm.submitReply = function () {
    var code = $('#summernote').summernote('code');
    if (code == "") {
        alert("輸入的內容不能為空~");
        return;
    }
    var params = {
        topicid: vm.topicid,
        content: code
    };
    topicData.addComment(params).success(function (data) {
        vm.message = data.length > 0 ? "" : "暫無數據";
        vm.topic.comments.push(data);//數據庫和頁面上的comments對象都要進行數據更新;
        $('#summernote').summernote('code', '');
    }).error(function (e) {
        console.log(e);
    });
};

通過topicData.addComment(params),鏈接到ReadData.service.js,通過綁定service()顯示路由,代碼如下:

function topicData ($http) {
     var addComment = function(data){
        return $http({
            method: 'POST',
            url: "/api/topics/" + data.topicid,
            data: data
        })
    };
    return {
        addComment: addComment,
    }
}

 通過$http()請求,鏈接到app_api/routes/index.js,代碼如下:

router.post('/topics/:topicid', topicCtrl.commentAppendOne);

通過topicCtrl.commentAppendOne,鏈接到app_api/controller/topic.js,代碼如下:

//讀取所有的有關這個topicid的評論
module.exports.commentAppendOne = function (req, res) {
    var topicid = req.params.topicid;
    if (!topicid) {
        sendJSONresponse(res, 404, {
            "message": "Not found, topicid is required"
        });
        return;
    }
    TopicModel.findById(topicid)
        .select('comments')
        .exec(function (err, topic) {
        if (!topic) {
            sendJSONresponse(res, 404, {
                "message": "topic not found"
            });
            return;
        } else if (err) {
            sendJSONresponse(res, 400, err);
            return;
        }
        doAddComment(req, res, topic);
    });
}

var doAddComment = function (req, res, topic) {
    if (!topic) {
        sendJSONresponse(res, 404, "topicid not found");
    } else {
        //console.log("user:", req.body.user);
        //console.log(req.body.content);
        topic.comments.push({
            user: req.body.user,
            createdOn: req.body.createdOn,
            content: req.body.content
        });
        topic.save(function (err, topic) {
            var thisReview;
            if (err) {
                sendJSONresponse(res, 400, err);
            } else {
                var length = topic.comments.length - 1;
                //console.log("length:",length);
                thisReview = topic.comments[length];
                //console.log("thisReview:", querystring.stringify(thisReview));
                sendJSONresponse(res, 200, thisReview);
            }
        });
  }
}
View Code

最后,在控制器中通過成功之后的回調函數,通過push()方法將要追加的數據放到數組topic.comments中,實現數據更新;評論追加成功后,記得清空富文本框里面的值,方法為:$('#summernote').summernote('code', '');

3、刪除評論

點擊刪除圖標,觸發刪除評論事件vm.deleteReply(comments._id),需要傳入評論id,確保要刪除的是哪條評論。根據入口,鏈接到控制器topicDetail.controller.js,代碼如下:

vm.deleteReply = function (commentid) {
    var params = {
        topicid: vm.topicid,
        commentid: commentid
    };
    topicData.deleteComment(params).success(function (data) {
        if (confirm("確定刪除?")) {
            for (var i = 0; i < vm.topic.comments.length; i++) {
                if (vm.topic.comments[i]._id == commentid) {
                    vm.topic.comments.splice(vm.topic.comments.indexOf(vm.topic.comments[i]._id), 1);
                }
            }
        }
    }).error(function (e) {
        console.log(e);
    });
}

通過topicData.deleteComment(params),鏈接到ReadData.service.js,代碼如下:

function topicData ($http) {
     var deleteComment = function (data) {
        console.log("deleteComment,topicid:", data.topicid, "commentid:", data.commentid);
        return $http.delete('/api/topics/' + data.topicid + '/comments/' + data.commentid);
    };
    return {
          deleteComment: deleteComment
    }
}

通過$http.delete()請求,鏈接到app_api/routes/index.js,代碼如下:

router.delete('/topics/:topicid/comments/:commentid', topicCtrl.commentDeleteOne);

通過topicCtrl.commentDeleteOne,鏈接到app_api/controllers/topic.js,代碼如下:

module.exports.commentDeleteOne = function (req, res) {
    if (!req.params.topicid || !req.params.commentid) {
        sendJSONresponse(res, 404, {
            "message": "Not found, topicid and commentid are both required"
        });
        return;
    }
    TopicModel
    .findById(req.params.topicid)
    .select('comments')
    .exec(
        function (err, topic) {
            if (!topic) {
                sendJSONresponse(res, 404, {
                    "message": "topicid not found"
                });
                return;
            } else if (err) {
                sendJSONresponse(res, 400, err);
                return;
            }
            if (topic.comments && topic.comments.length > 0) {
                console.log("length:", topic.comments.length);
                if (!topic.comments.id(req.params.commentid)) {
                    sendJSONresponse(res, 404, {
                        "message": "commentid not found"
                    });
                } else {
                    topic.comments.id(req.params.commentid).remove();
                    topic.save(function (err) {
                        if (err) {
                            sendJSONresponse(res, 404, err);
                        } else {
                            sendJSONresponse(res, 204, null);
                        }
                    });
                }
            } else {
                sendJSONresponse(res, 404, {
                    "message": "No comment to delete"
                });
            }
        }
    );
};
View Code

最后,在控制器中通過成功之后的回調函數,變量評論數組找到要刪除的那條評論,通過splice()從數組中刪除,實現頁面數據更新。

4、修改評論

 點擊編輯圖標,觸發編輯評論事件vm.editReply(comments._id),需要傳入評論id,確保要編輯的是哪條評論。根據入口,鏈接到控制器topicDetail.controller.js,注意編輯評論需要分為兩步:

第一步、將要編輯的評論內容追加到富文本框中,進行修改,代碼如下:

vm.editReply = function (commentid) {
    document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;
    var editContent = angular.element(document.getElementById(commentid)).parent('.user_action').parent('.author_content').next('.reply_content').children().text();
    $('#summernote').summernote('code', editContent);
    vm.reply = false;
    vm.commentid = commentid;
}

第二步、通過點擊編輯評論的回復按鈕,來實現修改評論的提交,代碼如下:

vm.updateReply = function () {
    vm.code = $('#summernote').summernote('code');
    var params = {
        topicid: vm.topicid,
        commentid: vm.commentid,
        editContent: vm.code
    };
    topicData.updateComment(params).success(function (data) {
        console.log(data);
        for(var i = 0;i < data.comments.length;i++){    
            if(data.comments[i]._id == vm.commentid){
                console.log('id into');
                 data.comments[i].content = vm.code;
                 vm.topic.comments = data.comments;
                 $('#summernote').summernote('code', '')
            }
        }
        vm.reply = true;

    }).error(function (e) {
        console.log(e);
    });
}

根據topicData.updateComment(params),鏈接到ReadData.service.js,代碼如下:

function topicData ($http) {
     var updateComment = function (data) {
        return $http({
            method:'POST',
            url:'/api/topics/' + data.topicid + '/comments/' + data.commentid,
            data: { editContent: data.editContent}
        })
    };
    return {
        updateComment: updateComment
    };
}

通過$http(),post方式請求,鏈接到app_api/routes/index.js,代碼如下:

router.post('/topics/:topicid/comments/:commentid', topicCtrl.commentUpdateOne);

通過topicCtrl.commentUpdateOne,鏈接到app_api/controllers/topic.js,代碼如下:

module.exports.commentUpdateOne = function (req, res) {
    if (!req.params.topicid || !req.params.commentid) {
        sendJSONresponse(res, 404, {
            "message": "Not found, topicid and commentid are both required"
        });
        return;
    }
    TopicModel
    .findById(req.params.topicid)
    .select('comments')
    .exec(
        function (err, topic) {
            if (!topic) {
                sendJSONresponse(res, 404, {
                    "message": "topicid not found"
                });
                return;
            } else if (err) {
                sendJSONresponse(res, 400, err);
                return;
            }
            if (topic.comments && topic.comments.length > 0) {
                if (!topic.comments.id(req.params.commentid)) {
                    sendJSONresponse(res, 404, {
                        "message": "commentid not found"
                    });
                } else {
                   
                    for(var i= 0;i<topic.comments.length;i++){
                        if(topic.comments[i]._id == req.params.commentid){
                            topic.comments[i].content = req.body.editContent;
                            topic.save(function (err, topic) {
                               if (err) {
                                   sendJSONresponse(res, 404, err);
                               } else {
                                   sendJSONresponse(res, 200, topic);
                               }
                            });
                        }
                    }

                }
            } else {
                sendJSONresponse(res, 404, {
                    "message": "No comment to update"
                });
            }
        }
    );
};

var updateContentById = function (req, res, commentid) {
    CommentModel
    .findById(commentid)
    .select('content')
    .exec(
       function (err, content) {
           if (!content) {
               sendJSONresponse(res, 404, {
                   "message": "content not found"
               });
               return;
           } else if (err) {
               sendJSONresponse(res, 400, err);
               return;
           }
           sendJSONresponse(res, 200, content);
           topic.save(function (err, comments) {
               if (err) {
                   sendJSONresponse(res, 404, err);
               } else {
                   sendJSONresponse(res, 200, book);
               }
           });
       }
    );
}
View Code

最后,通過控制器中執行成功后的回調函數,通過循環找到要修改的那個評論id,根據那條id去更新對應的評論內容,實現頁面數據更新。

細節:如果在node頁面console.log("漢字")或者后端接收到帶有中文的數據或者使用node提供的代碼壓縮uglifyJs.minify(),cmd窗口會報錯,原因是windows系統自帶的cmd窗口不能識別utf-8字符編碼,解決方法如下:

1、打開CMD.exe命令行窗口;

2、通過 chcp命令改變代碼頁,UTF-8的代碼頁為65001;

3、修改窗口屬性,改變字體

在命令行標題欄上點擊右鍵,選擇"屬性"->"字體",將字體修改為True Type字體"Lucida Console",然后點擊確定將屬性應用到當前窗口。如下圖所示:

     

這時使用type命令就可以顯示UTF-8文本文件的內容了:

type filename.txt

4、通過以上操作並不能完全解決問題,因為顯示出來的內容有可能不完全。可以先最小化,然后最大化命令行窗口,文件的內容就完整的顯示出來了。

5、另外提供一些chcp命令的參考:

chcp 65001  就是換成UTF-8代碼頁

chcp 936 可以換回默認的GBK

chcp 437 是美國英語  

 源碼:https://github.com/stoneniqiu/ReadingClub (注意不同分支)

小結:這一節主要講到了如何在Angular頁面來加載Jquery組件如富文本框summernote,調取數據和展示數據時應該注意的幾點細節,評論的增加、刪除、修改,其實內容不是很多,主要要理清思路,根據項目mvc的邏輯結構,自己走一遍,應該就沒問題了。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM