AI摘要

本文介绍了如何在Typecho Handsome主题中添加实时QPS图表,使其与PJAX功能兼容。通过三个核心部分:后台数据更新器、前台图表渲染器和PJAX兼容粘合剂,实现了在侧边栏显示实时QPS图表的功能。文章提供了详细的实现原理、文件准备、侧边栏集成和整合配置步骤,并提供了故障排查方法。

对于使用 Typecho Handsome 主题的博主来说,在侧边栏添加一个能实时反馈网站负载的 QPS (每秒查询率) 图表,是一项非常酷的功能。然而,Handsome 主题强大的 PJAX 无刷新加载特性,也给这类需要持续运行的动态脚本带来了挑战。

本教程将详细介绍如何创建一个与 PJAX 完全兼容的实时 QPS 图表,彻底解决在页面跳转后图表“罢工”、数据不再更新的问题。

一、最终效果

我们最终实现的效果是,在博客侧边栏有一个动态的QPS图表,无论您是首次加载页面,还是通过PJAX在不同页面间导航,这个图表都能持续、稳定地从服务器获取最新数据并实时渲染。

二、实现原理

为了完美兼容PJAX,我们将整个功能拆分为三个核心部分,各司其职:

  1. 后台数据更新器:这是“数据源”的核心。它由一个JavaScript脚本 (qps-updater.js) 和一个PHP脚本 (update-qps.php) 组成。qps-updater.js 会在后台持续运行,每隔5秒就向 update-qps.php 发起请求,后者则负责从您的WAF(如长亭雷池)获取最新的QPS数据,并将其写入一个简单的服务器缓存文件 (safeguard_qps.json)。
  2. 前台图表渲染器:这是“看得见”的部分。它位于您的侧边栏 (sidebar.php),包含一个图表容器 (<div>) 和一段独立的 ECharts 渲染脚本。这段脚本只做一件事:同样每隔5秒,从另一个PHP脚本 (get-qps.php) 读取上述的缓存文件,然后将数据绘制成图表。
  3. PJAX 兼容粘合剂:这是解决问题的关键。我们将前台图表的初始化逻辑,封装在一个全局函数 initQPSChartForHandsome() 中。然后,我们只需在 Handsome 主题后台的PJAX回调设置中填入这个函数名。这样,每次PJAX导航完成,主题就会自动调用这个函数,它会先彻底清理掉旧的图表实例和定时器,再重新初始化一个新的,从而实现了完美的无缝衔接。

这种前后端分离、逻辑解耦的架构,确保了即使在复杂的PJAX环境中,数据更新和图表渲染也能独立、稳定地运行。

三、文件准备

请将以下4个文件,按照指定的路径和内容,放置到您的主题目录中。

注意:本教程假设您已在 /usr/themes/handsome/static/js/ 目录下放置了 echarts.min.js 文件。这是本地的ECharts图表库,您可以从 ECharts官网 下载最新版本。若您没有此文件,我们的加载器会自动尝试从CDN获取,但建议使用本地版本以获得更好的加载速度和稳定性。

1. 数据读取脚本

路径: /usr/themes/handsome/component/get-qps.php
(该脚本用于让前台图表安全地读取缓存数据)

<?php
// usr/themes/handsome/component/get-qps.php

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');

$cache_dir = __DIR__ . '/cache';
$cache_file = $cache_dir . '/safeguard_qps.json';

if (file_exists($cache_file)) {
    echo file_get_contents($cache_file);
} else {
    // 如果缓存文件不存在,返回一个默认的JSON结构
    echo json_encode(['qps' => 0, 'timestamp' => time(), 'simulated' => true]);
}

2. 数据更新脚本

路径: /usr/themes/handsome/component/update-qps.php
(该脚本用于从WAF获取真实数据并写入缓存)

此处内容需要评论回复后(审核通过)方可阅读。

3. ECharts 延迟加载器

路径: /usr/themes/handsome/static/js/echarts-loader.js
(这是我们修改过的、兼容PJAX的版本)

/**
 * ECharts 延迟加载器 (PJAX兼容版)
 * 只在需要时才加载 ECharts 库,避免阻塞页面渲染
 */
(function() {
    function safeLog(message, type) {
        try {
            if (window.console && typeof window.console[type || 'log'] === 'function') {
                window.console[type || 'log'](message);
            }
        } catch (e) {}
    }

    window.ECHARTS_LOADER = {
        loaded: false,
        loading: false,
        callbacks: [],
        localPath: '/usr/themes/handsome/static/js/echarts.min.js', // 本地ECharts库路径
        cdnPath: 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js',
        
        onLoad: function(callback) {
            if (typeof callback !== 'function') return;
            if (this.loaded && window.echarts) {
                callback(window.echarts);
            } else {
                this.callbacks.push(callback);
                if (!this.loading) this.loadECharts();
            }
        },
        
        loadECharts: function() {
            if (this.loading || this.loaded) return;
            
            this.loading = true;
            var self = this;
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = this.localPath; // 优先使用本地路径
            
            script.onload = function() {
                self.loaded = true;
                self.loading = false;
                safeLog('ECharts 加载成功', 'info');
                while (self.callbacks.length > 0) {
                    try { self.callbacks.shift()(window.echarts); } catch (e) {}
                }
            };
            
            // 仅在本地加载失败时,才尝试从CDN加载
            script.onerror = function() {
                safeLog('从本地加载 ECharts 失败,尝试从 CDN 加载', 'warn');
                script.src = self.cdnPath;
                
                script.onerror = function() {
                    safeLog('从 CDN 加载 ECharts 也失败了', 'error');
                    self.loading = false;
                    self.callbacks = [];
                };
            };
            
            document.body.appendChild(script);
        }
    };
    
    function checkForEChartsElements() {
        if (window.echarts) {
            window.ECHARTS_LOADER.loaded = true;
            return;
        }
        var chartContainers = document.querySelectorAll('.top-echart, #safeguard-qps-chart, #steps-chart, #sleep-chart');
        if (chartContainers.length > 0) {
            window.ECHARTS_LOADER.loadECharts();
        }
    }
    
    if (document.readyState === 'complete') {
        checkForEChartsElements();
    } else {
        window.addEventListener('load', checkForEChartsElements);
    }
    
    // PJAX 兼容处理
    document.addEventListener('pjax:end', checkForEChartsElements);
    
    window.loadECharts = function(callback) {
        window.ECHARTS_LOADER.onLoad(callback);
    };
})();

4. 后台数据更新器 (JS)

路径: /usr/themes/handsome/component/qps-updater.js
(这是我们修改过的、兼容PJAX的版本)

此处内容需要评论回复后(审核通过)方可阅读。

四、侧边栏集成

现在,编辑您的侧边栏文件(例如 usr/themes/handsome/component/sidebar.php 或主题本身的 sidebar.php),在您希望显示图表的位置,加入以下代码块。

<!-- 雷池WAF实时QPS图表 -->
<section id="safeguard_qps_chart_widget" class="widget widget_categories wrapper-md padder-v-none clear">
    <h5 class="widget-title m-t-none">实时QPS</h5>
    <div class="panel wrapper-sm padder-v-ssm">
        <div class="safeguard-qps-chart-container" style="position:relative; height:180px; padding:15px; border-radius:8px; box-shadow:0 2px 5px rgba(0,0,0,0.05);">
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
                <span style="font-size:16px; font-weight:bold; color:#333;">
                    <i data-feather="activity" style="width:16px; height:16px; vertical-align:middle; margin-right:4px; color:#20c997;"></i>
                    <span id="current-qps">0</span>
                </span>
                <span id="refresh-btn" style="cursor:pointer; color:#20c997;">
                    <i data-feather="refresh-cw" style="width:14px; height:14px;"></i>
                </span>
            </div>
            <div id="safeguard-qps-chart" style="width:100%; height:120px;"></div>
        </div>

        <script>
            (function() {
                // 使用闭包管理状态,避免全局污染
                let qpsChart = null;
                let intervalId = null;
                let qpsData = [];
                let timeLabels = [];
                const maxDataPoints = 15;

                function cleanup() {
                    if (intervalId) clearInterval(intervalId);
                    if (qpsChart) { try { qpsChart.dispose(); } catch (e) {} }
                    qpsChart = null;
                    intervalId = null;
                    qpsData = [];
                    timeLabels = [];
                    const chartElement = document.getElementById('safeguard-qps-chart');
                    if (chartElement) chartElement.removeAttribute('data-qps-init');
                }

                // 将初始化函数暴露到全局,以便Handsome主题回调
                window.initQPSChartForHandsome = function() {
                    cleanup();
                    const chartElement = document.getElementById('safeguard-qps-chart');
                    if (!chartElement) return;

                    function fetchQPSData() {
                        fetch('/usr/themes/handsome/component/get-qps.php?t=' + Date.now())
                        .then(res => res.json())
                        .then(data => {
                            if (!data) return;
                            const qpsVal = parseFloat(data.qps || 0);
                            const now = new Date();
                            const timeStr = now.getHours() + ':' + ('0' + now.getMinutes()).slice(-2) + ':' + ('0' + now.getSeconds()).slice(-2);

                            const currentQpsEl = document.getElementById('current-qps');
                            if (currentQpsEl) currentQpsEl.textContent = qpsVal.toFixed(2);

                            qpsData.push(qpsVal);
                            timeLabels.push(timeStr);

                            if (qpsData.length > maxDataPoints) {
                                qpsData.shift();
                                timeLabels.shift();
                            }
                            drawChart();
                        }).catch(console.error);
                    }

                    function drawChart() {
                        if (!window.loadECharts) return;
                        window.loadECharts(function(echarts) {
                            try {
                                if (!qpsChart) {
                                    qpsChart = echarts.init(chartElement);
                                    window.addEventListener('resize', () => qpsChart && qpsChart.resize());
                                }
                                qpsChart.setOption({
                                    tooltip: { trigger: 'axis', formatter: p => `${timeLabels[p[0].dataIndex]}<br/>QPS: <strong>${p[0].value}</strong>` },
                                    grid: { left: '3%', right: '10%', bottom: '10%', top: '10%', containLabel: true },
                                    xAxis: { type: 'category', data: timeLabels, show: false },
                                    yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
                                    series: [{ data: qpsData, type: 'line', smooth: true, showSymbol: false, lineStyle: { color: '#20c997' }, areaStyle: { color: '#20c997', opacity: 0.1 } }]
                                });
                            } catch (e) { console.error("ECharts渲染失败:", e); }
                        });
                    }
                    
                    // 动态加载后台更新器脚本
                    if (typeof window.qpsUpdaterLoaded === 'undefined') {
                        const updaterScript = document.createElement('script');
                        updaterScript.src = '/usr/themes/handsome/component/qps-updater.js?v=' + Date.now();
                        document.body.appendChild(updaterScript);
                        window.qpsUpdaterLoaded = true;
                    }

                    fetchQPSData();
                    intervalId = setInterval(fetchQPSData, 5000);
                    
                    const refreshBtn = document.getElementById('refresh-btn');
                    if (refreshBtn) {
                        const newBtn = refreshBtn.cloneNode(true);
                        refreshBtn.parentNode.replaceChild(newBtn, refreshBtn);
                        newBtn.addEventListener('click', fetchQPSData);
                    }
                };

                // 首次加载时自动运行
                document.addEventListener('DOMContentLoaded', window.initQPSChartForHandsome);
            })();
        </script>
    </div>
</section>

五、整合与配置 (关键步骤)

  1. 确认文件位置:确保您已经将第三、四部分的所有文件都正确地放置到了您的主题目录下。

    • 数据操作相关脚本:/usr/themes/handsome/component/ 目录下的 get-qps.phpupdate-qps.php
    • ECharts 加载器和库文件:/usr/themes/handsome/static/js/ 目录下的 echarts-loader.jsecharts.min.js
    • 后台更新脚本:/usr/themes/handsome/component/ 目录下的 qps-updater.js
  2. 准备ECharts库:如果您还没有,请从ECharts官网下载最新版本的echarts.min.js文件,并将其放置于/usr/themes/handsome/static/js/目录下,与加载器脚本位于同一目录。这是本地图表库文件,可提供更好的加载性能。
  3. 创建缓存目录:手动在 /usr/themes/handsome/component/ 目录下创建一个名为 cache 的文件夹,并确保PHP对它有写入权限(权限 755777)。
  4. 引入核心加载器:编辑主题的 header.phpfooter.php 文件,确保 echarts-loader.js 被引入。您只需添加一行:
    <script src="<?php $this->options->themeUrl('static/js/echarts-loader.js'); ?>"></script>
  5. 配置 Handsome 主题 PJAX 回调

    • 进入您的 Handsome 主题后台
    • 找到 PJAX相关设置(通常在"全局设置"或"速度优化"等菜单下)。
    • 检查"PJAX回调函数"输入框中的内容:

      • 如果输入框为空:直接填入 initQPSChartForHandsome
      • 如果已有其他函数:您需要采用以下方式整合多个函数(请勿直接覆盖现有内容):

        // 保留您现有的所有函数代码...
        
        // 在最后添加QPS图表初始化
        if (typeof initQPSChartForHandsome === 'function') {
        initQPSChartForHandsome();
        }
    • 保存设置。

六、故障排查

如果配置后图表未显示或数据不更新,请按以下步骤检查:

  1. 清除浏览器缓存,强制加载最新的JS文件。
  2. 检查缓存目录权限:确认 /usr/themes/handsome/component/cache 目录存在且可写。
  3. 打开浏览器开发者工具 (F12)

    • 切换到 “控制台 (Console)” 选项卡,查看是否有红色的错误信息。我们开启了调试模式,您应该能看到 [QPS后台更新器] 开头的日志,这表示后台更新脚本正在工作。
    • 切换到 “网络 (Network)” 选项卡,筛选 Fetch/XHR,检查是否每5秒都有对 update-qps.phpget-qps.php 的网络请求,以及它们的返回状态是否为 200

通过以上步骤,您就能在博客中拥有一个稳定、可靠、且与 Handsome 主题完美兼容的实时QPS监控图表了。

如果觉得我的文章对你有用,请随意赞赏
END
本文作者:
文章标题:Typecho Handsome 添加实时QPS图表 - 兼容pjax
本文地址:https://blog.ybyq.wang/archives/776.html
版权说明:若无注明,本文皆Xuan's blog原创,转载请保留文章出处。