customer-chat-sdk 1.0.28 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ScreenshotManager.d.ts +72 -3
- package/dist/core/ScreenshotManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +501 -62
- package/dist/customer-sdk.esm.js +501 -62
- package/dist/customer-sdk.min.js +1 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/customer-sdk.esm.js
CHANGED
|
@@ -922,28 +922,6 @@ class IframeManager {
|
|
|
922
922
|
}
|
|
923
923
|
|
|
924
924
|
// @ts-ignore - modern-screenshot may not have type definitions
|
|
925
|
-
// Worker URL 将在创建上下文时动态获取
|
|
926
|
-
// 在 Vite 环境中可以使用: import workerUrl from 'modern-screenshot/worker?url'
|
|
927
|
-
// 在 Rollup 中,modern-screenshot 会自动处理 worker URL
|
|
928
|
-
let workerUrl = undefined;
|
|
929
|
-
// 尝试动态获取 worker URL(仅在支持的环境中)
|
|
930
|
-
async function getWorkerUrl() {
|
|
931
|
-
if (workerUrl) {
|
|
932
|
-
return workerUrl;
|
|
933
|
-
}
|
|
934
|
-
try {
|
|
935
|
-
// 尝试使用 Vite 的 ?url 语法(仅在 Vite 环境中有效)
|
|
936
|
-
// @ts-ignore - Vite 特有的 ?url 语法
|
|
937
|
-
const workerModule = await import('modern-screenshot/worker?url');
|
|
938
|
-
workerUrl = workerModule.default || workerModule;
|
|
939
|
-
return workerUrl;
|
|
940
|
-
}
|
|
941
|
-
catch {
|
|
942
|
-
// Rollup 或其他构建工具不支持 ?url 语法
|
|
943
|
-
// modern-screenshot 会自动处理 worker URL,返回 undefined 即可
|
|
944
|
-
return undefined;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
925
|
/**
|
|
948
926
|
* 截图管理器
|
|
949
927
|
* 负责页面截图、压缩和上传功能
|
|
@@ -973,8 +951,16 @@ class ScreenshotManager {
|
|
|
973
951
|
this.dynamicInterval = null;
|
|
974
952
|
// 过期定时器
|
|
975
953
|
this.expirationTimer = null;
|
|
976
|
-
//
|
|
954
|
+
// 图片代理缓存(带过期时间)
|
|
977
955
|
this.imageProxyCache = new Map();
|
|
956
|
+
// IndexedDB 缓存(持久化)
|
|
957
|
+
this.indexedDBCache = null;
|
|
958
|
+
this.indexedDBReady = false;
|
|
959
|
+
// Intersection Observer(用于高效检测可见性)
|
|
960
|
+
this.intersectionObserver = null;
|
|
961
|
+
this.visibleElementsCache = new Set();
|
|
962
|
+
// 预连接状态
|
|
963
|
+
this.preconnected = false;
|
|
978
964
|
// 全局错误处理器
|
|
979
965
|
this.globalErrorHandler = null;
|
|
980
966
|
this.globalRejectionHandler = null;
|
|
@@ -996,10 +982,37 @@ class ScreenshotManager {
|
|
|
996
982
|
maxRetries: options.maxRetries ?? 2,
|
|
997
983
|
preloadImages: options.preloadImages ?? false, // 默认不预加载,按需加载
|
|
998
984
|
maxConcurrentDownloads: options.maxConcurrentDownloads ?? 10, // 增加并发数
|
|
999
|
-
onlyVisibleImages: options.onlyVisibleImages ?? true // 默认只处理可视区域
|
|
985
|
+
onlyVisibleImages: options.onlyVisibleImages ?? true, // 默认只处理可视区域
|
|
986
|
+
imageCacheTTL: options.imageCacheTTL ?? 600000, // 默认10分钟(600000ms)
|
|
987
|
+
useIndexedDB: options.useIndexedDB ?? true, // 默认启用 IndexedDB 持久化缓存
|
|
988
|
+
usePreconnect: options.usePreconnect ?? true, // 默认预连接代理服务器
|
|
989
|
+
imageLoadTimeout: options.imageLoadTimeout ?? 5000, // 默认5秒超时
|
|
990
|
+
useIntersectionObserver: options.useIntersectionObserver ?? true, // 默认使用 Intersection Observer
|
|
991
|
+
fetchPriority: options.fetchPriority ?? 'high', // 默认高优先级
|
|
992
|
+
maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
|
|
993
|
+
maxCacheAge: options.maxCacheAge ?? 86400000 // 默认24小时(86400000ms)
|
|
1000
994
|
};
|
|
1001
995
|
this.setupMessageListener();
|
|
1002
996
|
this.setupVisibilityChangeListener();
|
|
997
|
+
// 启动缓存清理定时器
|
|
998
|
+
this.startCacheCleanup();
|
|
999
|
+
// 预连接代理服务器(如果启用)
|
|
1000
|
+
if (this.options.usePreconnect && this.options.proxyUrl) {
|
|
1001
|
+
this.preconnectProxy();
|
|
1002
|
+
}
|
|
1003
|
+
// 初始化 Intersection Observer(如果启用)
|
|
1004
|
+
if (this.options.useIntersectionObserver && this.options.onlyVisibleImages) {
|
|
1005
|
+
this.initIntersectionObserver();
|
|
1006
|
+
}
|
|
1007
|
+
// 初始化 IndexedDB(如果启用)
|
|
1008
|
+
if (this.options.useIndexedDB) {
|
|
1009
|
+
this.initIndexedDB().catch(() => {
|
|
1010
|
+
// IndexedDB 初始化失败,回退到内存缓存
|
|
1011
|
+
if (!this.options.silentMode) {
|
|
1012
|
+
console.warn('📸 IndexedDB 初始化失败,使用内存缓存');
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1003
1016
|
}
|
|
1004
1017
|
/**
|
|
1005
1018
|
* 设置目标元素
|
|
@@ -1310,9 +1323,23 @@ class ScreenshotManager {
|
|
|
1310
1323
|
}
|
|
1311
1324
|
// 优化:如果启用预加载,才预处理图片;否则让 modern-screenshot 按需加载
|
|
1312
1325
|
if (this.options.preloadImages && this.options.enableCORS) {
|
|
1326
|
+
// 清理过期缓存
|
|
1327
|
+
this.cleanExpiredCache();
|
|
1328
|
+
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
1329
|
+
if (this.options.useIndexedDB) {
|
|
1330
|
+
await this.cleanIndexedDBCache();
|
|
1331
|
+
}
|
|
1313
1332
|
await this.preprocessNetworkImages(this.targetElement);
|
|
1314
1333
|
await this.waitForImagesToLoad(this.targetElement);
|
|
1315
1334
|
}
|
|
1335
|
+
else {
|
|
1336
|
+
// 即使不预加载,也清理一下过期缓存
|
|
1337
|
+
this.cleanExpiredCache();
|
|
1338
|
+
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
1339
|
+
if (this.options.useIndexedDB) {
|
|
1340
|
+
await this.cleanIndexedDBCache();
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1316
1343
|
let dataUrl;
|
|
1317
1344
|
// 等待一小段时间,确保 DOM 更新完成(减少等待时间)
|
|
1318
1345
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
@@ -1412,9 +1439,25 @@ class ScreenshotManager {
|
|
|
1412
1439
|
}
|
|
1413
1440
|
// 如果配置了代理服务器,使用代理处理跨域图片
|
|
1414
1441
|
if (this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
|
|
1415
|
-
//
|
|
1416
|
-
|
|
1417
|
-
|
|
1442
|
+
// 检查内存缓存(优先使用缓存,带过期时间检查)
|
|
1443
|
+
const cachedDataUrl = this.getCachedImage(url);
|
|
1444
|
+
if (cachedDataUrl) {
|
|
1445
|
+
if (!this.options.silentMode) {
|
|
1446
|
+
console.log(`📸 ✅ 使用内存缓存图片: ${url.substring(0, 50)}...`);
|
|
1447
|
+
}
|
|
1448
|
+
return cachedDataUrl;
|
|
1449
|
+
}
|
|
1450
|
+
// 检查 IndexedDB 缓存(如果启用)
|
|
1451
|
+
if (this.options.useIndexedDB) {
|
|
1452
|
+
const indexedDBCache = await this.getIndexedDBCache(url);
|
|
1453
|
+
if (indexedDBCache) {
|
|
1454
|
+
// 同步到内存缓存
|
|
1455
|
+
this.setCachedImage(url, indexedDBCache);
|
|
1456
|
+
if (!this.options.silentMode) {
|
|
1457
|
+
console.log(`📸 ✅ 使用 IndexedDB 缓存图片: ${url.substring(0, 50)}...`);
|
|
1458
|
+
}
|
|
1459
|
+
return indexedDBCache;
|
|
1460
|
+
}
|
|
1418
1461
|
}
|
|
1419
1462
|
try {
|
|
1420
1463
|
// 构建代理请求参数
|
|
@@ -1428,11 +1471,11 @@ class ScreenshotManager {
|
|
|
1428
1471
|
let baseUrl = this.options.proxyUrl;
|
|
1429
1472
|
baseUrl = baseUrl.replace(/[?&]$/, '');
|
|
1430
1473
|
const proxyUrl = `${baseUrl}?${params.toString()}`;
|
|
1431
|
-
//
|
|
1474
|
+
// 请求代理服务器(优化:添加超时控制和优先级)
|
|
1432
1475
|
const controller = new AbortController();
|
|
1433
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
1476
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
|
|
1434
1477
|
try {
|
|
1435
|
-
const
|
|
1478
|
+
const fetchOptions = {
|
|
1436
1479
|
method: 'GET',
|
|
1437
1480
|
mode: 'cors',
|
|
1438
1481
|
credentials: 'omit',
|
|
@@ -1441,15 +1484,24 @@ class ScreenshotManager {
|
|
|
1441
1484
|
},
|
|
1442
1485
|
cache: 'no-cache',
|
|
1443
1486
|
signal: controller.signal
|
|
1444
|
-
}
|
|
1487
|
+
};
|
|
1488
|
+
// 添加 fetch priority(如果支持)
|
|
1489
|
+
if ('priority' in fetchOptions) {
|
|
1490
|
+
fetchOptions.priority = this.options.fetchPriority;
|
|
1491
|
+
}
|
|
1492
|
+
const response = await fetch(proxyUrl, fetchOptions);
|
|
1445
1493
|
clearTimeout(timeoutId);
|
|
1446
1494
|
if (!response.ok) {
|
|
1447
1495
|
throw new Error(`代理请求失败: ${response.status}`);
|
|
1448
1496
|
}
|
|
1449
1497
|
const blob = await response.blob();
|
|
1450
1498
|
const dataUrl = await this.blobToDataUrl(blob);
|
|
1451
|
-
//
|
|
1452
|
-
this.
|
|
1499
|
+
// 缓存结果(带时间戳,10分钟有效)
|
|
1500
|
+
this.setCachedImage(url, dataUrl);
|
|
1501
|
+
// 如果启用 IndexedDB,也保存到 IndexedDB
|
|
1502
|
+
if (this.options.useIndexedDB) {
|
|
1503
|
+
await this.setIndexedDBCache(url, dataUrl);
|
|
1504
|
+
}
|
|
1453
1505
|
return dataUrl;
|
|
1454
1506
|
}
|
|
1455
1507
|
catch (fetchError) {
|
|
@@ -1496,11 +1548,7 @@ class ScreenshotManager {
|
|
|
1496
1548
|
if (this.options.scale !== 1) {
|
|
1497
1549
|
contextOptions.scale = isMobile ? 0.8 : this.options.scale;
|
|
1498
1550
|
}
|
|
1499
|
-
//
|
|
1500
|
-
const resolvedWorkerUrl = await getWorkerUrl();
|
|
1501
|
-
if (resolvedWorkerUrl) {
|
|
1502
|
-
contextOptions.workerUrl = resolvedWorkerUrl;
|
|
1503
|
-
}
|
|
1551
|
+
// modern-screenshot 会自动处理 worker URL,不需要手动设置
|
|
1504
1552
|
// 创建 Worker 上下文
|
|
1505
1553
|
this.screenshotContext = await createContext(element, contextOptions);
|
|
1506
1554
|
}
|
|
@@ -1564,23 +1612,336 @@ class ScreenshotManager {
|
|
|
1564
1612
|
}
|
|
1565
1613
|
}
|
|
1566
1614
|
/**
|
|
1567
|
-
*
|
|
1615
|
+
* 预连接代理服务器(优化网络性能)
|
|
1616
|
+
*/
|
|
1617
|
+
preconnectProxy() {
|
|
1618
|
+
if (this.preconnected || !this.options.proxyUrl) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
try {
|
|
1622
|
+
const proxyOrigin = new URL(this.options.proxyUrl).origin;
|
|
1623
|
+
const link = document.createElement('link');
|
|
1624
|
+
link.rel = 'preconnect';
|
|
1625
|
+
link.href = proxyOrigin;
|
|
1626
|
+
link.crossOrigin = 'anonymous';
|
|
1627
|
+
document.head.appendChild(link);
|
|
1628
|
+
this.preconnected = true;
|
|
1629
|
+
if (!this.options.silentMode) {
|
|
1630
|
+
console.log(`📸 ✅ 已预连接代理服务器: ${proxyOrigin}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
catch (e) {
|
|
1634
|
+
// 预连接失败,不影响功能
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* 初始化 Intersection Observer(优化可见性检测)
|
|
1639
|
+
*/
|
|
1640
|
+
initIntersectionObserver() {
|
|
1641
|
+
if (!('IntersectionObserver' in window)) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
1645
|
+
entries.forEach((entry) => {
|
|
1646
|
+
if (entry.isIntersecting) {
|
|
1647
|
+
this.visibleElementsCache.add(entry.target);
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
this.visibleElementsCache.delete(entry.target);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
}, {
|
|
1654
|
+
root: null,
|
|
1655
|
+
rootMargin: '50px', // 提前50px开始加载
|
|
1656
|
+
threshold: 0.01
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* 初始化 IndexedDB(持久化缓存)
|
|
1661
|
+
*/
|
|
1662
|
+
async initIndexedDB() {
|
|
1663
|
+
return new Promise((resolve, reject) => {
|
|
1664
|
+
const request = indexedDB.open('screenshot-cache', 1);
|
|
1665
|
+
request.onerror = () => reject(request.error);
|
|
1666
|
+
request.onsuccess = () => {
|
|
1667
|
+
this.indexedDBCache = request.result;
|
|
1668
|
+
this.indexedDBReady = true;
|
|
1669
|
+
// 初始化后立即清理过期和超大小的缓存
|
|
1670
|
+
this.cleanIndexedDBCache().catch(() => {
|
|
1671
|
+
// 清理失败不影响功能
|
|
1672
|
+
});
|
|
1673
|
+
resolve();
|
|
1674
|
+
};
|
|
1675
|
+
request.onupgradeneeded = (event) => {
|
|
1676
|
+
const db = event.target.result;
|
|
1677
|
+
if (!db.objectStoreNames.contains('images')) {
|
|
1678
|
+
const store = db.createObjectStore('images', { keyPath: 'url' });
|
|
1679
|
+
// 创建索引用于按时间排序
|
|
1680
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* 从 IndexedDB 获取缓存
|
|
1687
|
+
*/
|
|
1688
|
+
async getIndexedDBCache(url) {
|
|
1689
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readonly');
|
|
1694
|
+
const store = transaction.objectStore('images');
|
|
1695
|
+
const request = store.get(url);
|
|
1696
|
+
return new Promise((resolve) => {
|
|
1697
|
+
request.onsuccess = () => {
|
|
1698
|
+
const result = request.result;
|
|
1699
|
+
if (result) {
|
|
1700
|
+
const now = Date.now();
|
|
1701
|
+
const age = now - result.timestamp;
|
|
1702
|
+
// 检查是否超过最大缓存时间
|
|
1703
|
+
if (age > this.options.maxCacheAge) {
|
|
1704
|
+
// 缓存过期,删除
|
|
1705
|
+
this.deleteIndexedDBCache(url);
|
|
1706
|
+
resolve(null);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
// 返回缓存数据(即使超过 imageCacheTTL,只要未超过 maxCacheAge 仍可使用)
|
|
1710
|
+
resolve(result.dataUrl);
|
|
1711
|
+
}
|
|
1712
|
+
else {
|
|
1713
|
+
resolve(null);
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
request.onerror = () => resolve(null);
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
catch {
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* 设置 IndexedDB 缓存(带大小和时间控制)
|
|
1725
|
+
*/
|
|
1726
|
+
async setIndexedDBCache(url, dataUrl) {
|
|
1727
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
try {
|
|
1731
|
+
// 计算当前缓存大小
|
|
1732
|
+
const currentSize = await this.getIndexedDBCacheSize();
|
|
1733
|
+
const newItemSize = this.estimateDataUrlSize(dataUrl);
|
|
1734
|
+
const maxSizeBytes = (this.options.maxCacheSize || 50) * 1024 * 1024; // 转换为字节
|
|
1735
|
+
// 如果添加新项后超过限制,清理最旧的数据
|
|
1736
|
+
if (currentSize + newItemSize > maxSizeBytes) {
|
|
1737
|
+
await this.cleanIndexedDBCacheBySize(maxSizeBytes - newItemSize);
|
|
1738
|
+
}
|
|
1739
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1740
|
+
const store = transaction.objectStore('images');
|
|
1741
|
+
store.put({ url, dataUrl, timestamp: Date.now() });
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
// IndexedDB 写入失败,忽略
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* 估算 data URL 的大小(字节)
|
|
1749
|
+
*/
|
|
1750
|
+
estimateDataUrlSize(dataUrl) {
|
|
1751
|
+
// Base64 编码后的大小约为原始大小的 4/3
|
|
1752
|
+
// data:image/xxx;base64, 前缀约 22-30 字节
|
|
1753
|
+
const base64Data = dataUrl.split(',')[1] || '';
|
|
1754
|
+
return Math.ceil(base64Data.length * 0.75) + 30;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* 获取 IndexedDB 当前缓存大小(字节)
|
|
1758
|
+
*/
|
|
1759
|
+
async getIndexedDBCacheSize() {
|
|
1760
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1761
|
+
return 0;
|
|
1762
|
+
}
|
|
1763
|
+
try {
|
|
1764
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readonly');
|
|
1765
|
+
const store = transaction.objectStore('images');
|
|
1766
|
+
const request = store.getAll();
|
|
1767
|
+
return new Promise((resolve) => {
|
|
1768
|
+
request.onsuccess = () => {
|
|
1769
|
+
const items = request.result || [];
|
|
1770
|
+
let totalSize = 0;
|
|
1771
|
+
items.forEach((item) => {
|
|
1772
|
+
totalSize += this.estimateDataUrlSize(item.dataUrl);
|
|
1773
|
+
});
|
|
1774
|
+
resolve(totalSize);
|
|
1775
|
+
};
|
|
1776
|
+
request.onerror = () => resolve(0);
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
catch {
|
|
1780
|
+
return 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* 清理 IndexedDB 缓存(按时间和大小)
|
|
1785
|
+
*/
|
|
1786
|
+
async cleanIndexedDBCache() {
|
|
1787
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
try {
|
|
1791
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1792
|
+
const store = transaction.objectStore('images');
|
|
1793
|
+
const index = store.index('timestamp');
|
|
1794
|
+
const request = index.getAll();
|
|
1795
|
+
return new Promise((resolve) => {
|
|
1796
|
+
request.onsuccess = () => {
|
|
1797
|
+
const items = request.result || [];
|
|
1798
|
+
const now = Date.now();
|
|
1799
|
+
const expiredUrls = [];
|
|
1800
|
+
let currentSize = 0;
|
|
1801
|
+
const maxSizeBytes = (this.options.maxCacheSize || 50) * 1024 * 1024;
|
|
1802
|
+
// 按时间排序(最旧的在前)
|
|
1803
|
+
items.sort((a, b) => a.timestamp - b.timestamp);
|
|
1804
|
+
// 清理过期数据
|
|
1805
|
+
items.forEach((item) => {
|
|
1806
|
+
const age = now - item.timestamp;
|
|
1807
|
+
if (age > this.options.maxCacheAge) {
|
|
1808
|
+
expiredUrls.push(item.url);
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
currentSize += this.estimateDataUrlSize(item.dataUrl);
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
// 如果仍然超过大小限制,删除最旧的数据
|
|
1815
|
+
const urlsToDelete = [...expiredUrls];
|
|
1816
|
+
if (currentSize > maxSizeBytes) {
|
|
1817
|
+
for (const item of items) {
|
|
1818
|
+
if (expiredUrls.includes(item.url))
|
|
1819
|
+
continue;
|
|
1820
|
+
currentSize -= this.estimateDataUrlSize(item.dataUrl);
|
|
1821
|
+
urlsToDelete.push(item.url);
|
|
1822
|
+
if (currentSize <= maxSizeBytes) {
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
// 删除过期和超大小的数据
|
|
1828
|
+
urlsToDelete.forEach((url) => {
|
|
1829
|
+
store.delete(url);
|
|
1830
|
+
});
|
|
1831
|
+
if (urlsToDelete.length > 0 && !this.options.silentMode) {
|
|
1832
|
+
console.log(`📸 IndexedDB 清理了 ${urlsToDelete.length} 个缓存项(过期或超大小)`);
|
|
1833
|
+
}
|
|
1834
|
+
resolve();
|
|
1835
|
+
};
|
|
1836
|
+
request.onerror = () => resolve();
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
catch {
|
|
1840
|
+
// 清理失败,忽略
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* 按大小清理 IndexedDB 缓存
|
|
1845
|
+
*/
|
|
1846
|
+
async cleanIndexedDBCacheBySize(targetSize) {
|
|
1847
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1852
|
+
const store = transaction.objectStore('images');
|
|
1853
|
+
const index = store.index('timestamp');
|
|
1854
|
+
const request = index.getAll();
|
|
1855
|
+
return new Promise((resolve) => {
|
|
1856
|
+
request.onsuccess = () => {
|
|
1857
|
+
const items = request.result || [];
|
|
1858
|
+
// 按时间排序(最旧的在前)
|
|
1859
|
+
items.sort((a, b) => a.timestamp - b.timestamp);
|
|
1860
|
+
let currentSize = 0;
|
|
1861
|
+
const urlsToDelete = [];
|
|
1862
|
+
// 计算当前大小
|
|
1863
|
+
items.forEach((item) => {
|
|
1864
|
+
currentSize += this.estimateDataUrlSize(item.dataUrl);
|
|
1865
|
+
});
|
|
1866
|
+
// 如果超过目标大小,删除最旧的数据
|
|
1867
|
+
if (currentSize > targetSize) {
|
|
1868
|
+
for (const item of items) {
|
|
1869
|
+
if (currentSize <= targetSize) {
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
currentSize -= this.estimateDataUrlSize(item.dataUrl);
|
|
1873
|
+
urlsToDelete.push(item.url);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
// 删除数据
|
|
1877
|
+
urlsToDelete.forEach((url) => {
|
|
1878
|
+
store.delete(url);
|
|
1879
|
+
});
|
|
1880
|
+
if (urlsToDelete.length > 0 && !this.options.silentMode) {
|
|
1881
|
+
console.log(`📸 IndexedDB 清理了 ${urlsToDelete.length} 个缓存项(超过大小限制)`);
|
|
1882
|
+
}
|
|
1883
|
+
resolve();
|
|
1884
|
+
};
|
|
1885
|
+
request.onerror = () => resolve();
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
catch {
|
|
1889
|
+
// 清理失败,忽略
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* 删除 IndexedDB 缓存
|
|
1894
|
+
*/
|
|
1895
|
+
async deleteIndexedDBCache(url) {
|
|
1896
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
try {
|
|
1900
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1901
|
+
const store = transaction.objectStore('images');
|
|
1902
|
+
store.delete(url);
|
|
1903
|
+
}
|
|
1904
|
+
catch {
|
|
1905
|
+
// 忽略错误
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* 检查元素是否在可视区域内(优化:使用 Intersection Observer 或 getBoundingClientRect)
|
|
1568
1910
|
*/
|
|
1569
1911
|
isElementVisible(element, container) {
|
|
1570
1912
|
if (!this.options.onlyVisibleImages) {
|
|
1571
1913
|
return true; // 如果禁用可视区域检测,返回 true
|
|
1572
1914
|
}
|
|
1915
|
+
// 如果使用 Intersection Observer 且元素已在缓存中,直接返回
|
|
1916
|
+
if (this.options.useIntersectionObserver && this.intersectionObserver) {
|
|
1917
|
+
if (this.visibleElementsCache.has(element)) {
|
|
1918
|
+
return true;
|
|
1919
|
+
}
|
|
1920
|
+
// 观察元素(如果还没有观察)
|
|
1921
|
+
this.intersectionObserver.observe(element);
|
|
1922
|
+
}
|
|
1923
|
+
// 回退到 getBoundingClientRect
|
|
1573
1924
|
try {
|
|
1574
1925
|
const rect = element.getBoundingClientRect();
|
|
1575
1926
|
const containerRect = container.getBoundingClientRect();
|
|
1576
1927
|
// 检查元素是否与容器有交集(考虑滚动位置)
|
|
1577
|
-
|
|
1928
|
+
const isIntersecting = !(rect.bottom < containerRect.top ||
|
|
1578
1929
|
rect.top > containerRect.bottom ||
|
|
1579
1930
|
rect.right < containerRect.left ||
|
|
1580
1931
|
rect.left > containerRect.right) && (rect.width > 0 && rect.height > 0 && // 元素有尺寸
|
|
1581
1932
|
window.getComputedStyle(element).display !== 'none' && // 元素可见
|
|
1582
1933
|
window.getComputedStyle(element).visibility !== 'hidden' &&
|
|
1583
1934
|
window.getComputedStyle(element).opacity !== '0');
|
|
1935
|
+
// 更新 Intersection Observer 缓存
|
|
1936
|
+
if (this.options.useIntersectionObserver && this.intersectionObserver) {
|
|
1937
|
+
if (isIntersecting) {
|
|
1938
|
+
this.visibleElementsCache.add(element);
|
|
1939
|
+
}
|
|
1940
|
+
else {
|
|
1941
|
+
this.visibleElementsCache.delete(element);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return isIntersecting;
|
|
1584
1945
|
}
|
|
1585
1946
|
catch {
|
|
1586
1947
|
return true; // 出错时返回 true,保守处理
|
|
@@ -1622,16 +1983,16 @@ class ScreenshotManager {
|
|
|
1622
1983
|
// 并发处理当前批次
|
|
1623
1984
|
await Promise.all(batch.map(async (img) => {
|
|
1624
1985
|
const originalSrc = img.src;
|
|
1625
|
-
//
|
|
1626
|
-
if (this.
|
|
1986
|
+
// 检查缓存(带过期时间)
|
|
1987
|
+
if (this.getCachedImage(originalSrc)) {
|
|
1627
1988
|
return;
|
|
1628
1989
|
}
|
|
1629
1990
|
try {
|
|
1630
1991
|
// 使用代理服务器获取图片
|
|
1631
1992
|
const dataUrl = await this.proxyImage(originalSrc);
|
|
1632
1993
|
if (dataUrl) {
|
|
1633
|
-
// 缓存 data URL
|
|
1634
|
-
this.
|
|
1994
|
+
// 缓存 data URL(带时间戳)
|
|
1995
|
+
this.setCachedImage(originalSrc, dataUrl);
|
|
1635
1996
|
}
|
|
1636
1997
|
}
|
|
1637
1998
|
catch (error) {
|
|
@@ -1712,7 +2073,7 @@ class ScreenshotManager {
|
|
|
1712
2073
|
});
|
|
1713
2074
|
}
|
|
1714
2075
|
/**
|
|
1715
|
-
*
|
|
2076
|
+
* 等待图片加载完成(优化:使用更短的超时和 Promise.race)
|
|
1716
2077
|
*/
|
|
1717
2078
|
async waitForImagesToLoad(element) {
|
|
1718
2079
|
const images = element.querySelectorAll('img');
|
|
@@ -1721,32 +2082,33 @@ class ScreenshotManager {
|
|
|
1721
2082
|
if (img.complete) {
|
|
1722
2083
|
return;
|
|
1723
2084
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
};
|
|
1736
|
-
}));
|
|
2085
|
+
// 使用更短的超时时间(可配置)
|
|
2086
|
+
const timeout = this.options.imageLoadTimeout || 5000;
|
|
2087
|
+
imagePromises.push(Promise.race([
|
|
2088
|
+
new Promise((resolve) => {
|
|
2089
|
+
img.onload = () => resolve();
|
|
2090
|
+
img.onerror = () => resolve(); // 失败也继续
|
|
2091
|
+
}),
|
|
2092
|
+
new Promise((resolve) => {
|
|
2093
|
+
setTimeout(() => resolve(), timeout);
|
|
2094
|
+
})
|
|
2095
|
+
]));
|
|
1737
2096
|
});
|
|
1738
2097
|
if (imagePromises.length > 0) {
|
|
1739
2098
|
await Promise.all(imagePromises);
|
|
1740
2099
|
}
|
|
1741
2100
|
}
|
|
1742
2101
|
/**
|
|
1743
|
-
* 等待 CSS
|
|
2102
|
+
* 等待 CSS 和字体加载完成(优化:减少等待时间,使用 requestAnimationFrame)
|
|
1744
2103
|
*/
|
|
1745
2104
|
async waitForStylesAndFonts() {
|
|
2105
|
+
// 使用 requestAnimationFrame 优化渲染时机
|
|
1746
2106
|
return new Promise((resolve) => {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2107
|
+
requestAnimationFrame(() => {
|
|
2108
|
+
setTimeout(() => {
|
|
2109
|
+
resolve();
|
|
2110
|
+
}, 30); // 减少到30ms
|
|
2111
|
+
});
|
|
1750
2112
|
});
|
|
1751
2113
|
}
|
|
1752
2114
|
/**
|
|
@@ -1966,9 +2328,86 @@ class ScreenshotManager {
|
|
|
1966
2328
|
this.messageHandler = null;
|
|
1967
2329
|
}
|
|
1968
2330
|
this.removeGlobalErrorHandlers();
|
|
2331
|
+
// 清理 Intersection Observer
|
|
2332
|
+
if (this.intersectionObserver) {
|
|
2333
|
+
this.intersectionObserver.disconnect();
|
|
2334
|
+
this.intersectionObserver = null;
|
|
2335
|
+
this.visibleElementsCache.clear();
|
|
2336
|
+
}
|
|
2337
|
+
// 关闭 IndexedDB
|
|
2338
|
+
if (this.indexedDBCache) {
|
|
2339
|
+
this.indexedDBCache.close();
|
|
2340
|
+
this.indexedDBCache = null;
|
|
2341
|
+
this.indexedDBReady = false;
|
|
2342
|
+
}
|
|
1969
2343
|
// 清理图片代理缓存
|
|
1970
2344
|
this.imageProxyCache.clear();
|
|
1971
2345
|
}
|
|
2346
|
+
/**
|
|
2347
|
+
* 获取缓存的图片(检查是否过期)
|
|
2348
|
+
*/
|
|
2349
|
+
getCachedImage(url) {
|
|
2350
|
+
const cached = this.imageProxyCache.get(url);
|
|
2351
|
+
if (!cached) {
|
|
2352
|
+
return null;
|
|
2353
|
+
}
|
|
2354
|
+
// 检查是否过期(10分钟)
|
|
2355
|
+
const now = Date.now();
|
|
2356
|
+
const age = now - cached.timestamp;
|
|
2357
|
+
if (age > this.options.imageCacheTTL) {
|
|
2358
|
+
// 缓存已过期,删除
|
|
2359
|
+
this.imageProxyCache.delete(url);
|
|
2360
|
+
return null;
|
|
2361
|
+
}
|
|
2362
|
+
return cached.dataUrl;
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* 设置缓存的图片(带时间戳)
|
|
2366
|
+
*/
|
|
2367
|
+
setCachedImage(url, dataUrl) {
|
|
2368
|
+
this.imageProxyCache.set(url, {
|
|
2369
|
+
dataUrl,
|
|
2370
|
+
timestamp: Date.now()
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* 清理过期缓存
|
|
2375
|
+
*/
|
|
2376
|
+
cleanExpiredCache() {
|
|
2377
|
+
const now = Date.now();
|
|
2378
|
+
const expiredUrls = [];
|
|
2379
|
+
this.imageProxyCache.forEach((cached, url) => {
|
|
2380
|
+
const age = now - cached.timestamp;
|
|
2381
|
+
if (age > this.options.imageCacheTTL) {
|
|
2382
|
+
expiredUrls.push(url);
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
expiredUrls.forEach(url => {
|
|
2386
|
+
this.imageProxyCache.delete(url);
|
|
2387
|
+
});
|
|
2388
|
+
if (expiredUrls.length > 0 && !this.options.silentMode) {
|
|
2389
|
+
console.log(`📸 清理了 ${expiredUrls.length} 个过期缓存`);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* 定期清理过期缓存(可选,在截图时也会自动清理)
|
|
2394
|
+
*/
|
|
2395
|
+
startCacheCleanup() {
|
|
2396
|
+
// 每5分钟清理一次过期缓存
|
|
2397
|
+
setInterval(() => {
|
|
2398
|
+
this.cleanExpiredCache();
|
|
2399
|
+
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
2400
|
+
if (this.options.useIndexedDB) {
|
|
2401
|
+
this.cleanIndexedDBCache().catch(() => {
|
|
2402
|
+
// 清理失败,忽略
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
if (!this.options.silentMode) {
|
|
2406
|
+
const memoryCacheSize = this.imageProxyCache.size;
|
|
2407
|
+
console.log(`📸 清理过期缓存,内存缓存数量: ${memoryCacheSize}`);
|
|
2408
|
+
}
|
|
2409
|
+
}, 300000); // 5分钟
|
|
2410
|
+
}
|
|
1972
2411
|
/**
|
|
1973
2412
|
* 获取状态
|
|
1974
2413
|
*/
|