数据页面 PJAX 兼容性修复
数据页面 PJAX 兼容性修复
🐛 问题描述
症状
数据页面(/data/)在第一次通过 PJAX 点击打开时,没有显示访问统计数据,所有数据卡片显示为骨架屏加载状态(--)。需要手动刷新页面(F5)才能正确显示数据。
用户体验影响
- ❌ 第一次点击"数据"菜单 → 页面显示但无数据
- ✅ 手动刷新页面(F5)→ 数据正常显示
- ✅ 直接访问
/data/URL → 数据正常显示
问题确认
这是典型的 PJAX 兼容性问题。
🔍 问题原因分析
技术原因
1. 内联脚本执行时机问题
原实现方式:
<!-- data.html 中的内联脚本 -->
<script>
(function() {
const apiUrl = '{{ site.Params.analytics.umami.apiUrl }}';
// 页面加载完成后获取数据
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fetchAnalytics);
} else {
fetchAnalytics();
}
// PJAX兼容:监听PJAX加载完成事件
document.addEventListener('pjax:complete', fetchAnalytics);
})();
</script>问题所在:
IIFE(立即执行函数)限制:
- 脚本使用
(function() { ... })()立即执行函数 - 在 PJAX 首次加载数据页面时,脚本确实会执行
- 但此时
pjax:complete事件已经触发完毕 - 事件监听器注册得太晚,错过了事件触发
- 脚本使用
事件监听器注册顺序:
PJAX 导航开始 ↓ PJAX 更新 DOM ↓ 触发 pjax:complete 事件 ← 此时事件已触发 ↓ 浏览器解析并执行新页面的 <script> ↓ 注册 pjax:complete 监听器 ← 太晚了,事件已经过去DOMContentLoaded 也不会触发:
- PJAX 导航时,DOM 已经处于
complete状态 DOMContentLoaded只在初始页面加载时触发一次- 所以
document.readyState === 'loading'条件永远为 false else分支中的fetchAnalytics()本应执行,但...
- PJAX 导航时,DOM 已经处于
关键问题:脚本位置:
- 内联脚本在页面底部(
</div>之后) - PJAX 更新 DOM 后,脚本虽然被插入,但执行时机不确定
- 在某些情况下,脚本可能在 DOM 元素(
#umami-analytics)渲染之前执行 - 导致
document.getElementById('umami-analytics')返回null
- 内联脚本在页面底部(
2. PJAX 脚本执行机制
PJAX 在更新页面内容时:
- ✅ 会重新插入
<script>标签 - ✅ 浏览器会执行这些脚本
- ❌ 但
pjax:complete事件在脚本执行之前就已经触发 - ❌ 脚本内部的事件监听器无法捕获到当前这次导航的
pjax:complete事件
3. 为什么刷新后正常?
直接访问或刷新页面时:
- 页面初始加载,
DOMContentLoaded或立即执行的else分支会触发 - DOM 元素完全渲染
- API 请求正常发起
- 数据正常显示
✅ 修复方案
解决思路
将数据页面的 JavaScript 逻辑提取为独立模块,通过全局函数暴露初始化接口,在 PJAX 的 reinitializePage 函数中主动调用。
修复步骤
1. 创建独立的 JavaScript 模块
新建文件:themes/demius/assets/js/_data-page.js
/**
* 数据页面 - Umami 统计数据获取和显示
*/
// 全局初始化函数
window.initDataPage = function() {
// 检查是否是数据页面
const analyticsGrid = document.getElementById('umami-analytics');
if (!analyticsGrid) {
return; // 不是数据页面,直接返回
}
// 从页面中获取 API URL(通过 data 属性)
const apiUrl = analyticsGrid.dataset.apiUrl;
if (!apiUrl) {
console.warn('数据页面:未配置 API URL');
return;
}
// 获取统计数据
async function fetchAnalytics() {
// ... API 请求逻辑
}
// 更新UI
function updateAnalyticsUI(data) {
// ... UI 更新逻辑
}
// 数字动画
function animateNumber(element, start, end, duration) {
// ... 动画逻辑
}
// 错误处理
function showError(message) {
// ... 错误显示逻辑
}
// 执行数据获取
fetchAnalytics();
};
// 页面加载时初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', window.initDataPage);
} else {
window.initDataPage();
}关键改进:
- ✅ 全局函数
window.initDataPage,可以被外部主动调用 - ✅ 页面初始加载时自动执行
- ✅ 可以在 PJAX 回调中主动调用
- ✅ 通过
data-api-url属性传递配置,避免模板变量
2. 在主 JS 中导入模块
修改文件:themes/demius/assets/js/main.js
import './_data-page.js';3. 修改数据页面模板
修改文件:themes/demius/layouts/data.html
移除内联脚本:
<!-- 删除整个 <script> 标签 -->添加 data 属性:
<div class="analytics-grid"
id="umami-analytics"
data-api-url="{{ site.Params.analytics.umami.apiUrl }}">为什么用 data 属性?
- ✅ 避免在 JS 模块中使用 Hugo 模板变量
- ✅ 配置数据与逻辑代码分离
- ✅ JavaScript 可以方便地读取
element.dataset.apiUrl
4. 在 PJAX 中调用初始化函数
修改文件:themes/demius/assets/js/_simple-pjax.js
在 reinitializePage() 函数中添加:
function reinitializePage() {
// ... 其他初始化代码 ...
// 重新初始化数据页面功能
if (window.initDataPage) {
window.initDataPage();
}
// ... 其他初始化代码 ...
}执行时机:
PJAX 导航开始
↓
PJAX 更新 DOM
↓
触发 pjax:complete 事件
↓
执行 reinitializePage() ← 我们的代码在这里
↓
主动调用 initDataPage() ← 确保执行
↓
检查是否是数据页面
↓
发起 API 请求
↓
更新 UI 显示数据🎯 修复效果
修复前
用户点击"数据"菜单
↓
PJAX 加载数据页面
↓
页面显示,但数据为 "--"(骨架屏)
↓
用户等待... 数据不会加载 ❌
↓
用户刷新页面(F5)
↓
数据正常显示 ✅修复后
用户点击"数据"菜单
↓
PJAX 加载数据页面
↓
页面显示
↓
initDataPage() 自动执行 ✅
↓
API 请求发起
↓
数据实时更新显示 ✅
↓
数字递增动画流畅呈现 ✨🔧 技术要点
1. 全局函数暴露模式
为什么使用全局函数?
// ❌ 不好的方式:闭包中的函数无法被外部调用
(function() {
function init() {
// ...
}
init(); // 只会在脚本加载时执行一次
})();
// ✅ 好的方式:全局函数可以被重复调用
window.initDataPage = function() {
// ...
};优点:
- ✅ PJAX 可以主动调用
- ✅ 可以在任何时机重新初始化
- ✅ 调试方便(可以在控制台手动调用)
2. 防御性编程
window.initDataPage = function() {
// 检查页面元素是否存在
const analyticsGrid = document.getElementById('umami-analytics');
if (!analyticsGrid) {
return; // 不是数据页面,安全退出
}
// 检查配置是否存在
const apiUrl = analyticsGrid.dataset.apiUrl;
if (!apiUrl) {
console.warn('数据页面:未配置 API URL');
return;
}
// ... 继续执行
};好处:
- ✅ 在非数据页面调用不会报错
- ✅ 缺少配置时给出友好提示
- ✅ 提高代码健壮性
3. PJAX 兼容性最佳实践
规则 1:功能模块化
- 每个功能独立为一个 JS 文件
- 暴露
init函数供外部调用
规则 2:避免直接事件监听
// ❌ 不好:在模块内部监听 PJAX 事件
document.addEventListener('pjax:complete', myFunction);
// ✅ 好:由 PJAX 主动调用
if (window.myFunction) {
window.myFunction();
}规则 3:清理和重建
// 有状态的功能需要先清理
if (window.cleanup) window.cleanup();
// 再重新初始化
if (window.init) window.init();规则 4:使用 data 属性传递配置
<!-- ✅ 好:使用 data 属性 -->
<div id="my-widget" data-api-url="{{ .Site.Params.apiUrl }}"></div>
<!-- ❌ 不好:在内联脚本中使用模板变量 -->
<script>
const apiUrl = '{{ .Site.Params.apiUrl }}';
</script>📊 影响范围
修改的文件
✅ 新建:
themes/demius/assets/js/_data-page.js- 数据页面的完整逻辑
- 约 120 行代码
✅ 修改:
themes/demius/assets/js/main.js- 添加
import './_data-page.js'; - 1 行改动
- 添加
✅ 修改:
themes/demius/layouts/data.html- 移除内联
<script>标签(约 120 行) - 添加
data-api-url属性(1 行) - 净减少约 119 行
- 移除内联
✅ 修改:
themes/demius/assets/js/_simple-pjax.js- 在
reinitializePage()中添加调用(3 行)
- 在
向后兼容性
- ✅ 完全向后兼容
- ✅ 不影响任何现有功能
- ✅ 不需要修改配置文件
- ✅ 直接访问数据页面仍然正常工作
其他功能影响
- ✅ 不影响任何其他页面
- ✅ 不影响 PJAX 的其他功能
- ✅ 不影响样式和布局
- ✅ Hugo 本地预览服务器端口不变(1313)
🧪 测试验证
测试场景 1:PJAX 导航
- 访问首页
/ - 点击"数据"菜单
- 预期结果:
- ✅ 页面平滑切换(无刷新)
- ✅ 数据立即开始加载(显示骨架屏)
- ✅ 1-2秒后数据显示(数字递增动画)
- ✅ 不需要手动刷新
测试场景 2:直接访问
- 在浏览器地址栏输入
/data/ - 预期结果:
- ✅ 页面正常加载
- ✅ 数据正常显示
- ✅ 与修复前行为一致
测试场景 3:刷新页面
- 在数据页面按 F5 刷新
- 预期结果:
- ✅ 页面重新加载
- ✅ 数据正常显示
- ✅ 与修复前行为一致
测试场景 4:多次切换
- 首页 → 数据页 → 文章页 → 数据页 → 首页 → 数据页
- 预期结果:
- ✅ 每次切换到数据页都能正常显示数据
- ✅ 无需手动刷新
- ✅ 动画效果流畅
测试场景 5:控制台验证
打开浏览器控制台,观察:
- ✅ 没有 JavaScript 错误
- ✅ API 请求正常发起(Network 面板)
- ✅ 返回数据格式正确
- ✅ UI 更新正常
💡 扩展说明
为什么不使用 MutationObserver?
有人可能会想:是否可以使用 MutationObserver 监听 DOM 变化?
// ❌ 复杂且容易出bug
const observer = new MutationObserver((mutations) => {
if (document.getElementById('umami-analytics')) {
initDataPage();
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });为什么不推荐?
- ❌ 性能开销大(监听整个 body)
- ❌ 可能触发多次
- ❌ 需要正确断开监听
- ❌ 代码复杂,不易维护
我们的方案更好:
- ✅ 由 PJAX 主动调用,时机精确
- ✅ 只在需要时执行一次
- ✅ 代码简洁清晰
- ✅ 易于理解和维护
其他页面是否也有类似问题?
检查了主题中其他使用内联脚本的页面:
- ✅ 说说页面:已经使用独立的
_shuoshuo.js模块,无问题 - ✅ 相册页面:已经使用独立的
_gallery.js模块,无问题 - ✅ 友链页面:已经使用独立的
_links.js模块,无问题 - ✅ 文章页面:功能都已模块化,无问题
结论:数据页面是唯一使用内联脚本的特殊页面,现已修复。
🎉 总结
问题本质
PJAX 导航时,内联脚本的执行时机与事件触发顺序不匹配,导致初始化函数未被调用。
解决方案
将内联脚本改为独立模块,暴露全局初始化函数,由 PJAX 主动调用。
技术收益
- ✅ 修复了 PJAX 兼容性问题
- ✅ 代码结构更清晰(独立模块 vs 内联脚本)
- ✅ 可维护性提高(集中管理 vs 分散在模板中)
- ✅ 复用性增强(全局函数可以在任何地方调用)
- ✅ 调试更方便(可以在控制台手动调用
initDataPage())
用户体验提升
- 🚀 数据页面首次加载即可显示数据
- 🎨 流畅的数字递增动画
- ⚡ 无需手动刷新
- 💯 完美的 PJAX 无刷新体验
修复日期: 2025-10-28
影响版本: v2.4.0
修复版本: v2.4.1
优先级: 🟡 中(影响用户体验但有手动刷新的workaround)
难度: 🟢 简单(标准的 PJAX 兼容性处理)
