Nuxt 项目中实现基于 Redis 的点赞功能
自上次将微信步数改为redis存储后 ~/note/6025 ,我就一直在想,让他一人独占redis是不是太孤单了,在折腾点什么呢。
正好前后端分离后开启了进阶玩法,技术也进步不少,那就整个点赞功能吧,虽然没什么卵用,重在折腾嘛,先理一下思路,结合最近学到的知识大概苏理出:后端逻辑 、API 仓库扩展 和 首页组件 三个部分。
那有了思路打开ai开干,现在的ai真强大,以前不敢想的现在变成只有你想不到的。
第一步:
安装redis就跳过,前期已部署完成,在 .env 中添加 Redis 地址:
REDIS_URL=redis://localhost:6379
第二步:服务端逻辑
1.创建 Redis 工具类 server/utils/redis.ts
// server/utils/redis.ts
import Redis from 'ioredis'
let redis: Redis | null = null
export const useRedis = () => {
if (!redis) {
const config = useRuntimeConfig()
// 优先读取 runtimeConfig 中的配置,如果没有则取环境变量
const url = config.redisUrl || process.env.REDIS_URL
if (!url) {
console.error('Redis URL 未配置!请检查 .env 文件。')
}
redis = new Redis(url)
// 监听连接错误(方便排查密码错误)
redis.on('error', (err) => {
console.error('Redis 连接失败:', err.message)
})
}
return redis
}- 创建点赞/取消点赞接口
server/api/likes/toggle.post.ts
export default defineEventHandler(async (event) => {
const { cid } = await readBody(event)
if (!cid) throw createError({ statusCode: 400, message: 'Missing CID' })
const redis = useRedis()
// 使用 IP 作为用户标识(如果没有登录系统)
const ip = getRequestIP(event, { xForwardedFor: true }) || 'anonymous'
const key = `post:likes:${cid}`
const hasLiked = await redis.sismember(key, ip)
if (hasLiked) {
await redis.srem(key, ip)
return { liked: false, count: await redis.scard(key) }
} else {
await redis.sadd(key, ip)
return { liked: true, count: await redis.scard(key) }
}
})- 创建批量获取点赞数接口
server/api/likes/batch.get.ts用于首页一次性获取当前页所有文章的点赞状态。
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const cids = (query.cids as string || '').split(',')
if (!cids.length || cids[0] === '') return {}
const redis = useRedis()
const pipeline = redis.pipeline()
cids.forEach(cid => pipeline.scard(`post:likes:${cid}`))
const results = await pipeline.exec()
const likesMap: Record<string, number> = {}
cids.forEach((cid, index) => {
likesMap[cid] = results?.[index]?.[1] || 0
})
return likesMap
})第三步:修改 API 仓库和 Composable
- 修改
repositories/modules/posts.ts增加点赞相关方法
// 在 PostsRepository 类中添加
async toggleLike(cid: string | number) {
return await this.fetcher('/api/likes/toggle', {
method: 'POST',
body: { cid }
})
}
async getBatchLikes(cids: (string | number)[]) {
return await this.fetcher('/api/likes/batch', {
method: 'GET',
params: { cids: cids.join(',') }
})
}第四步:首页组件集成 (pages/index.vue)
// 新增
const likesMap = ref({}) // 存储 { cid: { count: 0, liked: false } }
// 获取点赞数并同步 Cookie 状态
watch(postList, async (newList) => {
if (newList && newList.length > 0) {
const cids = newList.map(p => p.cid)
try {
const data = await postsRepo.getBatchLikes(cids)
const formattedData = {}
cids.forEach(cid => {
const cookieName = `liked_${cid}`
const hasLocalCookie = document.cookie.split(';').some(c => c.trim().startsWith(cookieName + '='))
const serverData = data[cid]
formattedData[cid] = {
count: typeof serverData === 'object' ? serverData.count : (Number(serverData) || 0),
liked: hasLocalCookie || (typeof serverData === 'object' ? !!serverData.liked : false)
}
})
likesMap.value = { ...likesMap.value, ...formattedData }
} catch (e) {
console.error('获取点赞失败', e)
}
}
}, { immediate: true }) // 4. 点赞动作(含 Cookie 处理)
const handleLike = async (cid) => {
if (!likesMap.value[cid]) {
likesMap.value[cid] = { count: 0, liked: false }
}
const item = likesMap.value[cid]
const isLiking = !item.liked
const cookieName = `liked_${cid}`
item.liked = isLiking
item.count += isLiking ? 1 : -1
try {
const result = await postsRepo.toggleLike(cid)
if (isLiking) {
document.cookie = `${cookieName}=true; path=/; max-age=${30*24*60*60}`
} else {
document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`
}
if (typeof result === 'object') {
item.count = result.count
} else if (!isNaN(result)) {
item.count = Number(result)
}
} catch (e) {
console.error('点赞请求失败', e)
item.liked = !isLiking
item.count += isLiking ? -1 : 1
}
} <!-- <template>区域合适位置插入点赞按钮 -->
<span :class="['i-like', likesMap[post.cid]?.liked ? 'liked' : '']" @click.stop="handleLike(post.cid)">
{{ likesMap[post.cid]?.count || 0 }}
</span>然后经过无数次调试,完美收工!!最终样子如图,还算满意👍这一周干了2026年最最最伟大的事情。
从无到有完成了前后端分离,增加了页面过渡动画、点赞、足迹地图等功能,同时做了大量优化,坚持能用原生方法实现的绝不加载第三方库,新主题虽外表无变化,但内在却有质的飞跃,感觉前几年的折腾简直太小儿科了,现在的自已强得可怕😱。



留言