塔罗会塔罗会

新文章通知

|
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
}));

工作流程

首次访问

  1. 获取 RSS 数据
  2. 存储到 IndexedDB
  3. 设置初始时间戳
  4. 不显示通知

后续访问(检测到变更)

  1. 获取最新 RSS 数据
  2. 读取 IndexedDB 中的旧数据
  3. 逐篇比较内容差异
  4. 更新 IndexedDB
  5. 存储 diff 数据到 sessionStorage
  6. 显示通知弹窗

查看文章变更

  1. 点击通知项跳转到文章
  2. PostDiffViewer 读取 diff 数据
  3. 显示 diff 面板
  4. 自动/手动高亮变更内容

组件说明

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)
  • 关闭通知后自动清理相关状态

评论

评论加载中...