文章回顾了友圈模块从诞生到四次迭代的完整历程。友圈通过自定义字段配置订阅地址,实时解析RSS/Atom生成JSON缓存展示前端。四次优化分别是:兼容RSS和Atom双格式;用fastcgi_finish_request()实现后台异步刷新,访客零等待;取消30条限制加AJAX分页;增加"好友动态"和"订阅发现"Tab分类,favicon图标兜底。文末附了当前完整源码,涵盖非阻塞锁、超时控制、session释放、分页接口等实现。
此内容根据文章生成,不代表个人观点,仅用于文章内容的解释与总结
关于博客友圈最早的诞生及过程可以参考博主之前的两篇文章:

第一版【友圈】的诞生,还是给博主很大的激励的,确实给博主带来了很大的便利,可以查看友链朋友们的最新动态~后续也在使用过程中也在逐步的完善和优化:
2025-01-13日优化:刷友圈模块时发现有一个邻居的动态没有刷新出来,经过查询订阅地址。发现原来rss订阅格式还分atom和rss两种,而我本地写的解析代码只考虑到了rss格式的订阅。所以抽空重构了一下订阅解析代码。
2025-01-20日优化:友圈模块原来是实时请求解析 feed 地址生成缓存,访客撞上缓存过期就得干等。放弃了"定时刷新"的治标方案,改用 PHP 后台异步刷新:请求进来先返回旧数据,让脚本在后台继续拉取并更新缓存数据,同时用文件锁防止多并发冲突。这样访客零等待,下次访问就是新数据。
2026-07-22日优化:优化了一下友圈只查看30条数据的限制,之前都是按照时间排序拉取前30条的,但是随着友链博客越来越多。30条已经无法覆盖所有订阅博客了。所以加了一个分页功能,支持加载更多。
2026-08-18日优化:友圈增加了一个Tab切换,主要对友圈的订阅做了下分类。【好友动态】订阅的是友链中的朋友,【订阅发现】为博主发现的一些不错的博客或者内容而进行的订阅。哦对了,顺便把图片问题显示也优化了下,订阅链接中信息没有图片的就取网站的favicon.ico。
原理解析:
友圈模块的大致原理就是通过请求feed地址,解析数据存到本地一个json文件。然后在每次访问时就去拉取json文件的数据,达到实时显示的目的。同时触发后台脚本刷新去拉取最新的数据来更新json文件的数据。之前做的时候图省事,直接做成了博客独立页面模板——

在这个模板代码中,通过获取自定义字段的url作为feed地址来请求,拉取订阅的数据,解析显示。每次只需要配置自定义字段就能达到设置订阅url的效果。这里设置的规律是循环key为friend1~friend50取url作为好友动态的订阅地址,循环key为follow1~follow50取url作为订阅发现的订阅地址。然后拉取数据显示。

friendrsss.php代码
<?php
/**
* RSS朋友圈
* @author liuqing
* @package custom
*/
if (!defined("__TYPECHO_ROOT_DIR__")) {
exit();
}
if (!function_exists('rss_get_groups')) {
function rss_get_groups()
{
return [
'friends' => [
'label' => '好友动态',
'fieldPrefix' => 'friends',
'fieldStart' => 1,
'fieldEnd' => 50,
'cacheFile' => 'outrss-friends.json',
'lockFile' => 'outrss-friends.lock',
],
'follow' => [
'label' => '订阅发现',
'fieldPrefix' => 'follow',
'fieldStart' => 1,
'fieldEnd' => 50,
'cacheFile' => 'outrss-follow.json',
'lockFile' => 'outrss-follow.lock',
],
];
}
}
if (!function_exists('rss_default_group_key')) {
function rss_default_group_key($groups)
{
foreach ($groups as $key => $group) {
return $key;
}
return '';
}
}
if (!function_exists('rss_get_active_group_key')) {
function rss_get_active_group_key($groups)
{
$defaultKey = rss_default_group_key($groups);
$tab = isset($_GET['tab']) ? $_GET['tab'] : $defaultKey;
return isset($groups[$tab]) ? $tab : $defaultKey;
}
}
if (!function_exists('rss_cache_dir')) {
function rss_cache_dir()
{
return __TYPECHO_ROOT_DIR__ . '/usr/uploads/rss/';
}
}
if (!function_exists('rss_cache_file')) {
function rss_cache_file($group)
{
return rss_cache_dir() . $group['cacheFile'];
}
}
if (!function_exists('rss_lock_file')) {
function rss_lock_file($group)
{
return rss_cache_dir() . $group['lockFile'];
}
}
if (!function_exists('rss_read_cache')) {
function rss_read_cache($cacheFile)
{
if (!file_exists($cacheFile)) {
return [];
}
$feeds = json_decode(file_get_contents($cacheFile), true);
return is_array($feeds) ? $feeds : [];
}
}
if (!function_exists('rss_load_initial_feeds')) {
function rss_load_initial_feeds($widget, $group)
{
$cacheFile = rss_cache_file($group);
$cachedFeeds = rss_read_cache($cacheFile);
if (file_exists($cacheFile) && !empty($cachedFeeds)) {
return $cachedFeeds;
}
$lockHandle = rss_lock_cache_file(rss_lock_file($group));
if (!$lockHandle) {
return $cachedFeeds;
}
try {
$cachedFeeds = rss_read_cache($cacheFile);
if (empty($cachedFeeds)) {
set_time_limit(30);
rss_write_cache($cacheFile, rss_fetch_group_feeds($widget, $group));
}
} catch (Exception $e) {
error_log('Error creating RSS cache: ' . $e->getMessage());
} finally {
rss_unlock_cache_file($lockHandle);
}
return rss_read_cache($cacheFile);
}
}
if (!function_exists('rss_write_cache')) {
function rss_write_cache($cacheFile, $feeds)
{
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
file_put_contents($cacheFile, json_encode($feeds, JSON_UNESCAPED_UNICODE), LOCK_EX);
}
}
if (!function_exists('rss_is_cache_stale')) {
function rss_is_cache_stale($cacheFile, $pullTime)
{
return !file_exists($cacheFile) || (time() - filemtime($cacheFile) > $pullTime);
}
}
if (!function_exists('rss_lock_cache_file')) {
function rss_lock_cache_file($lockFile)
{
$lockDir = dirname($lockFile);
if (!is_dir($lockDir)) {
mkdir($lockDir, 0755, true);
}
if (!file_exists($lockFile)) {
file_put_contents($lockFile, '');
}
$lockHandle = fopen($lockFile, 'w+');
if (!$lockHandle) {
error_log("Failed to open lock file: $lockFile");
return false;
}
if (flock($lockHandle, LOCK_EX | LOCK_NB)) {
return $lockHandle;
}
fclose($lockHandle);
return false;
}
}
if (!function_exists('rss_unlock_cache_file')) {
function rss_unlock_cache_file($lockHandle)
{
if ($lockHandle) {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
}
}
}
if (!function_exists('rss_extract_atom_link')) {
function rss_extract_atom_link($node)
{
foreach ($node->link as $link) {
$rel = (string) $link['rel'];
$href = (string) $link['href'];
if ($href !== '' && ($rel === '' || $rel === 'alternate')) {
return $href;
}
}
return (string) $node->link['href'];
}
}
if (!function_exists('rss_build_site_origin')) {
function rss_build_site_origin($siteLink, $fallbackUrl)
{
$url = trim($siteLink) !== '' ? trim($siteLink) : trim($fallbackUrl);
if ($url === '') {
return '';
}
if (strpos($url, '//') === 0) {
$url = 'https:' . $url;
} elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) {
$url = 'https://' . $url;
}
$parts = parse_url($url);
if (empty($parts['host'])) {
return '';
}
$scheme = isset($parts['scheme']) && in_array($parts['scheme'], ['http', 'https'], true)
? $parts['scheme']
: 'https';
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
return $scheme . '://' . $parts['host'] . $port;
}
}
if (!function_exists('rss_url_exists')) {
function rss_url_exists($url)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_NOBODY, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, 2);
curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 RSS Reader');
curl_exec($curl);
$status = intval(curl_getinfo($curl, CURLINFO_HTTP_CODE));
$contentType = (string) curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
curl_close($curl);
if (rss_is_valid_favicon_response($status, $contentType)) {
return true;
}
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, 2);
curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 RSS Reader');
curl_setopt($curl, CURLOPT_RANGE, '0-0');
curl_exec($curl);
$status = intval(curl_getinfo($curl, CURLINFO_HTTP_CODE));
$contentType = (string) curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
curl_close($curl);
return rss_is_valid_favicon_response($status, $contentType);
}
}
if (!function_exists('rss_is_valid_favicon_response')) {
function rss_is_valid_favicon_response($status, $contentType)
{
if ($status < 200 || $status >= 400) {
return false;
}
$contentType = strtolower($contentType);
if ($contentType === '') {
return true;
}
if (strpos($contentType, 'text/html') !== false || strpos($contentType, 'json') !== false || strpos($contentType, 'xml') !== false) {
return false;
}
return strpos($contentType, 'image/') !== false
|| strpos($contentType, 'icon') !== false
|| strpos($contentType, 'octet-stream') !== false;
}
}
if (!function_exists('rss_get_favicon_url')) {
function rss_get_favicon_url($siteLink, $fallbackUrl)
{
$origin = rss_build_site_origin($siteLink, $fallbackUrl);
if ($origin === '') {
return '';
}
$faviconUrl = $origin . '/favicon.ico';
return rss_url_exists($faviconUrl) ? $faviconUrl : '';
}
}
if (!function_exists('rss_fetch_url_items')) {
function rss_fetch_url_items($url)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($curl, CURLOPT_TIMEOUT, 3);
curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 RSS Reader');
$rssContent = curl_exec($curl);
curl_close($curl);
if ($rssContent === false || $rssContent === '') {
return [];
}
$rss = simplexml_load_string($rssContent, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($rss === false) {
return [];
}
$isAtom = $rss->getName() === 'feed';
$channel = $isAtom ? $rss : $rss->channel;
$name = (string) $channel->title;
$image = (string) ($isAtom ? $channel->logo : ($channel->image ? $channel->image->url : ''));
$siteLink = $isAtom ? rss_extract_atom_link($channel) : (string) $channel->link;
if (trim($image) === '') {
$image = rss_get_favicon_url($siteLink, $url);
}
$items = $isAtom ? $rss->entry : $rss->channel->item;
$feeds = [];
foreach ($items as $item) {
$title = (string) $item->title;
$link = (string) ($isAtom ? rss_extract_atom_link($item) : $item->link);
$pubDate = (string) ($isAtom ? $item->updated : $item->pubDate);
$description = (string) ($isAtom ? $item->summary : $item->description);
$author = (string) ($isAtom
? ($item->author ? $item->author->name : '')
: ($item->children('dc', true)->creator ? $item->children('dc', true)->creator : ''));
$timestamp = strtotime($pubDate);
$feeds[] = [
'title' => $title,
'link' => $link,
'pubDate' => $timestamp ? $timestamp : time(),
'name' => $name,
'image' => $image,
'description' => mb_substr(strip_tags($description), 0, 180, 'utf-8'),
'author' => $author,
'siteLink' => $siteLink,
];
}
return $feeds;
}
}
if (!function_exists('rss_fetch_group_feeds')) {
function rss_fetch_group_feeds($widget, $group)
{
$feeds = [];
$start = isset($group['fieldStart']) ? intval($group['fieldStart']) : 1;
$end = isset($group['fieldEnd']) ? intval($group['fieldEnd']) : 50;
for ($i = $start; $i <= $end; $i++) {
$fieldName = $group['fieldPrefix'] . $i;
$rssValue = $widget->fields->{$fieldName};
if (!empty($rssValue)) {
$feeds = array_merge($feeds, rss_fetch_url_items($rssValue));
}
}
usort($feeds, function ($a, $b) {
return $b['pubDate'] - $a['pubDate'];
});
return $feeds;
}
}
if (!function_exists('rss_finish_client_response')) {
function rss_finish_client_response()
{
if (function_exists('session_write_close')) {
session_write_close();
}
while (ob_get_level()) {
ob_end_flush();
}
flush();
ignore_user_abort(true);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
}
}
if (!function_exists('rss_refresh_cache_after_response')) {
function rss_refresh_cache_after_response($widget, $group, $pullTime)
{
$cacheFile = rss_cache_file($group);
if (!rss_is_cache_stale($cacheFile, $pullTime)) {
return;
}
$lockHandle = rss_lock_cache_file(rss_lock_file($group));
if (!$lockHandle) {
return;
}
try {
if (!rss_is_cache_stale($cacheFile, $pullTime)) {
return;
}
rss_finish_client_response();
set_time_limit(30);
rss_write_cache($cacheFile, rss_fetch_group_feeds($widget, $group));
} catch (Exception $e) {
error_log('Error refreshing RSS cache: ' . $e->getMessage());
} finally {
rss_unlock_cache_file($lockHandle);
}
}
}
if (!function_exists('rss_render_items_html')) {
function rss_render_items_html($rssFeeds)
{
$html = '';
foreach ($rssFeeds as $rssFeed) {
$html .= '<div class="post-content-rss">';
$html .= '<div class="post-content-item">';
$html .= '<p style="z-index: 1;display: flex;justify-content: space-between;align-items:center">';
$html .= '<a class="text-lg text-black line-clamp-1 dark:text-neutral-200" href="' . htmlspecialchars($rssFeed['link'], ENT_QUOTES, 'UTF-8') . '" target="_blank">' . htmlspecialchars($rssFeed['title'], ENT_QUOTES, 'UTF-8') . '</a>';
$html .= '<iconify-icon icon="tabler:rss" class="rounded px-3 py-2 text-lg"></iconify-icon>';
$html .= '</p>';
$html .= '<p class="my-2 jasmine-letter-spacing dark:dark:text-gray-400 break-all" style="overflow: hidden;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 3;z-index: 1;">' . htmlspecialchars($rssFeed['description'], ENT_QUOTES, 'UTF-8') . '</p>';
if (!empty($rssFeed['image'])) {
$html .= '<img class="rounded h-[90px] w-[90px]" style="z-index: 0;position: absolute;left: 1rem;opacity: 0.1;" src="' . htmlspecialchars($rssFeed['image'], ENT_QUOTES, 'UTF-8') . '" alt="' . htmlspecialchars($rssFeed['name'], ENT_QUOTES, 'UTF-8') . '">';
}
$html .= '<p class="author-info" style="display: flex;justify-content: space-between;align-items:center">';
$html .= '<span class="dark:text-neutral-200" style="font-size:0.875rem">' . getHumanizedDate($rssFeed['pubDate']) . '</span>';
$html .= '<a href="' . htmlspecialchars($rssFeed['siteLink'], ENT_QUOTES, 'UTF-8') . '" style="display: inline-block;border-radius: 25px;padding: 2px 8px;font-size: 0.875rem;background: #9999992b;" target="_blank" class="freelink dark:text-neutral-200 text-black">' . htmlspecialchars($rssFeed['name'], ENT_QUOTES, 'UTF-8') . '</a>';
$html .= '</p>';
$html .= '</div>';
$html .= '</div>';
}
return $html;
}
}
if (!function_exists('rss_build_tab_url')) {
function rss_build_tab_url($tab)
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$query = $_GET;
unset($query['action'], $query['page']);
$query['tab'] = $tab;
return $path . '?' . http_build_query($query);
}
}
$rssGroups = rss_get_groups();
$activeRssTab = rss_get_active_group_key($rssGroups);
$activeRssGroup = $rssGroups[$activeRssTab];
$perPage = 30;
$pullTime = 2 * 60 * 60;
$allFeeds = rss_load_initial_feeds($this, $activeRssGroup);
if (isset($_GET['action']) && $_GET['action'] === 'loadmore') {
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$offset = ($page - 1) * $perPage;
$result = array_slice($allFeeds, $offset, $perPage);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'html' => rss_render_items_html($result),
'hasMore' => ($offset + count($result)) < count($allFeeds),
'total' => count($allFeeds),
], JSON_UNESCAPED_UNICODE);
exit;
}
$initialFeeds = array_slice($allFeeds, 0, $perPage);
$hasMoreFeeds = count($allFeeds) > $perPage;
?>
<!DOCTYPE html>
<html lang="zh">
<style>
.rss-tabs {
display: flex;
gap: 1.75rem;
margin: .5rem 1rem 1.25rem;
padding: 0;
border-bottom: 1px solid #eef2f7;
background: transparent;
}
.rss-tab {
position: relative;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.75rem;
padding: 0 .1rem;
color: #64748b;
font-size: .875rem;
font-weight: 600;
text-decoration: none;
transition: color .2s ease;
}
.rss-tab:hover {
color: #111827;
background: transparent;
}
.rss-tab.is-active {
color: #111827;
background: transparent;
}
.rss-tab::after {
content: "";
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 2px;
border-radius: 999px;
background: transparent;
transform: scaleX(.35);
transition: background .2s ease, transform .2s ease;
}
.rss-tab.is-active::after {
background: #111827;
transform: scaleX(1);
}
.post-content-rss {
margin: 1rem;
}
.post-content-item {
position: relative;
border: 1px solid #e3e8f7;
border-radius: 8px;
padding: 1rem;
display: flex;
align-content: space-between;
flex-direction: column;
justify-content: space-between;
}
.freelink:hover {
color: #fff;
background: #425AEF !important;
}
.dark .rss-tabs {
border-color: #525252;
background: transparent;
}
.dark .rss-tab {
color: #a3a3a3;
}
.dark .rss-tab:hover {
color: #fff;
background: transparent;
}
.dark .rss-tab.is-active {
color: #fff;
background: transparent;
}
.dark .rss-tab.is-active::after {
background: #fff;
}
</style>
<?php $this->need("header.php"); ?>
<body class="jasmine-body" data-prismjs-copy="点击复制" data-prismjs-copy-error="按Ctrl+C复制" data-prismjs-copy-success="内容已复制!">
<div class="jasmine-container grid grid-cols-12">
<?php $this->need("component/sidebar-left.php"); ?>
<div class="flex col-span-12 lg:col-span-8 flex-col lg:border-x-2 border-stone-100 dark:border-neutral-600 lg:pt-0 lg:px-6 pb-10 px-3">
<?php $this->need("component/menu.php"); ?>
<div class="flex flex-col gap-y-12">
<div>
<nav class="rss-tabs" role="tablist" aria-label="RSS分类">
<?php foreach ($rssGroups as $groupKey => $group): ?>
<a
class="rss-tab<?php echo $groupKey === $activeRssTab ? ' is-active' : ''; ?>"
href="<?php echo htmlspecialchars(rss_build_tab_url($groupKey), ENT_QUOTES, 'UTF-8'); ?>"
role="tab"
aria-selected="<?php echo $groupKey === $activeRssTab ? 'true' : 'false'; ?>"
><?php echo htmlspecialchars($group['label'], ENT_QUOTES, 'UTF-8'); ?></a>
<?php endforeach; ?>
</nav>
<article class="post">
<div class="post-content" id="rss-container">
<?php echo rss_render_items_html($initialFeeds); ?>
</div>
</article>
<div style="text-align: center; margin: 2rem 0;">
<button id="loadMoreBtn" class="freelink" style="padding: 0.5rem 2rem; cursor: pointer;<?php echo $hasMoreFeeds ? '' : ' display:none;'; ?>">
加载更多
</button>
<span id="loadMoreText" style="margin-left: 1rem; color: #999; display: <?php echo $hasMoreFeeds ? 'none' : 'inline'; ?>;">
没有更多了
</span>
</div>
</div>
</div>
</div>
<div class="hidden lg:col-span-3 lg:block" id="sidebar-right">
<?php $this->need("component/sidebar.php"); ?>
</div>
</div>
<?php $this->need("footer.php"); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
fixAllLinks(document);
});
let currentPage = 1;
const activeRssTab = <?php echo json_encode($activeRssTab); ?>;
const loadMoreBtn = document.getElementById('loadMoreBtn');
const loadMoreText = document.getElementById('loadMoreText');
const rssContainer = document.getElementById('rss-container');
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', function() {
currentPage++;
loadMoreBtn.disabled = true;
loadMoreBtn.textContent = '加载中...';
const url = new URL(window.location.href);
url.searchParams.set('action', 'loadmore');
url.searchParams.set('tab', activeRssTab);
url.searchParams.set('page', currentPage);
fetch(url.toString())
.then(res => res.json())
.then(res => {
if (res.success && res.html) {
rssContainer.insertAdjacentHTML('beforeend', res.html);
fixAllLinks(rssContainer);
loadMoreBtn.disabled = false;
loadMoreBtn.textContent = '加载更多';
if (!res.hasMore) {
loadMoreBtn.style.display = 'none';
loadMoreText.style.display = 'inline';
}
} else {
loadMoreBtn.style.display = 'none';
loadMoreText.style.display = 'inline';
}
})
.catch(() => {
currentPage--;
loadMoreBtn.disabled = false;
loadMoreBtn.textContent = '加载失败,点击重试';
});
});
}
function fixAllLinks(root) {
const links = root.querySelectorAll('a');
links.forEach(link => {
const href = link.getAttribute('href');
if (
href &&
!href.startsWith('http://') &&
!href.startsWith('https://') &&
!href.startsWith('/') &&
!href.startsWith('?') &&
!href.startsWith('#') &&
!href.includes(':')
) {
link.setAttribute('href', 'https://' + href);
}
});
}
</script>
</body>
</html>
<?php rss_refresh_cache_after_response($this, $activeRssGroup, $pullTime); ?> 使用过程中也发现了一些弊端,首先是数据不好维护。随着订阅地址越来越多,自定义字段也变多。代码是按照序号进行循环查url, 但是它们的排序好像不是规律的,万一博主不小心配置了两个friends10咋整。再一个,现在是加一个tab,如果后面再想加tab又咋办。难道再来一个group1~groud50。其次,地址这么多,有些已经失效了。如何检测呢?
于是便萌生了搞波大的,直接让友圈模块超进化——插件版
插件版有圈
解决痛点: Tab地址的动态设置,可新增可删除。
订阅地址的可维护性,以视图的方式可直观性的管理,包括且不限新增、删除、关闭启用。
订阅地址的有效性检测
基于解决以上问题作为出发点,在AI工具的帮助下,最终还是完成了友圈的超进化,下面展示:

脚本工具的本质是解放双手和方便使用,还真别说。插件化之后,友圈订阅地址的管理确实更加方便直观了。对于失效的地址也能实时检测,选择性清理。
让插件解决一切问题,哈哈