友圈模块的发展史

技巧分享 · 今天 · 75 人浏览

AI摘要

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

此内容根据文章生成,不代表个人观点,仅用于文章内容的解释与总结

关于博客友圈最早的诞生及过程可以参考博主之前的两篇文章:

1787189121592663.png

第一版【友圈】的诞生,还是给博主很大的激励的,确实给博主带来了很大的便利,可以查看友链朋友们的最新动态~后续也在使用过程中也在逐步的完善和优化:

 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文件的数据。之前做的时候图省事,直接做成了博客独立页面模板——

1787190232665039.png

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

1787190473756096.png

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工具的帮助下,最终还是完成了友圈的超进化,下面展示:

1787191333810383.png

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


验证码:
取消回复
  1. obaby 今天

    让插件解决一切问题,哈哈

Theme: Jasmine | RSS订阅 | 网站地图