1062 lines
27 KiB
Markdown
1062 lines
27 KiB
Markdown
|
|
# 设计文档
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
本设计文档针对 Argon 主题的 PJAX 和 LazyLoad 功能进行优化。通过深入分析 argontheme.js(3700+ 行)的实际实现,我们识别出了重复初始化、资源泄漏、性能问题等关键缺陷。
|
|||
|
|
|
|||
|
|
**优化目标:**
|
|||
|
|
- 消除 PJAX 事件处理中的重复初始化调用
|
|||
|
|
- 完善 LazyLoad Observer 的生命周期管理
|
|||
|
|
- 优化图片加载效果,使用 requestAnimationFrame 替代 setTimeout
|
|||
|
|
- 改进 GT4 验证码、Zoomify、Tippy 等第三方库的清理逻辑
|
|||
|
|
- 优化滚动位置管理和移动端目录初始化
|
|||
|
|
|
|||
|
|
**设计原则:**
|
|||
|
|
- 最小化修改:优先重构现有代码,避免大规模重写
|
|||
|
|
- 向后兼容:保持现有 API 和配置选项不变
|
|||
|
|
- 性能优先:减少不必要的 DOM 操作和重复计算
|
|||
|
|
- 渐进增强:确保在旧浏览器上有合理的降级方案
|
|||
|
|
|
|||
|
|
## 架构
|
|||
|
|
|
|||
|
|
### 当前架构问题
|
|||
|
|
|
|||
|
|
**PJAX 事件流(当前):**
|
|||
|
|
```
|
|||
|
|
pjax:click
|
|||
|
|
↓
|
|||
|
|
pjax:send (NProgress.set(0.618))
|
|||
|
|
↓
|
|||
|
|
pjax:beforeReplace
|
|||
|
|
├─ 清理 lazyloadObserver
|
|||
|
|
├─ 清理 zoomifyInstances
|
|||
|
|
└─ 清理 Tippy 实例
|
|||
|
|
↓
|
|||
|
|
pjax:complete
|
|||
|
|
├─ waterflowInit() ← 重复调用
|
|||
|
|
├─ lazyloadInit() ← 重复调用
|
|||
|
|
├─ zoomifyInit()
|
|||
|
|
├─ highlightJsRender()
|
|||
|
|
├─ ... (10+ 个初始化函数)
|
|||
|
|
└─ 恢复滚动位置
|
|||
|
|
↓
|
|||
|
|
pjax:end
|
|||
|
|
├─ setTimeout 100ms
|
|||
|
|
│ ├─ waterflowInit() ← 重复调用
|
|||
|
|
│ └─ lazyloadInit() ← 重复调用
|
|||
|
|
└─ GT4 验证码重置
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题分析:**
|
|||
|
|
1. `waterflowInit()` 和 `lazyloadInit()` 在 `pjax:complete` 和 `pjax:end` 中都被调用
|
|||
|
|
2. `pjax:end` 使用 100ms 延迟,可能导致竞态条件
|
|||
|
|
3. 资源清理在 `beforeReplace` 中进行,但某些资源可能在 `complete` 后才创建
|
|||
|
|
|
|||
|
|
### 优化后架构
|
|||
|
|
|
|||
|
|
**PJAX 事件流(优化后):**
|
|||
|
|
```
|
|||
|
|
pjax:click
|
|||
|
|
↓
|
|||
|
|
pjax:send (NProgress.set(0.618))
|
|||
|
|
↓
|
|||
|
|
pjax:beforeReplace
|
|||
|
|
├─ cleanupResources()
|
|||
|
|
│ ├─ 清理 lazyloadObserver
|
|||
|
|
│ ├─ 清理 zoomifyInstances
|
|||
|
|
│ ├─ 清理 Tippy 实例
|
|||
|
|
│ └─ 清理事件监听器
|
|||
|
|
└─ 更新 UI 状态
|
|||
|
|
↓
|
|||
|
|
pjax:complete
|
|||
|
|
├─ initializePageResources()
|
|||
|
|
│ ├─ waterflowInit()
|
|||
|
|
│ ├─ lazyloadInit()
|
|||
|
|
│ ├─ zoomifyInit()
|
|||
|
|
│ ├─ highlightJsRender()
|
|||
|
|
│ └─ ... (其他初始化)
|
|||
|
|
└─ 恢复滚动位置
|
|||
|
|
↓
|
|||
|
|
pjax:end
|
|||
|
|
└─ finalizePageLoad()
|
|||
|
|
├─ resetMobileCatalog()
|
|||
|
|
└─ resetGT4Captcha()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优化要点:**
|
|||
|
|
1. 将资源清理逻辑封装到 `cleanupResources()` 函数
|
|||
|
|
2. 将初始化逻辑封装到 `initializePageResources()` 函数
|
|||
|
|
3. 移除 `pjax:end` 中的重复初始化调用
|
|||
|
|
4. 使用函数封装提高代码可维护性
|
|||
|
|
|
|||
|
|
## 组件和接口
|
|||
|
|
|
|||
|
|
### 1. 资源清理模块
|
|||
|
|
|
|||
|
|
**函数:`cleanupPjaxResources()`**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
/**
|
|||
|
|
* 清理 PJAX 页面切换前的所有资源
|
|||
|
|
* 在 pjax:beforeReplace 事件中调用
|
|||
|
|
*/
|
|||
|
|
function cleanupPjaxResources() {
|
|||
|
|
// 清理 LazyLoad Observer
|
|||
|
|
if (lazyloadObserver) {
|
|||
|
|
lazyloadObserver.disconnect();
|
|||
|
|
lazyloadObserver = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理 Zoomify 实例
|
|||
|
|
if (zoomifyInstances && zoomifyInstances.length > 0) {
|
|||
|
|
zoomifyInstances.forEach(function(instance) {
|
|||
|
|
try {
|
|||
|
|
if (instance && typeof instance.destroy === 'function') {
|
|||
|
|
instance.destroy();
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
console.warn('Failed to destroy Zoomify instance:', e);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
zoomifyInstances = [];
|
|||
|
|
}
|
|||
|
|
$('img.zoomify-initialized').removeClass('zoomify-initialized');
|
|||
|
|
|
|||
|
|
// 清理 Tippy 实例
|
|||
|
|
if (typeof tippy !== 'undefined') {
|
|||
|
|
document.querySelectorAll('[data-tippy-root]').forEach(function(el) {
|
|||
|
|
try {
|
|||
|
|
if (el._tippy && typeof el._tippy.destroy === 'function') {
|
|||
|
|
el._tippy.destroy();
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
console.warn('Failed to destroy Tippy instance:', e);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
$('.tippy-initialized').removeClass('tippy-initialized');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. LazyLoad 优化模块
|
|||
|
|
|
|||
|
|
**函数:`lazyloadInit()` 优化版**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
/**
|
|||
|
|
* 初始化图片懒加载
|
|||
|
|
* 优化:检查现有 Observer,使用 requestAnimationFrame 优化加载效果
|
|||
|
|
*/
|
|||
|
|
function lazyloadInit() {
|
|||
|
|
// 清理旧的 Observer(防御性编程)
|
|||
|
|
if (lazyloadObserver) {
|
|||
|
|
lazyloadObserver.disconnect();
|
|||
|
|
lazyloadObserver = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否启用懒加载
|
|||
|
|
if (argonConfig.lazyload === false || argonConfig.lazyload === 'false') {
|
|||
|
|
loadAllImagesImmediately();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let images = document.querySelectorAll('img.lazyload[data-src]');
|
|||
|
|
if (images.length === 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let effect = argonConfig.lazyload_effect || 'fadeIn';
|
|||
|
|
let threshold = parseInt(argonConfig.lazyload_threshold) || 800;
|
|||
|
|
|
|||
|
|
// 使用 IntersectionObserver 实现懒加载
|
|||
|
|
if ('IntersectionObserver' in window) {
|
|||
|
|
lazyloadObserver = new IntersectionObserver(function(entries) {
|
|||
|
|
entries.forEach(function(entry) {
|
|||
|
|
if (entry.isIntersecting) {
|
|||
|
|
let img = entry.target;
|
|||
|
|
loadImageOptimized(img, effect);
|
|||
|
|
lazyloadObserver.unobserve(img);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}, {
|
|||
|
|
rootMargin: threshold + 'px 0px'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
images.forEach(function(img) {
|
|||
|
|
// 重置图片状态
|
|||
|
|
img.style.opacity = '';
|
|||
|
|
img.style.transform = '';
|
|||
|
|
img.style.transition = '';
|
|||
|
|
lazyloadObserver.observe(img);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 降级方案:使用滚动监听
|
|||
|
|
lazyloadFallback(images, effect, threshold);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 优化的图片加载函数
|
|||
|
|
* 使用 requestAnimationFrame 替代 setTimeout
|
|||
|
|
*/
|
|||
|
|
function loadImageOptimized(img, effect) {
|
|||
|
|
let src = img.getAttribute('data-src');
|
|||
|
|
if (!src) return;
|
|||
|
|
|
|||
|
|
// 预加载图片
|
|||
|
|
let tempImg = new Image();
|
|||
|
|
tempImg.onload = function() {
|
|||
|
|
img.src = src;
|
|||
|
|
img.removeAttribute('data-src');
|
|||
|
|
img.classList.remove('lazyload');
|
|||
|
|
|
|||
|
|
// 移除所有 lazyload-style-* 类
|
|||
|
|
img.className = img.className.replace(/\blazyload-style-\d+\b/g, '').trim();
|
|||
|
|
|
|||
|
|
// 使用 requestAnimationFrame 应用加载效果
|
|||
|
|
applyLoadEffectOptimized(img, effect);
|
|||
|
|
};
|
|||
|
|
tempImg.onerror = function() {
|
|||
|
|
// 加载失败时使用原始 src
|
|||
|
|
img.src = src;
|
|||
|
|
img.removeAttribute('data-src');
|
|||
|
|
img.classList.remove('lazyload');
|
|||
|
|
};
|
|||
|
|
tempImg.src = src;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 使用 requestAnimationFrame 应用加载效果
|
|||
|
|
*/
|
|||
|
|
function applyLoadEffectOptimized(img, effect) {
|
|||
|
|
if (effect === 'fadeIn') {
|
|||
|
|
img.style.opacity = '0';
|
|||
|
|
img.style.transition = 'opacity 0.3s ease';
|
|||
|
|
requestAnimationFrame(function() {
|
|||
|
|
requestAnimationFrame(function() {
|
|||
|
|
img.style.opacity = '1';
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
// 清理 transition 样式
|
|||
|
|
setTimeout(function() {
|
|||
|
|
img.style.transition = '';
|
|||
|
|
}, 310);
|
|||
|
|
} else if (effect === 'slideDown') {
|
|||
|
|
img.style.opacity = '0';
|
|||
|
|
img.style.transform = 'translateY(-20px)';
|
|||
|
|
img.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|||
|
|
requestAnimationFrame(function() {
|
|||
|
|
requestAnimationFrame(function() {
|
|||
|
|
img.style.opacity = '1';
|
|||
|
|
img.style.transform = 'translateY(0)';
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
// 清理样式
|
|||
|
|
setTimeout(function() {
|
|||
|
|
img.style.transition = '';
|
|||
|
|
img.style.transform = '';
|
|||
|
|
}, 310);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 降级方案:使用滚动监听实现懒加载
|
|||
|
|
*/
|
|||
|
|
function lazyloadFallback(images, effect, threshold) {
|
|||
|
|
let loadedImages = new Set();
|
|||
|
|
|
|||
|
|
function checkImagesInView() {
|
|||
|
|
let viewportHeight = window.innerHeight;
|
|||
|
|
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
|||
|
|
|
|||
|
|
images.forEach(function(img) {
|
|||
|
|
if (loadedImages.has(img)) return;
|
|||
|
|
|
|||
|
|
let rect = img.getBoundingClientRect();
|
|||
|
|
if (rect.top < viewportHeight + threshold && rect.bottom > -threshold) {
|
|||
|
|
loadImageOptimized(img, effect);
|
|||
|
|
loadedImages.add(img);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用节流函数优化性能
|
|||
|
|
let throttleTimer = null;
|
|||
|
|
function throttledCheck() {
|
|||
|
|
if (throttleTimer) return;
|
|||
|
|
throttleTimer = setTimeout(function() {
|
|||
|
|
checkImagesInView();
|
|||
|
|
throttleTimer = null;
|
|||
|
|
}, 100);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绑定事件监听器
|
|||
|
|
window.addEventListener('scroll', throttledCheck, {passive: true});
|
|||
|
|
window.addEventListener('resize', throttledCheck, {passive: true});
|
|||
|
|
|
|||
|
|
// 立即检查一次
|
|||
|
|
checkImagesInView();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 立即加载所有图片(懒加载禁用时)
|
|||
|
|
*/
|
|||
|
|
function loadAllImagesImmediately() {
|
|||
|
|
let images = document.querySelectorAll('img.lazyload[data-src]');
|
|||
|
|
images.forEach(function(img) {
|
|||
|
|
let src = img.getAttribute('data-src');
|
|||
|
|
if (src) {
|
|||
|
|
img.src = src;
|
|||
|
|
img.removeAttribute('data-src');
|
|||
|
|
img.classList.remove('lazyload');
|
|||
|
|
img.className = img.className.replace(/\blazyload-style-\d+\b/g, '').trim();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. PJAX 事件处理优化
|
|||
|
|
|
|||
|
|
**优化后的事件处理器:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
$(document).on('pjax:beforeReplace', function(e, dom) {
|
|||
|
|
// 清理旧页面的资源
|
|||
|
|
cleanupPjaxResources();
|
|||
|
|
|
|||
|
|
// 更新 UI 状态
|
|||
|
|
if ($('#post_comment', dom[0]).length > 0) {
|
|||
|
|
$('#fabtn_go_to_comment').removeClass('d-none');
|
|||
|
|
} else {
|
|||
|
|
$('#fabtn_go_to_comment').addClass('d-none');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理滚动位置
|
|||
|
|
if ($('html').hasClass('banner-as-cover')) {
|
|||
|
|
if (!$('#main').hasClass('article-list-home')) {
|
|||
|
|
pjaxScrollTop = 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$(document).on('pjax:complete', function() {
|
|||
|
|
pjaxLoading = false;
|
|||
|
|
NProgress.inc();
|
|||
|
|
|
|||
|
|
// MathJax 渲染
|
|||
|
|
try {
|
|||
|
|
if (typeof MathJax !== 'undefined') {
|
|||
|
|
if (MathJax.Hub !== undefined) {
|
|||
|
|
MathJax.Hub.Typeset();
|
|||
|
|
} else {
|
|||
|
|
MathJax.typeset();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.warn('MathJax rendering failed:', err);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// KaTeX 渲染
|
|||
|
|
try {
|
|||
|
|
if (typeof renderMathInElement !== 'undefined') {
|
|||
|
|
renderMathInElement(document.body, {
|
|||
|
|
delimiters: [
|
|||
|
|
{left: '$', right: '$', display: true},
|
|||
|
|
{left: '$', right: '$', display: false},
|
|||
|
|
{left: '\\(', right: '\\)', display: false}
|
|||
|
|
]
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.warn('KaTeX rendering failed:', err);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化页面资源(只调用一次)
|
|||
|
|
waterflowInit();
|
|||
|
|
lazyloadInit();
|
|||
|
|
zoomifyInit();
|
|||
|
|
highlightJsRender();
|
|||
|
|
panguInit();
|
|||
|
|
clampInit();
|
|||
|
|
tippyInit();
|
|||
|
|
getGithubInfoCardContent();
|
|||
|
|
showPostOutdateToast();
|
|||
|
|
calcHumanTimesOnPage();
|
|||
|
|
foldLongComments();
|
|||
|
|
foldLongShuoshuo();
|
|||
|
|
$('html').trigger('resize');
|
|||
|
|
|
|||
|
|
// 恢复滚动位置
|
|||
|
|
if (pjaxScrollTop > 0) {
|
|||
|
|
$('body,html').scrollTop(pjaxScrollTop);
|
|||
|
|
pjaxScrollTop = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用用户自定义回调
|
|||
|
|
if (typeof window.pjaxLoaded === 'function') {
|
|||
|
|
try {
|
|||
|
|
window.pjaxLoaded();
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('pjaxLoaded callback failed:', err);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NProgress.done();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$(document).on('pjax:end', function() {
|
|||
|
|
// 只处理特殊的后置任务,不重复初始化
|
|||
|
|
|
|||
|
|
// 重置移动端目录状态
|
|||
|
|
if (typeof window.resetMobileCatalog === 'function') {
|
|||
|
|
try {
|
|||
|
|
window.resetMobileCatalog();
|
|||
|
|
} catch (err) {
|
|||
|
|
console.warn('resetMobileCatalog failed:', err);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GT4: PJAX 后确保评论页验证码已初始化
|
|||
|
|
resetGT4Captcha();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置 GT4 验证码
|
|||
|
|
*/
|
|||
|
|
function resetGT4Captcha() {
|
|||
|
|
try {
|
|||
|
|
if ($('#geetest-captcha').length > 0) {
|
|||
|
|
// 重置前端状态,避免重复提交阻塞
|
|||
|
|
window.geetestVerified = false;
|
|||
|
|
window.geetestAutoSubmitting = false;
|
|||
|
|
|
|||
|
|
// 清空隐藏字段,防止残留导致 pass_token 复用
|
|||
|
|
$('#geetest_lot_number').val('');
|
|||
|
|
$('#geetest_captcha_output').val('');
|
|||
|
|
$('#geetest_pass_token').val('');
|
|||
|
|
$('#geetest_gen_time').val('');
|
|||
|
|
|
|||
|
|
// 清空容器,防止重复 appendTo 导致多个实例
|
|||
|
|
$('#geetest-captcha').empty();
|
|||
|
|
|
|||
|
|
// 若页面脚本已提供初始化方法,则调用以加载验证码
|
|||
|
|
if (typeof initGeetestCaptcha === 'function') {
|
|||
|
|
initGeetestCaptcha();
|
|||
|
|
} else if (typeof loadGeetestScript === 'function' && typeof initGeetestCaptchaCore === 'function') {
|
|||
|
|
loadGeetestScript(function() {
|
|||
|
|
initGeetestCaptchaCore();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('Geetest init on PJAX failed:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 滚动位置管理优化
|
|||
|
|
|
|||
|
|
**优化后的滚动位置处理:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 在 pjax:click 事件中设置滚动位置
|
|||
|
|
$(document).on('pjax:click', function(e, options, g) {
|
|||
|
|
pjaxLoading = true;
|
|||
|
|
pjaxScrollTop = 0;
|
|||
|
|
|
|||
|
|
// 根据链接类型决定滚动位置
|
|||
|
|
if ($('html').hasClass('banner-as-cover')) {
|
|||
|
|
if (g.is('.page-link')) {
|
|||
|
|
// 分页链接:滚动到内容区域
|
|||
|
|
pjaxScrollTop = $('#content').offset().top - 80;
|
|||
|
|
}
|
|||
|
|
// 其他链接:滚动到顶部(pjaxScrollTop = 0)
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 数据模型
|
|||
|
|
|
|||
|
|
### 全局状态变量
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// LazyLoad 相关
|
|||
|
|
var lazyloadObserver = null; // IntersectionObserver 实例
|
|||
|
|
|
|||
|
|
// Zoomify 相关
|
|||
|
|
var zoomifyInstances = []; // Zoomify 实例数组
|
|||
|
|
|
|||
|
|
// PJAX 相关
|
|||
|
|
var pjaxLoading = false; // PJAX 加载状态
|
|||
|
|
var pjaxScrollTop = 0; // PJAX 后的滚动位置
|
|||
|
|
|
|||
|
|
// GT4 验证码相关
|
|||
|
|
window.geetestVerified = false;
|
|||
|
|
window.geetestAutoSubmitting = false;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 配置对象
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
argonConfig = {
|
|||
|
|
lazyload: true, // 是否启用懒加载
|
|||
|
|
lazyload_effect: 'fadeIn', // 加载效果:fadeIn | slideDown
|
|||
|
|
lazyload_threshold: 800, // 提前加载阈值(px)
|
|||
|
|
headroom: 'true', // Headroom 模式
|
|||
|
|
toolbar_blur: false, // 顶栏模糊效果
|
|||
|
|
waterflow_columns: '2and3', // 瀑布流列数
|
|||
|
|
// ... 其他配置
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 错误处理
|
|||
|
|
|
|||
|
|
### 错误处理策略
|
|||
|
|
|
|||
|
|
1. **资源清理错误**:使用 try-catch 包裹,记录警告但继续执行
|
|||
|
|
2. **初始化错误**:捕获异常,输出错误信息,不阻塞其他功能
|
|||
|
|
3. **第三方库错误**:检查库是否存在,使用可选链操作符
|
|||
|
|
4. **降级方案**:不支持的功能提供降级实现
|
|||
|
|
|
|||
|
|
### 错误处理示例
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 资源清理错误处理
|
|||
|
|
function cleanupPjaxResources() {
|
|||
|
|
try {
|
|||
|
|
if (lazyloadObserver) {
|
|||
|
|
lazyloadObserver.disconnect();
|
|||
|
|
lazyloadObserver = null;
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('Failed to cleanup LazyLoad Observer:', e);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 清理 Zoomify 实例
|
|||
|
|
if (zoomifyInstances && zoomifyInstances.length > 0) {
|
|||
|
|
zoomifyInstances.forEach(function(instance) {
|
|||
|
|
if (instance && typeof instance.destroy === 'function') {
|
|||
|
|
instance.destroy();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
zoomifyInstances = [];
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('Failed to cleanup Zoomify instances:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化错误处理
|
|||
|
|
function initializePageResources() {
|
|||
|
|
try {
|
|||
|
|
waterflowInit();
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('waterflowInit failed:', e);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
lazyloadInit();
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('lazyloadInit failed:', e);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ... 其他初始化
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 测试策略
|
|||
|
|
|
|||
|
|
### 单元测试
|
|||
|
|
|
|||
|
|
**测试范围:**
|
|||
|
|
- 资源清理函数(`cleanupPjaxResources`)
|
|||
|
|
- LazyLoad 初始化函数(`lazyloadInit`)
|
|||
|
|
- 图片加载函数(`loadImageOptimized`)
|
|||
|
|
- 滚动位置计算逻辑
|
|||
|
|
- GT4 验证码重置逻辑
|
|||
|
|
|
|||
|
|
**测试工具:**
|
|||
|
|
- Jest 或 Mocha(JavaScript 单元测试框架)
|
|||
|
|
- jsdom(模拟 DOM 环境)
|
|||
|
|
- Sinon.js(模拟和监视函数调用)
|
|||
|
|
|
|||
|
|
**测试示例:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
describe('cleanupPjaxResources', function() {
|
|||
|
|
it('should disconnect lazyloadObserver if it exists', function() {
|
|||
|
|
// 创建模拟 Observer
|
|||
|
|
lazyloadObserver = {
|
|||
|
|
disconnect: jest.fn()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
cleanupPjaxResources();
|
|||
|
|
|
|||
|
|
expect(lazyloadObserver.disconnect).toHaveBeenCalled();
|
|||
|
|
expect(lazyloadObserver).toBeNull();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should destroy all Zoomify instances', function() {
|
|||
|
|
// 创建模拟实例
|
|||
|
|
const mockInstance = {
|
|||
|
|
destroy: jest.fn()
|
|||
|
|
};
|
|||
|
|
zoomifyInstances = [mockInstance];
|
|||
|
|
|
|||
|
|
cleanupPjaxResources();
|
|||
|
|
|
|||
|
|
expect(mockInstance.destroy).toHaveBeenCalled();
|
|||
|
|
expect(zoomifyInstances).toHaveLength(0);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('lazyloadInit', function() {
|
|||
|
|
it('should create new Observer if IntersectionObserver is supported', function() {
|
|||
|
|
// 模拟 IntersectionObserver
|
|||
|
|
global.IntersectionObserver = jest.fn(function(callback, options) {
|
|||
|
|
this.observe = jest.fn();
|
|||
|
|
this.disconnect = jest.fn();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.body.innerHTML = '<img class="lazyload" data-src="test.jpg">';
|
|||
|
|
|
|||
|
|
lazyloadInit();
|
|||
|
|
|
|||
|
|
expect(lazyloadObserver).not.toBeNull();
|
|||
|
|
expect(lazyloadObserver.observe).toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should use fallback if IntersectionObserver is not supported', function() {
|
|||
|
|
global.IntersectionObserver = undefined;
|
|||
|
|
|
|||
|
|
document.body.innerHTML = '<img class="lazyload" data-src="test.jpg">';
|
|||
|
|
|
|||
|
|
lazyloadInit();
|
|||
|
|
|
|||
|
|
// 验证降级方案被调用
|
|||
|
|
// ...
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 属性测试(Property-Based Testing)
|
|||
|
|
|
|||
|
|
属性测试用于验证通用的正确性属性,确保在各种输入下系统行为符合预期。
|
|||
|
|
|
|||
|
|
**测试库:**
|
|||
|
|
- fast-check(JavaScript 属性测试库)
|
|||
|
|
|
|||
|
|
**测试配置:**
|
|||
|
|
- 每个属性测试运行 100 次迭代
|
|||
|
|
- 使用随机生成的输入数据
|
|||
|
|
- 每个测试标注对应的设计属性编号
|
|||
|
|
|
|||
|
|
|
|||
|
|
## 正确性属性
|
|||
|
|
|
|||
|
|
*属性是一个特征或行为,应该在系统的所有有效执行中保持为真——本质上是关于系统应该做什么的形式化陈述。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*
|
|||
|
|
|
|||
|
|
### 属性 1: PJAX 初始化函数单次调用
|
|||
|
|
|
|||
|
|
*对于任何* PJAX 页面切换过程,关键初始化函数(`waterflowInit`、`lazyloadInit`、`zoomifyInit`)在整个生命周期中应该只被调用一次。
|
|||
|
|
|
|||
|
|
**验证:需求 1.1, 1.2, 1.3**
|
|||
|
|
|
|||
|
|
### 属性 2: LazyLoad Observer 清理完整性
|
|||
|
|
|
|||
|
|
*对于任何* `lazyloadInit()` 调用,如果全局变量 `lazyloadObserver` 已存在,则必须先调用其 `disconnect()` 方法并清空引用,然后再创建新实例。
|
|||
|
|
|
|||
|
|
**验证:需求 2.1, 2.2**
|
|||
|
|
|
|||
|
|
### 属性 3: PJAX beforeReplace 资源清理
|
|||
|
|
|
|||
|
|
*对于任何* PJAX `beforeReplace` 事件触发,系统应该清理 `lazyloadObserver`、`zoomifyInstances` 和 Tippy 实例,并将相关全局变量重置为初始状态。
|
|||
|
|
|
|||
|
|
**验证:需求 2.3, 6.1-6.5, 7.1-7.5**
|
|||
|
|
|
|||
|
|
### 属性 4: IntersectionObserver 降级方案
|
|||
|
|
|
|||
|
|
*对于任何* 不支持 IntersectionObserver 的浏览器环境,`lazyloadInit()` 应该使用滚动监听降级方案,而不是抛出错误或不加载图片。
|
|||
|
|
|
|||
|
|
**验证:需求 2.4**
|
|||
|
|
|
|||
|
|
### 属性 5: 图片加载效果使用 requestAnimationFrame
|
|||
|
|
|
|||
|
|
*对于任何* 图片加载完成事件,应用加载效果(fadeIn 或 slideDown)时应该使用 `requestAnimationFrame` 而非 `setTimeout` 来同步浏览器渲染。
|
|||
|
|
|
|||
|
|
**验证:需求 3.1**
|
|||
|
|
|
|||
|
|
### 属性 6: 图片加载效果 CSS 属性正确性
|
|||
|
|
|
|||
|
|
*对于任何* 图片加载效果,应该正确设置 CSS 属性:fadeIn 效果设置 `opacity` 和 `transition`,slideDown 效果设置 `opacity`、`transform` 和 `transition`。
|
|||
|
|
|
|||
|
|
**验证:需求 3.2, 3.3**
|
|||
|
|
|
|||
|
|
### 属性 7: 图片加载失败处理
|
|||
|
|
|
|||
|
|
*对于任何* 图片加载失败事件,系统应该设置原始 src 并停止重试,不应该进入无限重试循环。
|
|||
|
|
|
|||
|
|
**验证:需求 3.4, 10.4**
|
|||
|
|
|
|||
|
|
### 属性 8: LazyLoad 类名清理
|
|||
|
|
|
|||
|
|
*对于任何* 图片加载完成或失败,系统应该移除 `lazyload` 类和所有 `lazyload-style-*` 类名。
|
|||
|
|
|
|||
|
|
**验证:需求 3.5**
|
|||
|
|
|
|||
|
|
### 属性 9: GT4 验证码状态重置完整性
|
|||
|
|
|
|||
|
|
*对于任何* PJAX `end` 事件且页面包含 `#geetest-captcha` 元素,系统应该重置所有验证码状态变量、清空隐藏字段、清空容器 DOM,并调用初始化函数。
|
|||
|
|
|
|||
|
|
**验证:需求 4.1, 4.2, 4.3, 4.4**
|
|||
|
|
|
|||
|
|
### 属性 10: GT4 验证码初始化错误处理
|
|||
|
|
|
|||
|
|
*对于任何* GT4 验证码初始化失败,系统应该记录警告但不抛出异常,不影响页面其他功能的正常运行。
|
|||
|
|
|
|||
|
|
**验证:需求 4.5**
|
|||
|
|
|
|||
|
|
### 属性 11: 滚动位置恢复正确性
|
|||
|
|
|
|||
|
|
*对于任何* PJAX 页面切换,如果 `pjaxScrollTop` 大于 0,则在 `pjax:complete` 事件中应该恢复到该滚动位置,并在恢复后将 `pjaxScrollTop` 重置为 0。
|
|||
|
|
|
|||
|
|
**验证:需求 5.3, 5.4**
|
|||
|
|
|
|||
|
|
### 属性 12: 分页链接滚动位置计算
|
|||
|
|
|
|||
|
|
*对于任何* 分页链接点击且 Banner 设置为封面模式,系统应该将 `pjaxScrollTop` 设置为内容区域顶部位置减去 80px。
|
|||
|
|
|
|||
|
|
**验证:需求 5.2, 5.5**
|
|||
|
|
|
|||
|
|
### 属性 13: 移动端目录重置调用
|
|||
|
|
|
|||
|
|
*对于任何* PJAX `end` 事件,如果 `window.resetMobileCatalog` 函数存在,系统应该调用该函数(不验证函数内部逻辑)。
|
|||
|
|
|
|||
|
|
**验证:需求 8.1, 8.2**
|
|||
|
|
|
|||
|
|
### 属性 14: 事件监听器清理
|
|||
|
|
|
|||
|
|
*对于任何* PJAX 资源清理过程,系统应该移除所有动态绑定的事件监听器,防止内存泄漏和重复绑定。
|
|||
|
|
|
|||
|
|
**验证:需求 9.2, 9.5**
|
|||
|
|
|
|||
|
|
### 属性 15: 事件监听器防重复绑定
|
|||
|
|
|
|||
|
|
*对于任何* 功能重新初始化,系统应该检查事件监听器是否已存在,避免重复绑定相同的监听器。
|
|||
|
|
|
|||
|
|
**验证:需求 9.3**
|
|||
|
|
|
|||
|
|
## 测试策略(续)
|
|||
|
|
|
|||
|
|
### 属性测试实现
|
|||
|
|
|
|||
|
|
**测试框架:** Jest + fast-check
|
|||
|
|
|
|||
|
|
**测试配置:**
|
|||
|
|
```javascript
|
|||
|
|
// jest.config.js
|
|||
|
|
module.exports = {
|
|||
|
|
testEnvironment: 'jsdom',
|
|||
|
|
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
|
|||
|
|
collectCoverageFrom: [
|
|||
|
|
'argontheme.js',
|
|||
|
|
'!**/node_modules/**',
|
|||
|
|
'!**/vendor/**'
|
|||
|
|
],
|
|||
|
|
coverageThreshold: {
|
|||
|
|
global: {
|
|||
|
|
branches: 70,
|
|||
|
|
functions: 80,
|
|||
|
|
lines: 80,
|
|||
|
|
statements: 80
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**属性测试示例:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const fc = require('fast-check');
|
|||
|
|
|
|||
|
|
describe('Property Tests - PJAX LazyLoad Optimization', function() {
|
|||
|
|
/**
|
|||
|
|
* Feature: pjax-lazyload-optimization, Property 1: PJAX 初始化函数单次调用
|
|||
|
|
*/
|
|||
|
|
it('should call initialization functions only once during PJAX lifecycle', function() {
|
|||
|
|
// 创建 spy
|
|||
|
|
const waterflowSpy = jest.spyOn(window, 'waterflowInit');
|
|||
|
|
const lazyloadSpy = jest.spyOn(window, 'lazyloadInit');
|
|||
|
|
const zoomifySpy = jest.spyOn(window, 'zoomifyInit');
|
|||
|
|
|
|||
|
|
// 模拟 PJAX 完整生命周期
|
|||
|
|
$(document).trigger('pjax:beforeReplace', [[]]);
|
|||
|
|
$(document).trigger('pjax:complete');
|
|||
|
|
$(document).trigger('pjax:end');
|
|||
|
|
|
|||
|
|
// 验证每个函数只被调用一次
|
|||
|
|
expect(waterflowSpy).toHaveBeenCalledTimes(1);
|
|||
|
|
expect(lazyloadSpy).toHaveBeenCalledTimes(1);
|
|||
|
|
expect(zoomifySpy).toHaveBeenCalledTimes(1);
|
|||
|
|
|
|||
|
|
// 清理
|
|||
|
|
waterflowSpy.mockRestore();
|
|||
|
|
lazyloadSpy.mockRestore();
|
|||
|
|
zoomifySpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Feature: pjax-lazyload-optimization, Property 2: LazyLoad Observer 清理完整性
|
|||
|
|
*/
|
|||
|
|
it('should cleanup existing Observer before creating new one', function() {
|
|||
|
|
// 创建模拟 Observer
|
|||
|
|
const mockObserver = {
|
|||
|
|
disconnect: jest.fn(),
|
|||
|
|
observe: jest.fn()
|
|||
|
|
};
|
|||
|
|
window.lazyloadObserver = mockObserver;
|
|||
|
|
|
|||
|
|
// 调用初始化
|
|||
|
|
lazyloadInit();
|
|||
|
|
|
|||
|
|
// 验证旧 Observer 被清理
|
|||
|
|
expect(mockObserver.disconnect).toHaveBeenCalled();
|
|||
|
|
expect(window.lazyloadObserver).not.toBe(mockObserver);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Feature: pjax-lazyload-optimization, Property 3: PJAX beforeReplace 资源清理
|
|||
|
|
*/
|
|||
|
|
it('should cleanup all resources on pjax:beforeReplace', function() {
|
|||
|
|
// 设置资源
|
|||
|
|
window.lazyloadObserver = {
|
|||
|
|
disconnect: jest.fn()
|
|||
|
|
};
|
|||
|
|
window.zoomifyInstances = [{
|
|||
|
|
destroy: jest.fn()
|
|||
|
|
}];
|
|||
|
|
|
|||
|
|
// 触发事件
|
|||
|
|
$(document).trigger('pjax:beforeReplace', [[]]);
|
|||
|
|
|
|||
|
|
// 验证清理
|
|||
|
|
expect(window.lazyloadObserver).toBeNull();
|
|||
|
|
expect(window.zoomifyInstances).toHaveLength(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Feature: pjax-lazyload-optimization, Property 5: 图片加载效果使用 requestAnimationFrame
|
|||
|
|
*/
|
|||
|
|
it('should use requestAnimationFrame for load effects', function() {
|
|||
|
|
const rafSpy = jest.spyOn(window, 'requestAnimationFrame');
|
|||
|
|
|
|||
|
|
// 创建测试图片
|
|||
|
|
document.body.innerHTML = '<img class="lazyload" data-src="test.jpg">';
|
|||
|
|
const img = document.querySelector('img');
|
|||
|
|
|
|||
|
|
// 调用加载函数
|
|||
|
|
loadImageOptimized(img, 'fadeIn');
|
|||
|
|
|
|||
|
|
// 模拟图片加载完成
|
|||
|
|
const tempImg = new Image();
|
|||
|
|
tempImg.onload();
|
|||
|
|
|
|||
|
|
// 验证 requestAnimationFrame 被调用
|
|||
|
|
expect(rafSpy).toHaveBeenCalled();
|
|||
|
|
|
|||
|
|
rafSpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Feature: pjax-lazyload-optimization, Property 7: 图片加载失败处理
|
|||
|
|
*/
|
|||
|
|
it('should handle image load failure without retry', function() {
|
|||
|
|
document.body.innerHTML = '<img class="lazyload" data-src="invalid.jpg">';
|
|||
|
|
const img = document.querySelector('img');
|
|||
|
|
|
|||
|
|
// 调用加载函数
|
|||
|
|
loadImageOptimized(img, 'fadeIn');
|
|||
|
|
|
|||
|
|
// 模拟加载失败
|
|||
|
|
const tempImg = new Image();
|
|||
|
|
tempImg.onerror();
|
|||
|
|
|
|||
|
|
// 验证图片 src 被设置且不重试
|
|||
|
|
expect(img.src).toContain('invalid.jpg');
|
|||
|
|
expect(img.hasAttribute('data-src')).toBe(false);
|
|||
|
|
expect(img.classList.contains('lazyload')).toBe(false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Feature: pjax-lazyload-optimization, Property 11: 滚动位置恢复正确性
|
|||
|
|
*/
|
|||
|
|
it('should restore scroll position and reset pjaxScrollTop', function() {
|
|||
|
|
// 设置滚动位置
|
|||
|
|
window.pjaxScrollTop = 500;
|
|||
|
|
|
|||
|
|
// 模拟 jQuery scrollTop 方法
|
|||
|
|
const scrollTopSpy = jest.spyOn($.fn, 'scrollTop');
|
|||
|
|
|
|||
|
|
// 触发 pjax:complete
|
|||
|
|
$(document).trigger('pjax:complete');
|
|||
|
|
|
|||
|
|
// 验证滚动位置恢复
|
|||
|
|
expect(scrollTopSpy).toHaveBeenCalledWith(500);
|
|||
|
|
expect(window.pjaxScrollTop).toBe(0);
|
|||
|
|
|
|||
|
|
scrollTopSpy.mockRestore();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 集成测试
|
|||
|
|
|
|||
|
|
**测试场景:**
|
|||
|
|
1. 完整的 PJAX 页面切换流程
|
|||
|
|
2. 多次连续 PJAX 切换
|
|||
|
|
3. 快速点击多个链接
|
|||
|
|
4. 浏览器前进/后退按钮
|
|||
|
|
5. 移动端和桌面端切换
|
|||
|
|
|
|||
|
|
**测试工具:**
|
|||
|
|
- Puppeteer 或 Playwright(浏览器自动化)
|
|||
|
|
- 真实浏览器环境测试
|
|||
|
|
|
|||
|
|
**集成测试示例:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
describe('Integration Tests - PJAX LazyLoad', function() {
|
|||
|
|
let page;
|
|||
|
|
|
|||
|
|
beforeAll(async function() {
|
|||
|
|
page = await browser.newPage();
|
|||
|
|
await page.goto('http://localhost:8080');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should handle multiple PJAX navigations without memory leak', async function() {
|
|||
|
|
// 记录初始内存
|
|||
|
|
const initialMemory = await page.metrics();
|
|||
|
|
|
|||
|
|
// 执行 10 次 PJAX 导航
|
|||
|
|
for (let i = 0; i < 10; i++) {
|
|||
|
|
await page.click('.pjax-link');
|
|||
|
|
await page.waitForSelector('#content');
|
|||
|
|
await page.goBack();
|
|||
|
|
await page.waitForSelector('#content');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查内存增长
|
|||
|
|
const finalMemory = await page.metrics();
|
|||
|
|
const memoryGrowth = finalMemory.JSHeapUsedSize - initialMemory.JSHeapUsedSize;
|
|||
|
|
|
|||
|
|
// 内存增长应该在合理范围内(< 10MB)
|
|||
|
|
expect(memoryGrowth).toBeLessThan(10 * 1024 * 1024);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should load images lazily after PJAX navigation', async function() {
|
|||
|
|
await page.click('.pjax-link');
|
|||
|
|
await page.waitForSelector('#content');
|
|||
|
|
|
|||
|
|
// 检查懒加载图片
|
|||
|
|
const lazyImages = await page.$$('img.lazyload[data-src]');
|
|||
|
|
expect(lazyImages.length).toBeGreaterThan(0);
|
|||
|
|
|
|||
|
|
// 滚动到图片位置
|
|||
|
|
await page.evaluate(function() {
|
|||
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 等待图片加载
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 验证图片已加载
|
|||
|
|
const loadedImages = await page.$$('img:not(.lazyload)');
|
|||
|
|
expect(loadedImages.length).toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 性能测试
|
|||
|
|
|
|||
|
|
**测试指标:**
|
|||
|
|
- PJAX 页面切换时间
|
|||
|
|
- LazyLoad 初始化时间
|
|||
|
|
- 内存使用情况
|
|||
|
|
- 事件监听器数量
|
|||
|
|
|
|||
|
|
**性能测试示例:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
describe('Performance Tests', function() {
|
|||
|
|
it('should complete PJAX navigation within 500ms', async function() {
|
|||
|
|
const startTime = performance.now();
|
|||
|
|
|
|||
|
|
await page.click('.pjax-link');
|
|||
|
|
await page.waitForSelector('#content');
|
|||
|
|
|
|||
|
|
const endTime = performance.now();
|
|||
|
|
const duration = endTime - startTime;
|
|||
|
|
|
|||
|
|
expect(duration).toBeLessThan(500);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should initialize LazyLoad within 100ms', function() {
|
|||
|
|
const startTime = performance.now();
|
|||
|
|
|
|||
|
|
lazyloadInit();
|
|||
|
|
|
|||
|
|
const endTime = performance.now();
|
|||
|
|
const duration = endTime - startTime;
|
|||
|
|
|
|||
|
|
expect(duration).toBeLessThan(100);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 测试覆盖率目标
|
|||
|
|
|
|||
|
|
- **行覆盖率**:≥ 80%
|
|||
|
|
- **分支覆盖率**:≥ 70%
|
|||
|
|
- **函数覆盖率**:≥ 80%
|
|||
|
|
- **语句覆盖率**:≥ 80%
|
|||
|
|
|
|||
|
|
### 持续集成
|
|||
|
|
|
|||
|
|
**CI 配置(GitHub Actions):**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
name: Test PJAX LazyLoad Optimization
|
|||
|
|
|
|||
|
|
on: [push, pull_request]
|
|||
|
|
|
|||
|
|
jobs:
|
|||
|
|
test:
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
steps:
|
|||
|
|
- uses: actions/checkout@v2
|
|||
|
|
|
|||
|
|
- name: Setup Node.js
|
|||
|
|
uses: actions/setup-node@v2
|
|||
|
|
with:
|
|||
|
|
node-version: '16'
|
|||
|
|
|
|||
|
|
- name: Install dependencies
|
|||
|
|
run: npm install
|
|||
|
|
|
|||
|
|
- name: Run unit tests
|
|||
|
|
run: npm test
|
|||
|
|
|
|||
|
|
- name: Run property tests
|
|||
|
|
run: npm run test:property
|
|||
|
|
|
|||
|
|
- name: Run integration tests
|
|||
|
|
run: npm run test:integration
|
|||
|
|
|
|||
|
|
- name: Upload coverage
|
|||
|
|
uses: codecov/codecov-action@v2
|
|||
|
|
with:
|
|||
|
|
files: ./coverage/lcov.info
|
|||
|
|
```
|