木木困玉光

仿制giscus文章反应


Tags: #前端 #Hexo
Created:
Updated:

前言

Waline评论本身支持文章反应,只是样式不太好看。
giscus 评论文章反应的样式个人还是蛮喜欢的,但对于访客其实不算友好,需要有 Github,非程序员不一定有,另外必须要登录才能评论,尝试下Hexo中仿制做一个类似的文章反应。

image-20241113235634207

有时候看博文不想要评论或者不知道评论什么,使用这个可以快速便捷的表达。

结构

点赞架构图.drawio

  • 前端:jshtmlcss
  • 服务器:NodejsNginx
  • 数据库:Leancloud

前端发送请求,服务器路由处理请求,从数据库中读取数据返回
其实也可以不用服务器作为中转,直接使用 Leancloud 的 API 执行请求,但不是很推荐,因为 KEY 会暴露在前端中

前端

html写一组表情

<div id="reactions">
  <button class="reaction" data-reaction="up">👍 <span class="count">0</span></button>
  <button class="reaction" data-reaction="love">❤️ <span class="count">0</span></button>
  <button class="reaction" data-reaction="emm">😑 <span class="count">0</span></button>
  <button class="reaction" data-reaction="down">👎 <span class="count">0</span></button>
  <button class="reaction" data-reaction="see">👀 <span class="count">0</span></button>
</div>

js绑定点击事件

采取页面URL作为唯一值存储数据库,以后都是通过URL进行判断,修改路径的话需要修改数据库,否则值全部归0
采用localStorage存储用户点赞状态

需要修改的地方:

  • 15行:修改为自己服务器域名或者ip
  • 41行:修改为自己服务器域名或者ip
document.addEventListener("DOMContentLoaded", function () {
  const reactions = document.querySelectorAll(".reaction");
  const postId = window.location.pathname; // 获取当前页面的路径作为postId

  // 页面加载时检查并设置点赞状态
  reactions.forEach((reaction) => {
    const reactionType = reaction.getAttribute("data-reaction");
    const key = `liked_${postId}_${reactionType}`;
    if (localStorage.getItem(key)) {
      reaction.classList.add("liked");
    }
  });

  // 获取点赞数据
  fetch(`http://xxx.xxx.xxx.xxx/like?postId=${encodeURIComponent(postId)}`, {
    method: "GET",
  })
    .then((response) => {
      if (!response.ok) {
        throw new Error("Network response was not ok");
      }
      return response.json();
    })
    .then((data) => {
      updateReactionCounts(data);
    })
    .catch((error) => {
      console.error("Error fetching reaction data:", error);
    });

  reactions.forEach((reaction) => {
    reaction.addEventListener("click", function () {
      const reactionType = this.getAttribute("data-reaction");
      const key = `liked_${postId}_${reactionType}`;

      if (localStorage.getItem(key)) {
        alert("您已经点赞过了!");
        return;
      }

      fetch("http://xxx.xxx.xxx.xxx/like", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ postId, reactionType }),
      })
        .then((response) => {
          if (!response.ok) {
            throw new Error("Network response was not ok");
          }
          return response.json();
        })
        .then((data) => {
          console.log(data);
          updateReactionCount(reactionType, 1);
          localStorage.setItem(key, true);
          this.classList.add("liked");
        })
        .catch((error) => {
          console.error("Error:", error);
        });
    });
  });

  // 更新点赞计数的函数
  function updateReactionCount(reactionType, increment) {
    const reactionCountElement = document.querySelector(
      `.reaction[data-reaction="${reactionType}"] .count`
    );
    if (reactionCountElement) {
      const currentCount = parseInt(reactionCountElement.textContent, 10) || 0;
      reactionCountElement.textContent = currentCount + increment;
    }
  }

  // 更新点赞计数的函数,用于初始化页面加载时的计数
  function updateReactionCounts(reactions) {
    reactions.forEach((reaction) => {
      updateReactionCount(reaction.reactionType, reaction.count);
    });
  }
});

修改样式(根据自己需要进行更改,目前是契合个人主题)

/* reactions.css */
#reactions {
    display: flex;
    gap: 10px;
    justify-content: center;
    margin: 25px;
    width:100%;
}

.reaction {
    cursor: pointer;
    padding: 5px;
    border: none;
    background-color: #f0f0f0;
    border-radius: 5px;
}
.reaction:hover {
    background-color: #e0e0e0;
}

.reaction.liked:hover {
    background-color: #afd1ff;
}

html.skin-dark .reaction{
    background-color: rgb(82 82 82);
}

/* 已点击样式 */
.reaction.liked,
html.skin-dark  .reaction.liked {
    background-color: #d0e4fe; 
    color: #000; 
  }

数据库

创建一个应用后,点击「结构化数据」-> 「创建 Class」

image-20241114003147106

新建一个 Reactions

image-20241114003219457

点击添加列,新建三个字段

  • postId:类型选择“字符串”,用于存储文章的唯一 ID。
  • reactionType:类型选择“字符串”,用于存储点赞类型,如 uplove 等。
  • count:类型选择“数字”,用于存储点赞的数量,初始值设置为 0。

image-20241114003509861

注:postId 虽然是作为唯一值,但建立字段的时候,不要勾选唯一值,否则会导致后期计数时候出错
image-20241114004037886

完成字段建立后,记录 API KEY ,后续要用到

image-20241114004151130

服务器

写一个nodejs脚本处理作为路由,处理请求
然后使用Nginx对这个路由进行反代即可

路由

用于处理前端发送过来的请求

先安装依赖包

npm install express
npm install leancloud-storage
npm install cors
  • express:框架
  • leancloud-storage:Leancloud的SDK,用于操作数据库
  • cors:配置CORS以允许跨域请求,调试啥的也要用到

下面是js代码,需要修改的地方有:

  • 10行:添加自己的域名,进行跨域请求放行
  • 21-23行:修改为自己Leancloud的API Key
const express = require('express');
const cors = require('cors');
const AV = require('leancloud-storage');

const app = express();
const port = 3000;

// 配置CORS
const corsOptions = {
  origin: ['http://localhost:4000'],  
  optionsSuccessStatus: 200 
};

app.use(cors(corsOptions)); 
app.use(express.json()); 

// 配置Leancloud
AV.init({
  appId: '',
  appKey: '',
  serverURL: 'https://bfrlyu2d.lc-cn-n1-shared.com'
});

// 获取点赞数据的路由
app.get('/like', (req, res) => {
  const { postId } = req.query; 
  if (!postId) {
    return res.status(400).json({ error: 'Missing postId' });
  }
  const query = new AV.Query('Reactions');
  query.equalTo('postId', postId);
  query.find().then(results => {
    const reactionData = results.map(result => {
      return {
        reactionType: result.get('reactionType'),
        count: result.get('count')
      };
    });
    res.json(reactionData);
  }).catch(error => {
    console.error('Query error:', error);
    res.status(500).json({ error: '查询失败' });
  });
});

// 处理点赞的路由
app.post('/like', (req, res) => {
    const { postId, reactionType } = req.body;
    if (!postId || !reactionType) {
      return res.status(400).json({ error: 'Missing postId or reactionType' });
    }
  
    const query = new AV.Query('Reactions');
    query.equalTo('postId', postId);
    query.equalTo('reactionType', reactionType);
    query.find().then(results => {
      if (results.length > 0) {
        // 检查是否已经点赞
        if (results[0].get('count') > 0) {
          return res.status(400).json({ error: 'Already liked' });
        }

        results[0].increment('count', 1);
        return results[0].save();
      } else {
        // 没有找到匹配的记录,创建新的记录
        const reaction = new AV.Object('Reactions');
        reaction.set('postId', postId);
        reaction.set('reactionType', reactionType);
        reaction.set('count', 1);
        return reaction.save();
      }
    }).then(() => {
      res.json({ message: '点赞成功' });
    }).catch(error => {
      res.status(500).json({ error: '点赞失败' });
    });
  });

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

保存成like.js,使用nodejs运行即可

node like

反代

使用Nginx反代,第3行修改为自己域名或者服务器 ip

server {
    listen 80;
    server_name x.x.x.x;

    location /like {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
:D 获取中...