• 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的逻辑结构,自己走一遍,应该就没问题了。

     
  • 相关阅读:
    tree-cli 自动生成项目目录结构
    按需导入vant-ui
    全局导入vant-ui
    mook使用流程
    axios使用流程
    Vuex使用流程
    vue-router使用流程
    img的complete和onload
    react-redux 如何在子组件里访问store对象
    ES6中的Export/import操作的是引用
  • 原文地址:https://www.cnblogs.com/wdlhao/p/5733884.html
Copyright © 2020-2023  润新知