新文章通知
|
675 字
|3 分钟
博客新文章通知功能
基于 RSS 订阅的文章更新检测系统,自动检测新文章和文章更新。
功能特性
- 自动检测: 页面加载后自动获取 RSS 订阅源
- IndexedDB 存储: 本地存储已读取的文章内容
- 差异比较: 对比新旧文章内容,检测变更
- 通知弹窗: 展示新文章和更新文章列表
- 文章内高亮: 在文章页面高亮显示变更内容
技术架构
┌─────────────────────────────────────────┐
│ 前端层 (Next.js) │
│ ┌─────────────────┐ ┌─────────────────┐│
│ │ NewPostNotification│ │ PostDiffViewer ││
│ │ (通知组件) │ │ (差异查看器) ││
│ └─────────────────┘ └─────────────────┘│
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 数据存储层 │
│ ┌─────────────┐ ┌─────────────────┐│
│ │ IndexedDB │ │ sessionStorage ││
│ │ (文章内容) │ │ (diff 数据) ││
│ └─────────────┘ └─────────────────┘│
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ RSS 数据源 │
│ /atom.xml │
└─────────────────────────────────────────┘
核心实现
数据类型
interface Post {
title: string;
link: string;
guid: string;
pubDate: number;
content: string;
}
interface DiffPart {
value: string;
added?: boolean;
removed?: boolean;
}
RSS 数据获取
const fetchRSS = async (): Promise<Post[]> => {
const response = await fetch('/atom.xml', { cache: 'no-store' });
const text = await response.text();
const parser = new DOMParser();
const xml = parser.parseFromString(text, 'text/xml');
return Array.from(xml.querySelectorAll('entry')).map(item => ({
title: item.querySelector('title')?.textContent || '',
link: item.querySelector('link')?.getAttribute('href') || '',
guid: item.querySelector('id')?.textContent || '',
pubDate: new Date(item.querySelector('updated')?.textContent || '').getTime(),
content: item.querySelector('content')?.textContent || '',
}));
};
差异计算
使用 diff 库进行行级差异比较:
import * as Diff from 'diff';
function computeDiff(oldText: string, newText: string) {
const diffs = Diff.diffLines(oldText, newText);
const hasChanges = diffs.some(part => part.added || part.removed);
return hasChanges ? diffs : null;
}
数据存储策略
// 存储所有变更文章的列表(用于通知面板)
const storageData = {
items: updatedPostsWithDiff,
timestamp: Date.now()
};
sessionStorage.setItem('cofe-diff-debug-state', JSON.stringify(storageData));
// 为每个文章单独存储 diff 数据(用于文章页面)
const postKey = `post-diff-${pathname}`;
sessionStorage.setItem(postKey, JSON.stringify({
title, link, guid, diff, timestamp
}));
工作流程
首次访问
- 获取 RSS 数据
- 存储到 IndexedDB
- 设置初始时间戳
- 不显示通知
后续访问(检测到变更)
- 获取最新 RSS 数据
- 读取 IndexedDB 中的旧数据
- 逐篇比较内容差异
- 更新 IndexedDB
- 存储 diff 数据到 sessionStorage
- 显示通知弹窗
查看文章变更
- 点击通知项跳转到文章
- PostDiffViewer 读取 diff 数据
- 显示 diff 面板
- 自动/手动高亮变更内容
组件说明
NewPostNotification
- 初始化检测文章变更
- 定时轮询(每 5 分钟)
- 显示新文章/更新文章列表
PostDiffViewer
- 读取当前文章的 diff 数据
- 显示变更列表面板
- 支持文章中高亮显示
样式配置
/* 新增行 */
.post-inline-diff-add-line {
background-color: rgba(34, 197, 94, 0.1);
border-left: 2px solid #22c55e;
}
/* 删除行 */
.post-inline-diff-del-line {
background-color: rgba(239, 68, 68, 0.1);
border-left: 2px solid #ef4444;
text-decoration: line-through;
}
使用方式
在 layout.tsx 中引入:
import NewPostNotification from '@/components/NewPostNotification';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<NewPostNotification />
</body>
</html>
);
}
在文章页面引入:
import PostDiffViewer from '@/components/PostDiffViewer';
export default function BlogPost() {
return (
<>
<PostDiffViewer />
{/* 文章内容 */}
</>
);
}
注意事项
- 所有数据存储在浏览器本地,不涉及服务器
- sessionStorage 有大小限制(通常 5-10MB)
- 关闭通知后自动清理相关状态
评论
评论加载中...