吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1760|回复: 1
收起左侧

[Java 原创] 【Javascript】MiniComment _ 动态评论系统 _Nodejs_Vue_Mongodb_综合实践

[复制链接]
小木曾雪菜 发表于 2020-11-25 12:22
本帖最后由 小木曾雪菜 于 2020-11-25 17:35 编辑

MiniComment _ 动态评论系统 _Nodejs_Vue_Mongodb_综合实践

1. 前言

以往都是在论坛里发逆向的教程,今天来尝试点别的~~
之前因为用到frida,相对系统的学习了一下JavaScript。然后又接触了jquery,Node.js,同时也关联的学习了Vue.js和mongodb。学了不少,感觉需要来实践一下。前一段时间自己搞了个hexo的博客,由于是静态博客,没法实现动态评论。我们往往采取动静结合的方法,即博客本身为静态,外加动态评论。

第三方的感觉也是感觉个别地方有点问题,比如说github相关的必须用github账号才能评论;disqus国内访问不了;valine需要实名认证麻烦。

虽然开源的评论系统也有,虽然功能很强大,但是都是比较庞大,不够精简,改起来也比较麻烦。
简化版的评论代码不是很多,于是就想着自己搞一个了,正好作为实践,了解一下前后端如何配合的。同时我认为这个例子对于初接触web开发的人,是个很不错的参考例子。

2. MiniComment介绍

minicomment_v0.7

minicomment_v0.8

MiniComment 评论系统前端由Vue编写而来,实现了分页系统,结合marked可以渲染markdown格式(见下图)。可以点reply回复指定的楼层,点击并跳转到引用处(暂时还没做子楼层)。并且有输入验证码回复的功能。前端页面可以嵌套到任意页面中,也可以通过$.load()动态加载,用法如下:

<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/jquery@3.2.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@1.2.3/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/highlight.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/styles/vs.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/highlightjs-line-numbers.js@2.8.0/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范式的传统关系结构。数据类型有mongooseshceme来定义。

数据库定义与相关操作如下:

model_comment.js

const mongoose = require('mongoose');
const commentSchema = new mongoose.Schema({
  article_title: String,
  date: {type:Date, default:new Date()}, //this default is the server start time
  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

const PORT = 3003;

const express = require('express');
var bodyParser = require('body-parser');  // must use body-paser to get post payload
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);
  })

api_comment.js

// 为了看起来结构更清晰,此处只保留核心代码,详见我的github ,https://github.com/YuriSizuku/MiniComment
const express = require('express');

const crypto = require('crypto');
const svgCaptcha = require('svg-captcha');
const {Comment, getComment, submitComment, getCommentCount} = require('./model_comment'); //datebase

const router = express.Router();
var SALT = svgCaptcha.randomText(4);

(function loop(interval){
  //console.log("SALT changed!");
  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', '*'); //use all headers
  next();
}

const authCaptchaMid = async(req, res, next) => { // 无session的验证码验证
  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; // 取得引用的id
  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,全纯手写htmlcssVue.js代码。Vue的设计理念就是数据驱动,数据与显示试图同步。此项目前端主要思想是:

  • $.get来调用api,得到当前页的评论,同时缓存评论
  • v-for 和结合marked渲染得到的评论,同时根据评论数量来选择分页,分页跳转再次获取评论
  • @submit$.post提交评论

ajax后端交互部分代码:

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渲染与代码块高亮化

// 此处省略与markdown无关的Vue部分代码
(function init_marked() { // 初始化marked与highlight.js插件,并且显示行号(通过table实现)
  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) { //the highlight style is depending on css
      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 : [], // ['«', 1, 2, 3, '»'],
        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);
                //console.log(this.comments_view[i].html);
            }
            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方法来动态改变,包括评论系统也是这么动态加进来的。详见我的另一篇博客文章Hexo_Butterfly主题_配置_插件_修改

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
o0你最珍贵0o + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

wanshiz 发表于 2020-11-26 06:57
楼主真大佬级。谢谢分享。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-25 23:31

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表