MiniComment_动态评论系统_Nodejs_Vue_Mongodb_综合实践 | Word count: 2.7k | Reading time: 12min | Post View:
1. 前言 之前因为用到frida,相对系统的学习了一下JavaScript。然后又接触了jquery,Node.js,同时也关联的学习了Vue.js和mongodb。学了不少,感觉需要来实践一下。前一段时间自己搞了个hexo的博客 ,由于是静态博客,没法实现动态评论。我们往往采取动静结合的方法,即博客本身为静态,外加动态评论。
第三方的感觉也是感觉个别地方有点问题,比如说github相关的必须用github账号才能评论;disqus国内访问不了;valine需要实名认证麻烦。
虽然开源的评论系统也有,虽然功能很强大,但是都是比较庞大,不够精简,改起来也比较麻烦。 简化版的评论代码不是很多,于是就想着自己搞一个了,正好作为实践,了解一下前后端如何配合的。同时我认为这个例子对于初接触web开发的人,是个很不错的参考例子。
MiniComment 评论系统前端由Vue编写而来,实现了分页系统,结合marked
可以渲染markdown格式(见下图)。可以点reply回复指定的楼层,点击并跳转到引用处(暂时还没做子楼层)。并且有输入验证码回复的功能。前端页面可以嵌套到任意页面中,也可以通过$.load()
动态加载,用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> <meta article_title ="Comments" api_host ="http://localhost:3003" /> <meta comment_view_limit ="10" page_limit ="10" /> <script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/jquery.min.js" > </script > <script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/jquery.min.js" > </script > <script src ="https://cdn.jsdelivr.net/npm/[email protected] " > </script > <script src ="https://cdn.jsdelivr.net/npm/[email protected] /marked.min.js" > </script > <script src ="https://cdn.jsdelivr.net/gh/highlightjs/[email protected] /build/highlight.js" > </script > <link rel ="stylesheet" href ="https://cdn.jsdelivr.net/gh/highlightjs/[email protected] /build/styles/vs.min.css" /> <script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/highlightjs-line-numbers.min.js" > </script > <div id ="mini_comment" > </div > <script > $("#mini_comment" ).load ("/ui_comment.xhtml" ); </script >
后端是Node.js和mongodb,Node.js理论上可以部署在cloudflare的worker里,mongodb可以用atlas的云服务,可以做到serverless的后端。
用法和源码详见我的github MiniComment ,如果觉得还不错,还请点个star~
实例demo可以见我的博客 Comment ~
3. 数据库设计 数据库采取了mongodb,因为nosql比较轻量配置和修改起来也比较方便。但是为了方便维护,还是采取了2范式
的传统关系结构。数据类型有mongoose
的shceme
来定义。
数据库定义与相关操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const mongoose = require ('mongoose' );const commentSchema = new mongoose.Schema ({ article_title : String , date : {type :Date , default :new Date ()}, ref : {type :mongoose.ObjectId , default :null }, idx : Number , name : String , content : String , _email : String , _hide :{type :Boolean , default :false }, }) const Comment = new mongoose.model ("comments" , commentSchema);async function getCommentCount (article_title ){ return await Comment .find ({"article_title" :article_title, _hide :false }) .countDocuments (); } async function getComment (article_title, skip, limit ){ var comments = await Comment .find ({article_title :article_title, _hide :false }, {_email :0 , _hide :0 , __v :0 }) .skip (skip) .limit (limit) .sort ({idx :-1 }); return comments; } async function submitComment (article_title, ref, name, email, content ){ idx = await Comment .find ({article_title :article_title}).countDocuments (); if (ref!=undefined ){ res = await Comment .find ({id :ref, article_title :article_title}) if (res==[]) return false ; } comment = new Comment ({ article_title : article_title, date : new Date (), ref : ref, idx : idx+1 , name : name, content : content, _email : email, }) if (res=await comment.save ()){ console .log (res) return true ; } return false ; } module .exports = {Comment , getCommentCount, getComment, submitComment}
4. 后端api设计与express中间件 目前我们需要实现几个功能:
获取评论数量(count)/api/comment/count
获取一部分评论(skip,limit)并默认按照时间倒叙排序 /api/comment/get
提交评论 /api/comment/submit
请求验证码用于提交评论 /api/captcha
这里采取了express
这个轻量级的框架,路由功能和各种中间件的理念非常值得学习。中间件的概念特别像流水线,结果前面中间件处理,通过next
函数传给下一个中间件。
中间件常用方法:
app.use(path, mid)
全局使用中间件函数async (req, res, next) =>{}
,
中间件函数async (req, res, next) =>{}
加到参数里。
路由中间件router = express.Router()
,定义了get或post的各种路由,然后在app.use('/', router)
使用。
关于验证和log等公用方法,可以写成自定义中间件方便管理。见logMid
, authCaptchaMid
为了让代码清楚整洁,全部采取了async
await
的方式。
server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const PORT = 3003 ;const express = require ('express' );var bodyParser = require ('body-parser' ); const {api_comment_router} = require ('./api_comment' );console .log (api_comment_router)const app = express ();app.use (bodyParser.json ()); app.use (bodyParser.urlencoded ({ extended : false })); app.use ('/' , api_comment_router); var server = app.listen (PORT , function ( ) { var host = server.address ().address ; var port = server.address ().port ; console .log ("comment server at http://%s:%s" , host, port); })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 const express = require ('express' );const crypto = require ('crypto' );const svgCaptcha = require ('svg-captcha' );const {Comment , getComment, submitComment, getCommentCount} = require ('./model_comment' ); const router = express.Router ();var SALT = svgCaptcha.randomText (4 );(function loop (interval ){ SALT = svgCaptcha.randomText (4 ); setTimeout ( ()=> {loop (interval)}, interval) })(600000 ); const corsMid = async (req, res, next ) => { res.setHeader ("Access-Control-Allow-Origin" , "*" ); res.header ('Access-Control-Expose-Headers' , '*' ); next (); } const authCaptchaMid = async (req, res, next ) => { let text = req.body .captcha_code ; let hash = req.body .captcha_hash ; if (text!=undefined && hash!=undefined ){ let hash2 = crypto.createHash ('sha1' ).update (text.toLowerCase () + SALT ).digest ('hex' ); if (hash2==hash) { next (); return ; } } res.writeHead (400 , {message :"Captcha wrong, Please input again!" }); res.end (); } const logMid = async (req, res, next ) =>{ let time = new Date (new Date ().getTime () - new Date ().getTimezoneOffset ()*60 *1000 ).toLocaleString ('zh' , { hour12 : false , timeZone : 'UTC' }); console .log (time, req.header ('x-forwarded-for' ), req.path , req.query , req.body ); next (); } router.get ('/api/captcha' , logMid, corsMid, async (req, res) => { let cap = svgCaptcha.create ({height :30 , width :90 , fontSize :30 }); let hash = crypto.createHash ('sha1' ).update (cap.text .toLowerCase () + SALT ).digest ('hex' ); res.json ({data :cap.data , hash :hash}); }) router.get ('/api/comment/count' , logMid, corsMid, async (req, res) => { count = await getCommentCount (req.query .article_title ); res.json ({count :count}); }) router.get ('/api/comment/get' , logMid, corsMid, async (req, res) => { var {article_title, limit, skip} = req.query ; skip = parseInt (skip); limit = parseInt (limit); if (limit==undefined || limit == NaN ) limit = 10 ; if (skip==undefined || skip == NaN ) skip = 0 ; comments = await getComment (article_title, skip, limit); res.json (comments); }) router.get ("/api/comment/refidx" , logMid, corsMid, async (req, res) => { var {ref} = req.query ; var comment = await Comment .findById (ref); res.json ({refidx : comment.idx }); return ; }) router.post ('/api/comment/submit' , logMid, corsMid, authCaptchaMid, async (req, res) => { var {article_title, ref, name, email, content} = req.body ; if (await submitComment (article_title, ref, name, email, content)){ res.writeHead (200 , {message :"Submit comment successfully!" }); res.end (); } else { res.writeHead (400 , {message :"Unable to submit comment!" }); res.end (); } }) module .exports = {api_comment_router : router};
5. 前端设计 相比与后端,我认为前端设计比较费劲。对于没什么前端经验的来说,排版真是挺麻烦的。首先需要设计,然后对齐是真的头疼,调css后死活不动,要么就全乱套了。
为了简化,不用vue-cli
也不用webpack
,全纯手写html
,css
和Vue.js
代码。Vue的设计理念就是数据驱动,数据与显示试图同步。此项目前端主要思想是:
用$.get
来调用api,得到当前页的评论,同时缓存评论
v-for
和结合marked
渲染得到的评论,同时根据评论数量来选择分页,分页跳转再次获取评论
@submit
用$.post
提交评论
ajax后端交互部分代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 async function get_comment_count ( ) { return new Promise (resolve => { $.ajax (API_HOST + "/api/comment/count" , { dataType :'json' , data : {article_title :article_title}}) .done (function (data ){return resolve (data.count );})}) } async function get_comments (article_title, skip, limit ){ return new Promise (resolve => { $.ajax (API_HOST + "/api/comment/get" , { dataType :'json' , method : 'GET' , data : { article_title : article_title, skip : skip, limit : limit }}) .done (function (data ){return resolve (data);})}) } async function submit_comment (article_title, ref, name, email, content, captcha_code, captcha_hash ){ return new Promise (resolve => { $.ajax (API_HOST + "/api/comment/submit" , { method : 'POST' , data : { article_title : article_title, ref : ref, name : name, email : email, content : content, captcha_code : captcha_code, captcha_hash : captcha_hash } })
markdown渲染与代码块高亮化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 (function init_marked ( ) { var rendererMD = new marked.Renderer (); marked.setOptions ({ renderer : rendererMD, gfm : true , tables : true , breaks : false , pedantic : false , sanitize : false , smartLists : true , smartypants : true , highlight : function (code, lang ) { if (hljs==undefined || hljs==null ) return code; let validLang = hljs.getLanguage (lang) ? lang : 'c++' ; let highlight_code = hljs.highlight (validLang, code).value ; if (hljs.lineNumbersValue ==undefined || hljs.lineNumbersValue ==null ){ return highlight_code } return hljs.lineNumbersValue (highlight_code); } }); })(); var app_comment_block = new Vue ({ el : '#comment_view' , data : { comment_view_limit : $("meta[comment_view_limit]" ).length > 0 ? parseInt ($("meta[comment_view_limit]" ).attr ('comment_view_limit' )): 10 , comments_count : 0 , comments : [], comments_view : null , page_limit : $("meta[page_limit]" ).length > 0 ? parseInt ($("meta[page_limit]" ).attr ('page_limit' )): 10 , page_count : 0 , page_view : [], cur_page : 1 , text_type : "md" , refmap : {} }, methods : { render_md : function ( ){ if (this .comments_view ==null || this .comments_view ==undefined ) return ; if (marked==undefined ) return ; for (i=0 ;i<this .comments_view .length ;i++){ this .comments_view [i].html = marked (this .comments_view [i].content ); } this .$forceUpdate(); } }
需要注意的是Vue库函数必须要提前加载,new Vue(el:'', data:{}, methods:{})
要在body加载完成后执行。同理marked
也是,要提前加载相关的依赖库,如hightlight.js
。
至于前端界面显示的其他代码,太长了,这里就省略了。详见我github中的ui_comment.xhtml , ui_comment,js 。
附录:说说我hexo 修改原则与方法 hexo我是用了butterfly这个主题,这个主题非常好看,而且也不感觉花里胡哨,
设置里有很多东西可调,但是有些细节需要自己调一下,比如说页面标题颜色、有些页面太空旷,需要加张图什么的。
我的原则是尽量不修改模板,这样更新主题的时候比较方便,不用merge,兼容性比较好。
因此我的做法是,通过前端css的覆盖来调整样式,没法调整的用js方法来动态改变,包括评论系统也是这么动态加进来的。详见我的另一篇博客文章HexoButterfly主题 配置插件 修改