目 录CONTENT

文章目录

Github 风格的 Reaction 点赞功能后端实现

16Reverie
2025-04-01 / 0 评论 / 0 点赞 / 80 阅读 / 0 字

又遇到一个好玩的东西:Github 风格的 reaction 点赞功能。

Memos 上也集成了同样的功能,不过没怎么关注,直到刷到了这个仓库:emaction/emaction.frontend,emaction.frontend 把这个功能很好地实现了。好!为网站添砖加瓦。

实际效果:#功能展示

前端集成

前端集成非常简单,只需要引入 JS:

可以通过CDN引入或者下载到本地

<script type="module" src="https://cdn.jsdelivr.net/gh/emaction/frontend.dist@1.0.11/bundle.js"></script>

然后放置占位标签

<emoji-reaction></emoji-reaction>

就ok了。其他自定义参数参考 #标签自定义参数

后端开发

emaction.frontend 同时提供了后端代码和后端的接口文档,emaction/emaction.backend

eallion 大佬解析代码推算出了后端的数据库结构,同时提供了 Cloudflare 部署后端的方法:自部署 GitHub 风格的 Reactions 点赞功能 · 大大的小蜗牛

不过 Cloudflare、vercel 这类免费服务虽然造福人类,单速度还是慢了点。根据数据结构和接口文档,Node.js + SQLite 简单实现一个后端并不难。

冻手

环境

  • 系统:Ubuntu 22.04.4 LTS

  • npm版本:10.2.4

  • Node.js版本:v18.19.1

环境影响不大,这里只是说明

初始化和依赖安装

mkdir -p emaction-backend/src & cd emaction-backend
npm init -y

npm install express sqlite3

代码

src/database.js用于初始化数据库和表,以及实现数据库操作:

const sqlite3 = require('sqlite3').verbose();

// 连接到 SQLite 数据库
const db = new sqlite3.Database('emaction.db', (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Connected to the emaction database.');

  db.serialize(() => {
    db.run(`
      CREATE TABLE IF NOT EXISTS reactions (  // 创建表
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        target_id TEXT NOT NULL,
        reaction_name TEXT NOT NULL,
        count INTEGER NOT NULL,
        created_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      )
    `);
  });
});

const getReactionsByTargetId = (targetId, callback) => {  // 获取特定 targetId 已收到的所有 reactions
  db.all('SELECT reaction_name, count FROM reactions WHERE target_id = ?', [targetId], callback);
};  

// 新增/更新一个 reaction
const updateReaction = (targetId, reactionName, diff, callback) => {
  db.get('SELECT count FROM reactions WHERE target_id = ? AND reaction_name = ?', [targetId, reactionName], (err, row) => {
    if (err) {
      return callback(err);
    }

    if (row) {
      const newCount = Math.max(0, row.count + diff);
      const now = Date.now();
      db.run('UPDATE reactions SET count = ?, updated_at = ? WHERE target_id = ? AND reaction_name = ?', [newCount, now, targetId, reactionName], callback);    // 更新现有记录
    } else {
      const now = Date.now();
      db.run('INSERT INTO reactions (target_id, reaction_name, count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [targetId, reactionName, Math.max(0, diff), now, now], callback);  // 插入新记录
    }
  });
};

module.exports = {
  getReactionsByTargetId,
  updateReaction
};

src/functions.js:功能函数

const validateStringParam = (paramName) => {
  return (req, res, next) => {
    const param = req.query[paramName];
    if (typeof param !== 'string' || param.trim() === '') {  // 验证 targetId 和 reaction_name 是否为字符串
      return res.status(400).json({ code: 1, msg: `Invalid ${paramName}` });
    }
    next();
  };
};

const validateDiffParam = (req, res, next) => {
  const diff = parseInt(req.query.diff);
  if (isNaN(diff) || (diff !== 1 && diff !== -1)) {  // 验证 diff 是否为 1 或 -1
    return res.status(400).json({ code: 1, msg: 'Invalid diff, must be 1 or -1' });
  }
  next();
};

module.exports = {
  validateStringParam,
  validateDiffParam
};

src/index.js:后端接口实现

const express = require('express');
const app = express();
const db = require('./database');
const cors = require('cors');
const { validateStringParam, validateDiffParam } = require('./functions');

app.use(cors());  // 默认关闭跨域检查,实际环境自行设置

app.get('/reactions', validateStringParam('targetId'), (req, res) => {  // 获取特定 targetId 已收到的所有 reactions
  const targetId = req.query.targetId;
  db.getReactionsByTargetId(targetId, (err, rows) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ code: 1, msg: `Database error: ${err.message}` });
    }
    res.json({ code: 0, msg: 'success', data: { reactionsGot: rows } });
  });
});

app.patch('/reaction', validateStringParam('targetId'), validateStringParam('reaction_name'), validateDiffParam, (req, res) => {
  const targetId = req.query.targetId;
  const reactionName = req.query.reaction_name;  // 新增/更新一个 reaction
  const diff = parseInt(req.query.diff);

  db.updateReaction(targetId, reactionName, diff, (err) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ code: 1, msg: `Database error: ${err.message}` });
    }
    res.json({ code: 0, msg: 'success' });
  });
});

// 启动
const port = process.env.PORT || 9000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

调试运行

运行

node src/index.js

测试接口

获取 reaction 数据

curl http://localhost:9000/reactions?targetId=123

更新 reaction 计数

curl -X PATCH "http://localhost:9000/reaction?targetId=123&reaction_name=like&diff=1"

成功!

前端传入后端接口地址

将后端的接口地址通过标签自定义属性 endpoint 传入。(末尾不能有 ’/’)

<emoji-reaction endpoint=""http://localhost:9000"></emoji-reaction>

完成!

标签自定义参数

自定义 reaction emoji

通过给标签加上属性值 availableArrayString 来自定义 emoji 列表。格式为 "<emoji>,<emoji_name>;"

<emoji-reaction availableArrayString="👍,thumbs-up;😄,smile-face;🎉,party-popper;😕,confused-face;❤️,red-heart;🚀,rocket;👀,eyes;"></emoji-reaction>

自定义 targetId

emaction 通过 targetId 来区分页面,如果有多个部署需求,需要区分targetId,通过 reacttargetid属性值来指定

<emoji-reaction reacttargetid="newTargetId" endpoint="https://api-emaction.xxxxxx.workers.dev"></emoji-reaction>

自定义

自定义样式

<style>
  .reactions {
    --start-smile-border-color: #d0d7de;
    --start-smile-border-color-hover: #bbb;
    --start-smile-bg-color: #f6f8fa;
    --start-smile-svg-fill-color: #656d76;
    --reaction-got-not-reacted-bg-color: #fff;
    --reaction-got-not-reacted-bg-color-hover: #eaeef2;
    --reaction-got-not-reacted-border-color: #d0d7de;
    --reaction-got-not-reacted-text-color: #656d76;
    --reaction-got-reacted-bg-color: #ddf4ff;
    --reaction-got-reacted-bg-color-hover: #b6e3ff;
    --reaction-got-reacted-border-color: #0969da;
    --reaction-got-reacted-text-color: #0969da;
    --reaction-available-popup-bg-color: #fff;
    --reaction-available-popup-border-color: #d0d7de;
    --reaction-available-popup-box-shadow: #8c959f33 0px 8px 24px 0px;
    --reaction-available-emoji-reacted-bg-color: #ddf4ff;
    --reaction-available-emoji-bg-color-hover: #f3f4f6;
    --reaction-available-emoji-z-index: 100;
    --reaction-available-mask-z-index: 80;
  }
</style>
<emoji-reaction class="reactions"></emoji-reaction>

链接

https://github.com/emaction/emaction.frontend

https://github.com/emaction/emaction.backend

https://www.eallion.com/self-hosted-github-flavored-reactions/

功能展示

👇👇👇👇👇 返回🔙

👇👇👇👇👇

0

评论区