customer-chat-sdk 1.0.38 → 1.0.41
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 +11 -0
- package/dist/core/ScreenshotManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +583 -354
- package/dist/customer-sdk.esm.js +583 -354
- package/dist/customer-sdk.min.js +2 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/customer-sdk.cjs.js
CHANGED
|
@@ -2472,12 +2472,24 @@ async function domToDataUrl(node, options) {
|
|
|
2472
2472
|
return dataUrl;
|
|
2473
2473
|
}
|
|
2474
2474
|
|
|
2475
|
+
async function domToJpeg(node, options) {
|
|
2476
|
+
return domToDataUrl(
|
|
2477
|
+
await orCreateContext(node, { ...options, type: "image/jpeg" })
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2475
2481
|
async function domToPng(node, options) {
|
|
2476
2482
|
return domToDataUrl(
|
|
2477
2483
|
await orCreateContext(node, { ...options, type: "image/png" })
|
|
2478
2484
|
);
|
|
2479
2485
|
}
|
|
2480
2486
|
|
|
2487
|
+
async function domToWebp(node, options) {
|
|
2488
|
+
return domToDataUrl(
|
|
2489
|
+
await orCreateContext(node, { ...options, type: "image/webp" })
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2481
2493
|
/*
|
|
2482
2494
|
* snapdom
|
|
2483
2495
|
* v.1.9.14
|
|
@@ -14311,6 +14323,13 @@ class ScreenshotManager {
|
|
|
14311
14323
|
this.screenshotTimer = null;
|
|
14312
14324
|
// modern-screenshot Worker 上下文(用于复用,避免频繁创建和销毁)
|
|
14313
14325
|
this.screenshotContext = null;
|
|
14326
|
+
this.contextElement = null; // 当前 context 对应的元素
|
|
14327
|
+
this.contextOptionsHash = ''; // context 配置的哈希值,用于判断是否需要重新创建
|
|
14328
|
+
// 截图锁,防止并发截图
|
|
14329
|
+
this.isScreenshotInProgress = false;
|
|
14330
|
+
// 截图队列(用于处理频繁的截图请求)
|
|
14331
|
+
this.screenshotQueue = [];
|
|
14332
|
+
this.isProcessingQueue = false;
|
|
14314
14333
|
// PostMessage 监听器
|
|
14315
14334
|
this.messageHandler = null;
|
|
14316
14335
|
// 动态轮询间隔(由 iframe 消息控制)
|
|
@@ -14336,7 +14355,7 @@ class ScreenshotManager {
|
|
|
14336
14355
|
this.globalRejectionHandler = null;
|
|
14337
14356
|
this.targetElement = targetElement;
|
|
14338
14357
|
this.options = {
|
|
14339
|
-
interval: options.interval ??
|
|
14358
|
+
interval: options.interval ?? 1000,
|
|
14340
14359
|
quality: options.quality ?? 0.3, // 降低默认质量:0.4 -> 0.3,减少 base64 大小
|
|
14341
14360
|
scale: options.scale ?? 1,
|
|
14342
14361
|
maxHistory: options.maxHistory ?? 10,
|
|
@@ -14363,7 +14382,8 @@ class ScreenshotManager {
|
|
|
14363
14382
|
maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
|
|
14364
14383
|
maxCacheAge: options.maxCacheAge ?? 86400000, // 默认24小时(86400000ms)
|
|
14365
14384
|
maxImageSize: options.maxImageSize ?? 5, // 不使用代理时,单个图片最大尺寸(MB),默认5MB
|
|
14366
|
-
skipLargeImages: options.skipLargeImages ?? true // 不使用代理时,是否跳过过大的图片,默认true(跳过)
|
|
14385
|
+
skipLargeImages: options.skipLargeImages ?? true, // 不使用代理时,是否跳过过大的图片,默认true(跳过)
|
|
14386
|
+
workerNumber: options.workerNumber ?? undefined // modern-screenshot Worker 数量,默认自动计算(undefined 表示自动)
|
|
14367
14387
|
};
|
|
14368
14388
|
this.setupMessageListener();
|
|
14369
14389
|
this.setupVisibilityChangeListener();
|
|
@@ -14391,15 +14411,22 @@ class ScreenshotManager {
|
|
|
14391
14411
|
* 设置目标元素
|
|
14392
14412
|
*/
|
|
14393
14413
|
setTargetElement(element) {
|
|
14394
|
-
//
|
|
14395
|
-
if (this.targetElement !== element
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14414
|
+
// 如果元素变化,需要清理 context(下次截图时会重新创建)
|
|
14415
|
+
if (this.targetElement !== element) {
|
|
14416
|
+
if (this.screenshotContext) {
|
|
14417
|
+
try {
|
|
14418
|
+
destroyContext(this.screenshotContext);
|
|
14419
|
+
if (!this.options.silentMode) {
|
|
14420
|
+
console.log('📸 目标元素变化,清理 context');
|
|
14421
|
+
}
|
|
14422
|
+
}
|
|
14423
|
+
catch (e) {
|
|
14424
|
+
// 忽略清理错误
|
|
14425
|
+
}
|
|
14426
|
+
this.screenshotContext = null;
|
|
14427
|
+
this.contextElement = null;
|
|
14428
|
+
this.contextOptionsHash = '';
|
|
14401
14429
|
}
|
|
14402
|
-
this.screenshotContext = null;
|
|
14403
14430
|
}
|
|
14404
14431
|
this.targetElement = element;
|
|
14405
14432
|
}
|
|
@@ -14627,22 +14654,36 @@ class ScreenshotManager {
|
|
|
14627
14654
|
if (!this.worker && this.options.compress) {
|
|
14628
14655
|
this.worker = this.createWorker();
|
|
14629
14656
|
}
|
|
14630
|
-
//
|
|
14631
|
-
|
|
14657
|
+
// 设置定时器(使用递归 setTimeout,确保等待前一个完成)
|
|
14658
|
+
// 这样可以避免 setInterval 不等待异步完成的问题
|
|
14659
|
+
const scheduleNext = async () => {
|
|
14632
14660
|
if (this.isRunning && this.isEnabled && !document.hidden) {
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
.
|
|
14640
|
-
|
|
14641
|
-
|
|
14661
|
+
try {
|
|
14662
|
+
await this.takeScreenshot();
|
|
14663
|
+
// 如果配置了上传,且当前有上传配置,自动上传
|
|
14664
|
+
if (this.currentUploadConfig) {
|
|
14665
|
+
const latestScreenshot = this.getLatestScreenshot();
|
|
14666
|
+
if (latestScreenshot && !this.isUploading) {
|
|
14667
|
+
this.uploadScreenshot(latestScreenshot, this.currentUploadConfig)
|
|
14668
|
+
.catch((error) => {
|
|
14669
|
+
console.error('📸 [轮询] 自动上传失败:', error);
|
|
14670
|
+
});
|
|
14671
|
+
}
|
|
14672
|
+
}
|
|
14673
|
+
}
|
|
14674
|
+
catch (error) {
|
|
14675
|
+
if (!this.options.silentMode) {
|
|
14676
|
+
console.error('📸 [轮询] 截图失败:', error);
|
|
14642
14677
|
}
|
|
14643
14678
|
}
|
|
14644
14679
|
}
|
|
14645
|
-
|
|
14680
|
+
// 如果还在运行,安排下一次截图
|
|
14681
|
+
if (this.isRunning) {
|
|
14682
|
+
this.screenshotTimer = setTimeout(scheduleNext, currentInterval);
|
|
14683
|
+
}
|
|
14684
|
+
};
|
|
14685
|
+
// 立即开始第一次
|
|
14686
|
+
scheduleNext();
|
|
14646
14687
|
// 注意:不再立即执行一次,因为已经在 takeScreenshotAndUpload 中执行了
|
|
14647
14688
|
}
|
|
14648
14689
|
/**
|
|
@@ -14945,16 +14986,22 @@ class ScreenshotManager {
|
|
|
14945
14986
|
console.log('📸 使用 html2canvas 引擎截图...');
|
|
14946
14987
|
}
|
|
14947
14988
|
try {
|
|
14989
|
+
// 检测 iOS 设备
|
|
14990
|
+
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
14948
14991
|
// html2canvas 需要确保样式完全加载,额外等待
|
|
14949
|
-
//
|
|
14992
|
+
// iOS 设备需要更长的等待时间,因为样式表加载和处理方式不同
|
|
14950
14993
|
await this.waitForAllStylesLoaded();
|
|
14951
14994
|
// 等待字体加载完成
|
|
14952
14995
|
await this.waitForFonts();
|
|
14996
|
+
// iOS 设备需要额外的等待时间,确保样式完全应用
|
|
14997
|
+
if (isIOS) {
|
|
14998
|
+
await new Promise(resolve => setTimeout(resolve, 300)); // iOS 额外等待 300ms
|
|
14999
|
+
}
|
|
14953
15000
|
// 等待 DOM 完全渲染
|
|
14954
15001
|
await new Promise(resolve => {
|
|
14955
15002
|
requestAnimationFrame(() => {
|
|
14956
15003
|
requestAnimationFrame(() => {
|
|
14957
|
-
setTimeout(() => resolve(), 100);
|
|
15004
|
+
setTimeout(() => resolve(), isIOS ? 200 : 100); // iOS 使用更长的等待时间
|
|
14958
15005
|
});
|
|
14959
15006
|
});
|
|
14960
15007
|
});
|
|
@@ -14990,9 +15037,11 @@ class ScreenshotManager {
|
|
|
14990
15037
|
// width: finalWidth, // ❌ 移除,会导致宽度不正确
|
|
14991
15038
|
// height: finalHeight, // ❌ 移除,会导致高度不正确
|
|
14992
15039
|
// 关键配置:确保样式正确渲染
|
|
14993
|
-
//
|
|
14994
|
-
//
|
|
14995
|
-
onclone: (clonedDoc,
|
|
15040
|
+
// iOS 特定配置:启用 foreignObjectRendering 以确保样式正确渲染
|
|
15041
|
+
// iOS Safari 和 Chrome 对样式表的处理方式不同,需要启用此选项
|
|
15042
|
+
onclone: (clonedDoc, clonedElement) => {
|
|
15043
|
+
// 检测 iOS 设备(需要在 onclone 内部检测,因为这是回调函数)
|
|
15044
|
+
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
14996
15045
|
// 在克隆的文档中,确保所有样式都正确应用
|
|
14997
15046
|
// html2canvas 会自动处理样式,但我们需要确保样式表被正确复制
|
|
14998
15047
|
// 确保 clonedDoc.head 存在
|
|
@@ -15081,10 +15130,53 @@ class ScreenshotManager {
|
|
|
15081
15130
|
catch (e) {
|
|
15082
15131
|
// 忽略错误,继续执行
|
|
15083
15132
|
}
|
|
15133
|
+
// 4. iOS 特定处理:将计算后的样式内联化(确保样式正确应用)
|
|
15134
|
+
// iOS Safari/Chrome 对样式表的处理方式不同,需要将计算后的样式内联化
|
|
15135
|
+
if (isIOS) {
|
|
15136
|
+
try {
|
|
15137
|
+
// 遍历克隆文档中的所有元素,将计算后的样式内联化
|
|
15138
|
+
const allElements = clonedElement.querySelectorAll('*');
|
|
15139
|
+
allElements.forEach((el) => {
|
|
15140
|
+
try {
|
|
15141
|
+
const htmlEl = el;
|
|
15142
|
+
const computedStyle = window.getComputedStyle(htmlEl);
|
|
15143
|
+
// 获取关键样式属性并内联化
|
|
15144
|
+
const importantStyles = [];
|
|
15145
|
+
// 获取所有样式属性(iOS 需要更完整的样式)
|
|
15146
|
+
for (let i = 0; i < computedStyle.length; i++) {
|
|
15147
|
+
const prop = computedStyle[i];
|
|
15148
|
+
const value = computedStyle.getPropertyValue(prop);
|
|
15149
|
+
const priority = computedStyle.getPropertyPriority(prop);
|
|
15150
|
+
// 只内联化非默认值的重要样式
|
|
15151
|
+
if (value && value !== 'none' && value !== 'auto' && value !== 'normal') {
|
|
15152
|
+
importantStyles.push(`${prop}: ${value}${priority === 'important' ? ' !important' : ''}`);
|
|
15153
|
+
}
|
|
15154
|
+
}
|
|
15155
|
+
// 如果有关键样式,添加到内联样式
|
|
15156
|
+
if (importantStyles.length > 0) {
|
|
15157
|
+
const currentStyle = htmlEl.getAttribute('style') || '';
|
|
15158
|
+
const newStyle = currentStyle
|
|
15159
|
+
? `${currentStyle}; ${importantStyles.join('; ')}`
|
|
15160
|
+
: importantStyles.join('; ');
|
|
15161
|
+
htmlEl.setAttribute('style', newStyle);
|
|
15162
|
+
}
|
|
15163
|
+
}
|
|
15164
|
+
catch (e) {
|
|
15165
|
+
// 忽略单个元素的错误
|
|
15166
|
+
}
|
|
15167
|
+
});
|
|
15168
|
+
}
|
|
15169
|
+
catch (e) {
|
|
15170
|
+
// 忽略内联化错误
|
|
15171
|
+
if (!this.options.silentMode) {
|
|
15172
|
+
console.warn('📸 iOS 样式内联化失败:', e);
|
|
15173
|
+
}
|
|
15174
|
+
}
|
|
15175
|
+
}
|
|
15084
15176
|
if (!this.options.silentMode) {
|
|
15085
15177
|
const styleLinks = clonedDoc.querySelectorAll('link[rel="stylesheet"]').length;
|
|
15086
15178
|
const styleTags = clonedDoc.querySelectorAll('style').length;
|
|
15087
|
-
console.log(`📸 onclone: 已复制 ${styleLinks} 个样式表链接和 ${styleTags}
|
|
15179
|
+
console.log(`📸 onclone: 已复制 ${styleLinks} 个样式表链接和 ${styleTags} 个内联样式标签${isIOS ? ' (iOS 模式:已内联化计算样式)' : ''}`);
|
|
15088
15180
|
}
|
|
15089
15181
|
},
|
|
15090
15182
|
// 性能优化
|
|
@@ -15210,389 +15302,514 @@ class ScreenshotManager {
|
|
|
15210
15302
|
* - 页面资源较少
|
|
15211
15303
|
*/
|
|
15212
15304
|
async takeScreenshotWithModernScreenshot(element) {
|
|
15305
|
+
// 检查是否有截图正在进行(防止并发冲突)
|
|
15306
|
+
// 如果正在进行,将请求加入队列,而不是直接拒绝
|
|
15307
|
+
if (this.isScreenshotInProgress) {
|
|
15308
|
+
// 队列最多保留 1 个请求,避免积压
|
|
15309
|
+
if (this.screenshotQueue.length >= 1) {
|
|
15310
|
+
if (!this.options.silentMode) {
|
|
15311
|
+
console.log('📸 截图队列已满,跳过当前请求(等待队列处理)');
|
|
15312
|
+
}
|
|
15313
|
+
// 等待队列中的请求完成
|
|
15314
|
+
return new Promise((resolve, reject) => {
|
|
15315
|
+
const checkQueue = () => {
|
|
15316
|
+
if (!this.isScreenshotInProgress && this.screenshotQueue.length === 0) {
|
|
15317
|
+
// 队列已清空,重新尝试
|
|
15318
|
+
this.takeScreenshotWithModernScreenshot(element).then(resolve).catch(reject);
|
|
15319
|
+
}
|
|
15320
|
+
else {
|
|
15321
|
+
setTimeout(checkQueue, 100); // 100ms 后再次检查
|
|
15322
|
+
}
|
|
15323
|
+
};
|
|
15324
|
+
checkQueue();
|
|
15325
|
+
});
|
|
15326
|
+
}
|
|
15327
|
+
// 将请求加入队列
|
|
15328
|
+
return new Promise((resolve, reject) => {
|
|
15329
|
+
this.screenshotQueue.push({ resolve: () => {
|
|
15330
|
+
this.takeScreenshotWithModernScreenshot(element).then(resolve).catch(reject);
|
|
15331
|
+
}, reject });
|
|
15332
|
+
// 启动队列处理(如果还没启动)
|
|
15333
|
+
if (!this.isProcessingQueue) {
|
|
15334
|
+
this.processScreenshotQueue();
|
|
15335
|
+
}
|
|
15336
|
+
});
|
|
15337
|
+
}
|
|
15338
|
+
this.isScreenshotInProgress = true;
|
|
15213
15339
|
if (!this.options.silentMode) {
|
|
15214
15340
|
console.log('📸 使用 modern-screenshot 引擎截图(Worker 模式)...');
|
|
15215
15341
|
}
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
// 对于 body 或 html 元素,使用页面的完整尺寸(包括滚动内容)
|
|
15222
|
-
elementWidth = Math.max(element.scrollWidth, element.offsetWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth, window.innerWidth);
|
|
15223
|
-
elementHeight = Math.max(element.scrollHeight, element.offsetHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight, window.innerHeight);
|
|
15224
|
-
}
|
|
15225
|
-
else {
|
|
15226
|
-
// 对于其他元素,使用元素的完整尺寸
|
|
15227
|
-
elementWidth = element.scrollWidth || element.clientWidth || element.offsetWidth;
|
|
15228
|
-
elementHeight = element.scrollHeight || element.clientHeight || element.offsetHeight;
|
|
15229
|
-
}
|
|
15230
|
-
if (!this.options.silentMode) {
|
|
15231
|
-
console.log(`📸 目标元素: ${element.tagName}${element.id ? '#' + element.id : ''}${element.className ? '.' + element.className.split(' ').join('.') : ''}`);
|
|
15232
|
-
console.log(`📸 元素尺寸: ${elementWidth}x${elementHeight}`);
|
|
15233
|
-
console.log(`📸 scrollWidth: ${element.scrollWidth}, scrollHeight: ${element.scrollHeight}`);
|
|
15234
|
-
console.log(`📸 clientWidth: ${element.clientWidth}, clientHeight: ${element.clientHeight}`);
|
|
15235
|
-
console.log(`📸 offsetWidth: ${element.offsetWidth}, offsetHeight: ${element.offsetHeight}`);
|
|
15342
|
+
try {
|
|
15343
|
+
// 获取元素的实际尺寸(使用 scrollWidth/scrollHeight 获取完整内容尺寸)
|
|
15344
|
+
// 对于 document.body,需要特殊处理,确保截取完整页面内容
|
|
15345
|
+
let elementWidth;
|
|
15346
|
+
let elementHeight;
|
|
15236
15347
|
if (element === document.body || element === document.documentElement) {
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
}
|
|
15241
|
-
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
15242
|
-
const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
|
|
15243
|
-
// 进一步降低质量以减少 base64 大小
|
|
15244
|
-
// 桌面设备:使用配置的质量(默认 0.3)
|
|
15245
|
-
// 移动设备/低端设备:进一步降低到 0.2(最低)
|
|
15246
|
-
const finalQuality = isMobile || isLowEndDevice
|
|
15247
|
-
? Math.max(this.options.quality * 0.65, 0.2) // 移动设备:质量 * 0.65,最低 0.2
|
|
15248
|
-
: this.options.quality; // 桌面设备:使用配置的质量(默认 0.3)
|
|
15249
|
-
// 计算压缩后的尺寸(对所有元素都应用,包括 document.body)
|
|
15250
|
-
// 这样可以避免生成过大的截图,减少 base64 大小
|
|
15251
|
-
const { width, height } = this.calculateCompressedSize(elementWidth, elementHeight, this.options.maxWidth, this.options.maxHeight);
|
|
15252
|
-
// 对于所有元素都应用尺寸限制(包括 body),避免截图过大
|
|
15253
|
-
// 如果计算后的尺寸小于元素实际尺寸,使用压缩尺寸;否则使用元素实际尺寸(但不超过最大值)
|
|
15254
|
-
const finalWidth = width < elementWidth ? width : Math.min(elementWidth, this.options.maxWidth);
|
|
15255
|
-
const finalHeight = height < elementHeight ? height : Math.min(elementHeight, this.options.maxHeight);
|
|
15256
|
-
// 处理跨域图片的函数
|
|
15257
|
-
const handleCrossOriginImage = async (url) => {
|
|
15258
|
-
// 如果是 data URL 或 blob URL,直接返回
|
|
15259
|
-
if (url.startsWith('data:') || url.startsWith('blob:')) {
|
|
15260
|
-
return url;
|
|
15348
|
+
// 对于 body 或 html 元素,使用页面的完整尺寸(包括滚动内容)
|
|
15349
|
+
elementWidth = Math.max(element.scrollWidth, element.offsetWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth, window.innerWidth);
|
|
15350
|
+
elementHeight = Math.max(element.scrollHeight, element.offsetHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight, window.innerHeight);
|
|
15261
15351
|
}
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
return url;
|
|
15267
|
-
}
|
|
15352
|
+
else {
|
|
15353
|
+
// 对于其他元素,使用元素的完整尺寸
|
|
15354
|
+
elementWidth = element.scrollWidth || element.clientWidth || element.offsetWidth;
|
|
15355
|
+
elementHeight = element.scrollHeight || element.clientHeight || element.offsetHeight;
|
|
15268
15356
|
}
|
|
15269
|
-
|
|
15270
|
-
|
|
15357
|
+
if (!this.options.silentMode) {
|
|
15358
|
+
console.log(`📸 目标元素: ${element.tagName}${element.id ? '#' + element.id : ''}${element.className ? '.' + element.className.split(' ').join('.') : ''}`);
|
|
15359
|
+
console.log(`📸 元素尺寸: ${elementWidth}x${elementHeight}`);
|
|
15360
|
+
console.log(`📸 scrollWidth: ${element.scrollWidth}, scrollHeight: ${element.scrollHeight}`);
|
|
15361
|
+
console.log(`📸 clientWidth: ${element.clientWidth}, clientHeight: ${element.clientHeight}`);
|
|
15362
|
+
console.log(`📸 offsetWidth: ${element.offsetWidth}, offsetHeight: ${element.offsetHeight}`);
|
|
15363
|
+
if (element === document.body || element === document.documentElement) {
|
|
15364
|
+
console.log(`📸 页面完整尺寸: ${document.documentElement.scrollWidth}x${document.documentElement.scrollHeight}`);
|
|
15365
|
+
console.log(`📸 窗口尺寸: ${window.innerWidth}x${window.innerHeight}`);
|
|
15366
|
+
}
|
|
15271
15367
|
}
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15368
|
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
15369
|
+
const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
|
|
15370
|
+
// 进一步降低质量以减少 base64 大小
|
|
15371
|
+
// 桌面设备:使用配置的质量(默认 0.3)
|
|
15372
|
+
// 移动设备/低端设备:进一步降低到 0.2(最低)
|
|
15373
|
+
const finalQuality = isMobile || isLowEndDevice
|
|
15374
|
+
? Math.max(this.options.quality * 0.65, 0.2) // 移动设备:质量 * 0.65,最低 0.2
|
|
15375
|
+
: this.options.quality; // 桌面设备:使用配置的质量(默认 0.3)
|
|
15376
|
+
// 计算压缩后的尺寸(对所有元素都应用,包括 document.body)
|
|
15377
|
+
// 这样可以避免生成过大的截图,减少 base64 大小
|
|
15378
|
+
const { width, height } = this.calculateCompressedSize(elementWidth, elementHeight, this.options.maxWidth, this.options.maxHeight);
|
|
15379
|
+
// 对于所有元素都应用尺寸限制(包括 body),避免截图过大
|
|
15380
|
+
// 如果计算后的尺寸小于元素实际尺寸,使用压缩尺寸;否则使用元素实际尺寸(但不超过最大值)
|
|
15381
|
+
const finalWidth = width < elementWidth ? width : Math.min(elementWidth, this.options.maxWidth);
|
|
15382
|
+
const finalHeight = height < elementHeight ? height : Math.min(elementHeight, this.options.maxHeight);
|
|
15383
|
+
// 处理跨域图片的函数
|
|
15384
|
+
const handleCrossOriginImage = async (url) => {
|
|
15385
|
+
// 如果是 data URL 或 blob URL,直接返回
|
|
15386
|
+
if (url.startsWith('data:') || url.startsWith('blob:')) {
|
|
15387
|
+
return url;
|
|
15388
|
+
}
|
|
15389
|
+
// 如果是同源图片,直接返回
|
|
15390
|
+
try {
|
|
15391
|
+
const imgUrl = new URL(url, window.location.href);
|
|
15392
|
+
if (imgUrl.origin === window.location.origin) {
|
|
15393
|
+
return url;
|
|
15281
15394
|
}
|
|
15282
|
-
return cachedDataUrl;
|
|
15283
15395
|
}
|
|
15284
|
-
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15396
|
+
catch (e) {
|
|
15397
|
+
// URL 解析失败,继续处理
|
|
15398
|
+
}
|
|
15399
|
+
// 如果配置了代理服务器,使用代理处理跨域图片
|
|
15400
|
+
// 只有当 useProxy 为 true 且 proxyUrl 存在时才使用代理
|
|
15401
|
+
const shouldUseProxy = this.options.useProxy && this.options.proxyUrl && this.options.proxyUrl.trim() !== '';
|
|
15402
|
+
if (shouldUseProxy) {
|
|
15403
|
+
// 检查内存缓存(优先使用缓存,带过期时间检查)
|
|
15404
|
+
const cachedDataUrl = this.getCachedImage(url);
|
|
15405
|
+
if (cachedDataUrl) {
|
|
15290
15406
|
if (!this.options.silentMode) {
|
|
15291
|
-
console.log(`📸 ✅
|
|
15407
|
+
console.log(`📸 ✅ 使用内存缓存图片: ${url.substring(0, 50)}...`);
|
|
15292
15408
|
}
|
|
15293
|
-
return
|
|
15409
|
+
return cachedDataUrl;
|
|
15294
15410
|
}
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
let baseUrl = this.options.proxyUrl;
|
|
15306
|
-
baseUrl = baseUrl.replace(/[?&]$/, '');
|
|
15307
|
-
const proxyUrl = `${baseUrl}?${params.toString()}`;
|
|
15308
|
-
// 请求代理服务器(优化:添加超时控制和优先级)
|
|
15309
|
-
const controller = new AbortController();
|
|
15310
|
-
const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
|
|
15311
|
-
try {
|
|
15312
|
-
const fetchOptions = {
|
|
15313
|
-
method: 'GET',
|
|
15314
|
-
mode: 'cors',
|
|
15315
|
-
credentials: 'omit',
|
|
15316
|
-
headers: {
|
|
15317
|
-
'Accept': 'image/*'
|
|
15318
|
-
},
|
|
15319
|
-
cache: 'no-cache',
|
|
15320
|
-
signal: controller.signal
|
|
15321
|
-
};
|
|
15322
|
-
// 添加 fetch priority(如果支持)
|
|
15323
|
-
if ('priority' in fetchOptions) {
|
|
15324
|
-
fetchOptions.priority = this.options.fetchPriority;
|
|
15411
|
+
// 检查 IndexedDB 缓存(如果启用)
|
|
15412
|
+
if (this.options.useIndexedDB) {
|
|
15413
|
+
const indexedDBCache = await this.getIndexedDBCache(url);
|
|
15414
|
+
if (indexedDBCache) {
|
|
15415
|
+
// 同步到内存缓存
|
|
15416
|
+
this.setCachedImage(url, indexedDBCache);
|
|
15417
|
+
if (!this.options.silentMode) {
|
|
15418
|
+
console.log(`📸 ✅ 使用 IndexedDB 缓存图片: ${url.substring(0, 50)}...`);
|
|
15419
|
+
}
|
|
15420
|
+
return indexedDBCache;
|
|
15325
15421
|
}
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
|
|
15329
|
-
|
|
15422
|
+
}
|
|
15423
|
+
try {
|
|
15424
|
+
// 构建代理请求参数
|
|
15425
|
+
const params = new URLSearchParams({
|
|
15426
|
+
url: url,
|
|
15427
|
+
maxWidth: String(this.options.maxWidth || 1600),
|
|
15428
|
+
maxHeight: String(this.options.maxHeight || 900),
|
|
15429
|
+
quality: String(Math.round((this.options.quality || 0.4) * 100)),
|
|
15430
|
+
format: this.options.outputFormat || 'webp'
|
|
15431
|
+
});
|
|
15432
|
+
let baseUrl = this.options.proxyUrl;
|
|
15433
|
+
baseUrl = baseUrl.replace(/[?&]$/, '');
|
|
15434
|
+
const proxyUrl = `${baseUrl}?${params.toString()}`;
|
|
15435
|
+
// 请求代理服务器(优化:添加超时控制和优先级)
|
|
15436
|
+
const controller = new AbortController();
|
|
15437
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
|
|
15438
|
+
try {
|
|
15439
|
+
const fetchOptions = {
|
|
15440
|
+
method: 'GET',
|
|
15441
|
+
mode: 'cors',
|
|
15442
|
+
credentials: 'omit',
|
|
15443
|
+
headers: {
|
|
15444
|
+
'Accept': 'image/*'
|
|
15445
|
+
},
|
|
15446
|
+
cache: 'no-cache',
|
|
15447
|
+
signal: controller.signal
|
|
15448
|
+
};
|
|
15449
|
+
// 添加 fetch priority(如果支持)
|
|
15450
|
+
if ('priority' in fetchOptions) {
|
|
15451
|
+
fetchOptions.priority = this.options.fetchPriority;
|
|
15452
|
+
}
|
|
15453
|
+
const response = await fetch(proxyUrl, fetchOptions);
|
|
15454
|
+
clearTimeout(timeoutId);
|
|
15455
|
+
if (!response.ok) {
|
|
15456
|
+
throw new Error(`代理请求失败: ${response.status}`);
|
|
15457
|
+
}
|
|
15458
|
+
const blob = await response.blob();
|
|
15459
|
+
const dataUrl = await this.blobToDataUrl(blob);
|
|
15460
|
+
// 缓存结果(带时间戳,10分钟有效)
|
|
15461
|
+
this.setCachedImage(url, dataUrl);
|
|
15462
|
+
// 如果启用 IndexedDB,也保存到 IndexedDB
|
|
15463
|
+
if (this.options.useIndexedDB) {
|
|
15464
|
+
await this.setIndexedDBCache(url, dataUrl);
|
|
15465
|
+
}
|
|
15466
|
+
return dataUrl;
|
|
15330
15467
|
}
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
this.setCachedImage(url, dataUrl);
|
|
15335
|
-
// 如果启用 IndexedDB,也保存到 IndexedDB
|
|
15336
|
-
if (this.options.useIndexedDB) {
|
|
15337
|
-
await this.setIndexedDBCache(url, dataUrl);
|
|
15468
|
+
catch (fetchError) {
|
|
15469
|
+
clearTimeout(timeoutId);
|
|
15470
|
+
throw fetchError;
|
|
15338
15471
|
}
|
|
15339
|
-
return dataUrl;
|
|
15340
15472
|
}
|
|
15341
|
-
catch (
|
|
15342
|
-
|
|
15343
|
-
|
|
15473
|
+
catch (error) {
|
|
15474
|
+
if (!this.options.silentMode) {
|
|
15475
|
+
console.warn(`📸 代理处理图片失败: ${url.substring(0, 100)}...`, error);
|
|
15476
|
+
}
|
|
15477
|
+
// 失败时返回原 URL
|
|
15478
|
+
return url;
|
|
15344
15479
|
}
|
|
15345
15480
|
}
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15481
|
+
// 如果没有配置代理,需要添加内存保护机制
|
|
15482
|
+
// 不使用代理时,modern-screenshot 会直接下载图片,可能导致内存问题
|
|
15483
|
+
// 由于已配置 CORS,可以直接下载并检查大小
|
|
15484
|
+
if (this.options.enableCORS) {
|
|
15485
|
+
// 对于不使用代理的情况,添加内存保护和缓存机制:
|
|
15486
|
+
// 1. 先检查内存缓存(避免重复下载)
|
|
15487
|
+
// 2. 检查 IndexedDB 缓存
|
|
15488
|
+
// 3. 使用下载队列避免并发重复下载
|
|
15489
|
+
// 4. 下载时检查大小,如果过大则使用占位符
|
|
15490
|
+
// 先检查内存缓存(优先使用缓存,避免重复下载)
|
|
15491
|
+
const cachedDataUrl = this.getCachedImage(url);
|
|
15492
|
+
if (cachedDataUrl) {
|
|
15493
|
+
if (!this.options.silentMode) {
|
|
15494
|
+
console.log(`📸 ✅ 使用内存缓存图片(无代理模式): ${url.substring(0, 50)}...`);
|
|
15495
|
+
}
|
|
15496
|
+
return cachedDataUrl;
|
|
15349
15497
|
}
|
|
15350
|
-
//
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
// 3. 使用下载队列避免并发重复下载
|
|
15362
|
-
// 4. 下载时检查大小,如果过大则使用占位符
|
|
15363
|
-
// 先检查内存缓存(优先使用缓存,避免重复下载)
|
|
15364
|
-
const cachedDataUrl = this.getCachedImage(url);
|
|
15365
|
-
if (cachedDataUrl) {
|
|
15366
|
-
if (!this.options.silentMode) {
|
|
15367
|
-
console.log(`📸 ✅ 使用内存缓存图片(无代理模式): ${url.substring(0, 50)}...`);
|
|
15498
|
+
// 检查 IndexedDB 缓存(如果启用)
|
|
15499
|
+
if (this.options.useIndexedDB) {
|
|
15500
|
+
const indexedDBCache = await this.getIndexedDBCache(url);
|
|
15501
|
+
if (indexedDBCache) {
|
|
15502
|
+
// 同步到内存缓存
|
|
15503
|
+
this.setCachedImage(url, indexedDBCache);
|
|
15504
|
+
if (!this.options.silentMode) {
|
|
15505
|
+
console.log(`📸 ✅ 使用 IndexedDB 缓存图片(无代理模式): ${url.substring(0, 50)}...`);
|
|
15506
|
+
}
|
|
15507
|
+
return indexedDBCache;
|
|
15508
|
+
}
|
|
15368
15509
|
}
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
if (this.options.useIndexedDB) {
|
|
15373
|
-
const indexedDBCache = await this.getIndexedDBCache(url);
|
|
15374
|
-
if (indexedDBCache) {
|
|
15375
|
-
// 同步到内存缓存
|
|
15376
|
-
this.setCachedImage(url, indexedDBCache);
|
|
15510
|
+
// 检查是否正在下载(避免重复下载)
|
|
15511
|
+
if (this.imageDownloadQueue.has(url)) {
|
|
15512
|
+
// 如果已经在下载队列中,等待现有下载完成
|
|
15377
15513
|
if (!this.options.silentMode) {
|
|
15378
|
-
console.log(`📸
|
|
15514
|
+
console.log(`📸 ⏳ 等待图片下载完成: ${url.substring(0, 50)}...`);
|
|
15379
15515
|
}
|
|
15380
|
-
return
|
|
15516
|
+
return await this.imageDownloadQueue.get(url);
|
|
15381
15517
|
}
|
|
15382
|
-
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15518
|
+
// 检查并发下载数限制
|
|
15519
|
+
if (this.activeDownloads.size >= this.maxConcurrentImageDownloads) {
|
|
15520
|
+
// 并发数已满,返回原 URL,让 modern-screenshot 自己处理(可能会失败,但不阻塞)
|
|
15521
|
+
if (!this.options.silentMode) {
|
|
15522
|
+
console.warn(`📸 ⚠️ 并发下载数已满(${this.activeDownloads.size}/${this.maxConcurrentImageDownloads}),跳过: ${url.substring(0, 50)}...`);
|
|
15523
|
+
}
|
|
15524
|
+
return url;
|
|
15388
15525
|
}
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15526
|
+
// 创建下载 Promise 并加入队列
|
|
15527
|
+
const downloadPromise = this.downloadImageWithoutProxy(url);
|
|
15528
|
+
this.imageDownloadQueue.set(url, downloadPromise);
|
|
15529
|
+
this.activeDownloads.add(url);
|
|
15530
|
+
try {
|
|
15531
|
+
const result = await downloadPromise;
|
|
15532
|
+
return result;
|
|
15533
|
+
}
|
|
15534
|
+
finally {
|
|
15535
|
+
// 下载完成后清理
|
|
15536
|
+
this.imageDownloadQueue.delete(url);
|
|
15537
|
+
this.activeDownloads.delete(url);
|
|
15396
15538
|
}
|
|
15397
|
-
return url;
|
|
15398
|
-
}
|
|
15399
|
-
// 创建下载 Promise 并加入队列
|
|
15400
|
-
const downloadPromise = this.downloadImageWithoutProxy(url);
|
|
15401
|
-
this.imageDownloadQueue.set(url, downloadPromise);
|
|
15402
|
-
this.activeDownloads.add(url);
|
|
15403
|
-
try {
|
|
15404
|
-
const result = await downloadPromise;
|
|
15405
|
-
return result;
|
|
15406
|
-
}
|
|
15407
|
-
finally {
|
|
15408
|
-
// 下载完成后清理
|
|
15409
|
-
this.imageDownloadQueue.delete(url);
|
|
15410
|
-
this.activeDownloads.delete(url);
|
|
15411
15539
|
}
|
|
15540
|
+
// 默认返回原 URL
|
|
15541
|
+
return url;
|
|
15542
|
+
};
|
|
15543
|
+
// 检查元素是否可见且有尺寸
|
|
15544
|
+
const rect = element.getBoundingClientRect();
|
|
15545
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
15546
|
+
throw new Error('元素尺寸为 0,无法截图');
|
|
15412
15547
|
}
|
|
15413
|
-
//
|
|
15414
|
-
|
|
15415
|
-
|
|
15416
|
-
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
|
|
15420
|
-
}
|
|
15421
|
-
// 每次截图都重新创建 context,确保使用最新的元素状态
|
|
15422
|
-
// 如果已有 context,先清理
|
|
15423
|
-
if (this.screenshotContext) {
|
|
15424
|
-
try {
|
|
15425
|
-
destroyContext(this.screenshotContext);
|
|
15548
|
+
// Worker 数量配置:智能计算或使用用户配置
|
|
15549
|
+
// workerNumber > 0 会启用 Worker 模式,截图处理在后台线程执行,不会阻塞主线程 UI
|
|
15550
|
+
// 如果用户指定了 workerNumber,直接使用;否则根据设备性能自动计算
|
|
15551
|
+
let workerNumber;
|
|
15552
|
+
if (this.options.workerNumber !== undefined && this.options.workerNumber > 0) {
|
|
15553
|
+
// 用户明确指定了 workerNumber
|
|
15554
|
+
workerNumber = this.options.workerNumber;
|
|
15426
15555
|
}
|
|
15427
|
-
|
|
15428
|
-
//
|
|
15556
|
+
else {
|
|
15557
|
+
// 自动计算 workerNumber
|
|
15558
|
+
const cpuCores = navigator.hardwareConcurrency || 4; // 默认假设 4 核
|
|
15559
|
+
if (isMobile || isLowEndDevice) {
|
|
15560
|
+
// 移动设备/低端设备:使用 1 个 Worker(避免内存压力)
|
|
15561
|
+
workerNumber = 1;
|
|
15562
|
+
}
|
|
15563
|
+
else if (cpuCores >= 8) {
|
|
15564
|
+
// 高性能设备(8核及以上):使用 3-4 个 Worker(充分利用多核)
|
|
15565
|
+
// 但根据截图间隔调整:频繁截图(间隔 < 2秒)时使用更多 Worker
|
|
15566
|
+
const isFrequentScreenshot = this.options.interval < 2000;
|
|
15567
|
+
workerNumber = isFrequentScreenshot ? Math.min(4, Math.floor(cpuCores / 2)) : 3;
|
|
15568
|
+
}
|
|
15569
|
+
else if (cpuCores >= 4) {
|
|
15570
|
+
// 中等性能设备(4-7核):使用 2 个 Worker
|
|
15571
|
+
workerNumber = 2;
|
|
15572
|
+
}
|
|
15573
|
+
else {
|
|
15574
|
+
// 低性能设备(< 4核):使用 1 个 Worker
|
|
15575
|
+
workerNumber = 1;
|
|
15576
|
+
}
|
|
15429
15577
|
}
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
cache: 'no-cache',
|
|
15578
|
+
// 限制 workerNumber 范围:1-8(避免过多 Worker 导致资源竞争)
|
|
15579
|
+
workerNumber = Math.max(1, Math.min(8, workerNumber));
|
|
15580
|
+
// 构建 createContext 配置
|
|
15581
|
+
// 参考: https://github.com/qq15725/modern-screenshot/blob/main/src/options.ts
|
|
15582
|
+
const contextOptions = {
|
|
15583
|
+
workerNumber, // Worker 数量,> 0 启用 Worker 模式
|
|
15584
|
+
quality: finalQuality, // 图片质量(0-1),已优化为更低的值以减少 base64 大小
|
|
15585
|
+
fetchFn: handleCrossOriginImage, // 使用代理服务器处理跨域图片
|
|
15586
|
+
fetch: {
|
|
15587
|
+
requestInit: {
|
|
15588
|
+
cache: 'no-cache',
|
|
15589
|
+
},
|
|
15590
|
+
bypassingCache: true,
|
|
15444
15591
|
},
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
|
|
15455
|
-
|
|
15456
|
-
|
|
15457
|
-
if (
|
|
15458
|
-
|
|
15459
|
-
|
|
15592
|
+
// 设置最大 canvas 尺寸,防止生成过大的 canvas(避免内存问题)
|
|
15593
|
+
// 参考: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
|
15594
|
+
// 大多数浏览器限制为 16,777,216 像素(4096x4096),这里设置为更保守的值
|
|
15595
|
+
maximumCanvasSize: 16777216, // 16M 像素(约 4096x4096)
|
|
15596
|
+
// 使用 modern-screenshot 内置的 timeout(更可靠)
|
|
15597
|
+
timeout: Math.max(this.options.interval * 6, 5000),
|
|
15598
|
+
};
|
|
15599
|
+
// 限制 timeout 最多 15 秒
|
|
15600
|
+
contextOptions.timeout = Math.min(contextOptions.timeout, 15000);
|
|
15601
|
+
// 如果用户指定了 workerUrl,使用指定的 URL
|
|
15602
|
+
// 否则让 modern-screenshot 自动处理(它会尝试从 node_modules 或 CDN 加载)
|
|
15603
|
+
// 注意:在某些构建工具(如 Rollup)中,可能需要手动指定 workerUrl
|
|
15604
|
+
if (this.options.workerUrl) {
|
|
15605
|
+
contextOptions.workerUrl = this.options.workerUrl;
|
|
15606
|
+
if (!this.options.silentMode) {
|
|
15607
|
+
console.log(`📸 使用指定的 Worker URL: ${this.options.workerUrl}`);
|
|
15460
15608
|
}
|
|
15461
|
-
|
|
15462
|
-
|
|
15609
|
+
}
|
|
15610
|
+
else {
|
|
15611
|
+
// 未指定 workerUrl 时,modern-screenshot 会自动处理
|
|
15612
|
+
// 但在某些构建环境中可能需要手动指定,可以使用 CDN 作为后备
|
|
15613
|
+
// 这里不设置 workerUrl,让 modern-screenshot 自己处理
|
|
15614
|
+
if (!this.options.silentMode) {
|
|
15615
|
+
console.log('📸 Worker URL 未指定,modern-screenshot 将自动处理');
|
|
15463
15616
|
}
|
|
15464
15617
|
}
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
if (
|
|
15468
|
-
|
|
15618
|
+
// 对所有元素都设置尺寸限制(包括 document.body),避免截图过大
|
|
15619
|
+
// 这样可以减少 base64 大小,提高性能
|
|
15620
|
+
if (finalWidth && finalHeight) {
|
|
15621
|
+
contextOptions.width = finalWidth;
|
|
15622
|
+
contextOptions.height = finalHeight;
|
|
15623
|
+
if (!this.options.silentMode) {
|
|
15624
|
+
if (element === document.body || element === document.documentElement) {
|
|
15625
|
+
console.log(`📸 截取完整页面(document.body),使用压缩尺寸: ${finalWidth}x${finalHeight}`);
|
|
15626
|
+
}
|
|
15627
|
+
else {
|
|
15628
|
+
console.log(`📸 使用压缩尺寸: ${finalWidth}x${finalHeight}`);
|
|
15629
|
+
}
|
|
15630
|
+
}
|
|
15469
15631
|
}
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15474
|
-
contextOptions.scale = isMobile ? 0.7 : this.options.scale; // 移动设备:0.8 -> 0.7
|
|
15475
|
-
}
|
|
15476
|
-
else if (isMobile) {
|
|
15477
|
-
// 如果未指定 scale,移动设备默认使用 0.7
|
|
15478
|
-
contextOptions.scale = 0.7;
|
|
15479
|
-
}
|
|
15480
|
-
// modern-screenshot 会自动处理 worker URL,不需要手动设置 workerUrl
|
|
15481
|
-
// 当 workerNumber > 0 时,截图处理会在 Worker 线程中执行,不会阻塞主线程 UI
|
|
15482
|
-
// 创建 Worker 上下文(每次截图都创建新的,确保元素状态最新)
|
|
15483
|
-
if (!this.options.silentMode) {
|
|
15484
|
-
console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
|
|
15485
|
-
}
|
|
15486
|
-
this.screenshotContext = await createContext$1(element, contextOptions);
|
|
15487
|
-
try {
|
|
15488
|
-
// 使用 Worker 上下文进行截图
|
|
15489
|
-
const dataUrl = await domToPng(this.screenshotContext);
|
|
15490
|
-
// 验证截图结果
|
|
15491
|
-
if (!dataUrl || dataUrl.length < 100) {
|
|
15492
|
-
throw new Error('生成的截图数据无效或过短');
|
|
15632
|
+
else {
|
|
15633
|
+
if (!this.options.silentMode) {
|
|
15634
|
+
console.log(`📸 使用元素实际尺寸: ${elementWidth}x${elementHeight}`);
|
|
15635
|
+
}
|
|
15493
15636
|
}
|
|
15494
|
-
//
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15637
|
+
// 缩放配置:移动设备使用更低的缩放比例,进一步减少图片大小
|
|
15638
|
+
// scale < 1 会降低图片分辨率,减少 base64 大小
|
|
15639
|
+
if (this.options.scale !== 1) {
|
|
15640
|
+
contextOptions.scale = isMobile ? 0.7 : this.options.scale; // 移动设备:0.8 -> 0.7
|
|
15641
|
+
}
|
|
15642
|
+
else if (isMobile) {
|
|
15643
|
+
// 如果未指定 scale,移动设备默认使用 0.7
|
|
15644
|
+
contextOptions.scale = 0.7;
|
|
15645
|
+
}
|
|
15646
|
+
// 优化:复用 context,避免频繁创建和销毁(性能提升 20%+)
|
|
15647
|
+
// 只在元素变化或配置变化时重新创建 context
|
|
15648
|
+
const contextOptionsHash = JSON.stringify({
|
|
15649
|
+
workerNumber,
|
|
15650
|
+
quality: finalQuality,
|
|
15651
|
+
scale: contextOptions.scale,
|
|
15652
|
+
width: contextOptions.width,
|
|
15653
|
+
height: contextOptions.height,
|
|
15654
|
+
maximumCanvasSize: contextOptions.maximumCanvasSize,
|
|
15655
|
+
timeout: contextOptions.timeout
|
|
15656
|
+
});
|
|
15657
|
+
const needsRecreateContext = !this.screenshotContext ||
|
|
15658
|
+
this.contextElement !== element ||
|
|
15659
|
+
this.contextOptionsHash !== contextOptionsHash;
|
|
15660
|
+
if (needsRecreateContext) {
|
|
15661
|
+
if (!this.options.silentMode) {
|
|
15662
|
+
if (this.screenshotContext) {
|
|
15663
|
+
console.log('📸 检测到元素或配置变化,重新创建 context...');
|
|
15664
|
+
}
|
|
15665
|
+
else {
|
|
15666
|
+
console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
|
|
15667
|
+
}
|
|
15501
15668
|
}
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
|
|
15509
|
-
|
|
15510
|
-
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
try {
|
|
15520
|
-
const testCanvas = document.createElement('canvas');
|
|
15521
|
-
testCanvas.width = 1;
|
|
15522
|
-
testCanvas.height = 1;
|
|
15523
|
-
const testDataUrl = testCanvas.toDataURL('image/webp');
|
|
15524
|
-
if (testDataUrl.indexOf('webp') !== -1) {
|
|
15525
|
-
mimeType = 'image/webp';
|
|
15669
|
+
// 销毁旧 context
|
|
15670
|
+
if (this.screenshotContext) {
|
|
15671
|
+
try {
|
|
15672
|
+
destroyContext(this.screenshotContext);
|
|
15673
|
+
}
|
|
15674
|
+
catch (e) {
|
|
15675
|
+
// 忽略清理错误
|
|
15676
|
+
}
|
|
15677
|
+
this.screenshotContext = null;
|
|
15678
|
+
}
|
|
15679
|
+
// 添加 progress 回调(可选,用于显示进度)
|
|
15680
|
+
if (!this.options.silentMode) {
|
|
15681
|
+
contextOptions.progress = (current, total) => {
|
|
15682
|
+
if (total > 0) {
|
|
15683
|
+
const percent = Math.round((current / total) * 100);
|
|
15684
|
+
if (percent % 25 === 0 || current === total) { // 每 25% 或完成时打印
|
|
15685
|
+
console.log(`📸 截图进度: ${current}/${total} (${percent}%)`);
|
|
15526
15686
|
}
|
|
15527
15687
|
}
|
|
15528
|
-
|
|
15529
|
-
|
|
15688
|
+
};
|
|
15689
|
+
}
|
|
15690
|
+
// 添加重试机制创建新 context
|
|
15691
|
+
let retries = 0;
|
|
15692
|
+
const maxRetries = this.options.maxRetries || 2;
|
|
15693
|
+
while (retries <= maxRetries) {
|
|
15694
|
+
try {
|
|
15695
|
+
this.screenshotContext = await createContext$1(element, contextOptions);
|
|
15696
|
+
this.contextElement = element;
|
|
15697
|
+
this.contextOptionsHash = contextOptionsHash;
|
|
15698
|
+
break;
|
|
15699
|
+
}
|
|
15700
|
+
catch (error) {
|
|
15701
|
+
if (retries === maxRetries) {
|
|
15702
|
+
throw new Error(`创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
|
|
15703
|
+
}
|
|
15704
|
+
retries++;
|
|
15705
|
+
const delay = 1000 * retries; // 递增延迟:1秒、2秒...
|
|
15706
|
+
if (!this.options.silentMode) {
|
|
15707
|
+
console.warn(`📸 ⚠️ 创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
|
|
15530
15708
|
}
|
|
15709
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
15531
15710
|
}
|
|
15532
|
-
// 使用优化后的质量进行格式转换
|
|
15533
|
-
convertedDataUrl = mimeType === 'image/png'
|
|
15534
|
-
? canvas.toDataURL(mimeType)
|
|
15535
|
-
: canvas.toDataURL(mimeType, conversionQuality);
|
|
15536
15711
|
}
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
img.onerror = null;
|
|
15542
|
-
canvas.width = 0; // 清空 canvas
|
|
15543
|
-
canvas.height = 0;
|
|
15544
|
-
ctx.clearRect(0, 0, 0, 0); // 清除绘制内容
|
|
15712
|
+
}
|
|
15713
|
+
else {
|
|
15714
|
+
if (!this.options.silentMode) {
|
|
15715
|
+
console.log('📸 复用现有 context(性能优化)');
|
|
15545
15716
|
}
|
|
15546
|
-
return convertedDataUrl;
|
|
15547
15717
|
}
|
|
15548
|
-
|
|
15718
|
+
try {
|
|
15719
|
+
// 根据输出格式选择对应的 API,避免格式转换(性能优化)
|
|
15720
|
+
// 注意:timeout 已经在 createContext 时设置,modern-screenshot 内部会处理超时
|
|
15721
|
+
let dataUrl;
|
|
15722
|
+
const outputFormat = this.options.outputFormat || 'webp';
|
|
15723
|
+
if (!this.options.silentMode) {
|
|
15724
|
+
console.log(`📸 使用 ${outputFormat.toUpperCase()} 格式截图(直接输出,无需转换)...`);
|
|
15725
|
+
}
|
|
15726
|
+
// 根据输出格式选择对应的 API
|
|
15727
|
+
// modern-screenshot 内部已经处理了超时,不需要额外的 Promise.race
|
|
15728
|
+
if (outputFormat === 'webp') {
|
|
15729
|
+
// 使用 domToWebp,直接输出 WebP 格式,无需转换
|
|
15730
|
+
dataUrl = await domToWebp(this.screenshotContext);
|
|
15731
|
+
}
|
|
15732
|
+
else if (outputFormat === 'jpeg') {
|
|
15733
|
+
// 使用 domToJpeg,直接输出 JPEG 格式,无需转换
|
|
15734
|
+
dataUrl = await domToJpeg(this.screenshotContext);
|
|
15735
|
+
}
|
|
15736
|
+
else {
|
|
15737
|
+
// 默认使用 domToPng
|
|
15738
|
+
dataUrl = await domToPng(this.screenshotContext);
|
|
15739
|
+
}
|
|
15740
|
+
// 验证截图结果
|
|
15741
|
+
if (!dataUrl || dataUrl.length < 100) {
|
|
15742
|
+
throw new Error('生成的截图数据无效或过短');
|
|
15743
|
+
}
|
|
15744
|
+
if (!this.options.silentMode) {
|
|
15745
|
+
console.log(`📸 ✅ modern-screenshot 截图成功(${outputFormat.toUpperCase()} 格式,直接输出,无需转换)`);
|
|
15746
|
+
}
|
|
15747
|
+
return dataUrl;
|
|
15748
|
+
}
|
|
15749
|
+
catch (error) {
|
|
15750
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
15751
|
+
if (!this.options.silentMode) {
|
|
15752
|
+
console.error('📸 modern-screenshot 截图失败:', errorMessage);
|
|
15753
|
+
console.error('📸 元素信息:', {
|
|
15754
|
+
width: rect.width,
|
|
15755
|
+
height: rect.height,
|
|
15756
|
+
scrollWidth: element.scrollWidth,
|
|
15757
|
+
scrollHeight: element.scrollHeight,
|
|
15758
|
+
display: window.getComputedStyle(element).display,
|
|
15759
|
+
visibility: window.getComputedStyle(element).visibility
|
|
15760
|
+
});
|
|
15761
|
+
}
|
|
15762
|
+
throw error;
|
|
15763
|
+
}
|
|
15764
|
+
finally {
|
|
15765
|
+
// 优化:不复用 context 时才清理(性能优化)
|
|
15766
|
+
// 如果元素或配置没有变化,保留 context 以便下次复用
|
|
15767
|
+
// 这样可以避免频繁创建和销毁 Worker,提升性能 20%+
|
|
15768
|
+
// 释放截图锁
|
|
15769
|
+
this.isScreenshotInProgress = false;
|
|
15770
|
+
}
|
|
15549
15771
|
}
|
|
15550
15772
|
catch (error) {
|
|
15773
|
+
// 外层错误处理:确保即使发生错误也释放锁
|
|
15551
15774
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
15552
15775
|
if (!this.options.silentMode) {
|
|
15553
|
-
console.error('📸 modern-screenshot
|
|
15554
|
-
console.error('📸 元素信息:', {
|
|
15555
|
-
width: rect.width,
|
|
15556
|
-
height: rect.height,
|
|
15557
|
-
scrollWidth: element.scrollWidth,
|
|
15558
|
-
scrollHeight: element.scrollHeight,
|
|
15559
|
-
display: window.getComputedStyle(element).display,
|
|
15560
|
-
visibility: window.getComputedStyle(element).visibility
|
|
15561
|
-
});
|
|
15776
|
+
console.error('📸 modern-screenshot 截图异常:', errorMessage);
|
|
15562
15777
|
}
|
|
15563
15778
|
throw error;
|
|
15564
15779
|
}
|
|
15565
15780
|
finally {
|
|
15566
|
-
//
|
|
15567
|
-
|
|
15568
|
-
|
|
15569
|
-
try {
|
|
15570
|
-
destroyContext(this.screenshotContext);
|
|
15571
|
-
if (!this.options.silentMode) {
|
|
15572
|
-
console.log('📸 ✅ modern-screenshot context 已清理');
|
|
15573
|
-
}
|
|
15574
|
-
}
|
|
15575
|
-
catch (e) {
|
|
15576
|
-
if (!this.options.silentMode) {
|
|
15577
|
-
console.warn('📸 ⚠️ 清理 context 失败:', e);
|
|
15578
|
-
}
|
|
15579
|
-
}
|
|
15580
|
-
finally {
|
|
15581
|
-
// 确保 context 引用被清除
|
|
15582
|
-
this.screenshotContext = null;
|
|
15583
|
-
}
|
|
15781
|
+
// 确保截图锁被释放(即使发生未捕获的错误)
|
|
15782
|
+
if (this.isScreenshotInProgress) {
|
|
15783
|
+
this.isScreenshotInProgress = false;
|
|
15584
15784
|
}
|
|
15585
|
-
//
|
|
15586
|
-
|
|
15587
|
-
|
|
15785
|
+
// 处理队列中的下一个请求
|
|
15786
|
+
this.processScreenshotQueue();
|
|
15787
|
+
}
|
|
15788
|
+
}
|
|
15789
|
+
/**
|
|
15790
|
+
* 处理截图队列
|
|
15791
|
+
*/
|
|
15792
|
+
async processScreenshotQueue() {
|
|
15793
|
+
if (this.isProcessingQueue || this.screenshotQueue.length === 0) {
|
|
15794
|
+
return;
|
|
15795
|
+
}
|
|
15796
|
+
this.isProcessingQueue = true;
|
|
15797
|
+
while (this.screenshotQueue.length > 0 && !this.isScreenshotInProgress) {
|
|
15798
|
+
const task = this.screenshotQueue.shift();
|
|
15799
|
+
if (task) {
|
|
15588
15800
|
try {
|
|
15589
|
-
|
|
15801
|
+
task.resolve();
|
|
15802
|
+
// 等待当前截图完成
|
|
15803
|
+
while (this.isScreenshotInProgress) {
|
|
15804
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
15805
|
+
}
|
|
15590
15806
|
}
|
|
15591
|
-
catch {
|
|
15592
|
-
|
|
15807
|
+
catch (error) {
|
|
15808
|
+
task.reject(error instanceof Error ? error : new Error(String(error)));
|
|
15593
15809
|
}
|
|
15594
15810
|
}
|
|
15595
15811
|
}
|
|
15812
|
+
this.isProcessingQueue = false;
|
|
15596
15813
|
}
|
|
15597
15814
|
/**
|
|
15598
15815
|
* 预连接代理服务器(优化网络性能)
|
|
@@ -16466,6 +16683,18 @@ class ScreenshotManager {
|
|
|
16466
16683
|
* 清理资源
|
|
16467
16684
|
*/
|
|
16468
16685
|
destroy() {
|
|
16686
|
+
// 清理 modern-screenshot context
|
|
16687
|
+
if (this.screenshotContext) {
|
|
16688
|
+
try {
|
|
16689
|
+
destroyContext(this.screenshotContext);
|
|
16690
|
+
}
|
|
16691
|
+
catch (e) {
|
|
16692
|
+
// 忽略清理错误
|
|
16693
|
+
}
|
|
16694
|
+
this.screenshotContext = null;
|
|
16695
|
+
this.contextElement = null;
|
|
16696
|
+
this.contextOptionsHash = '';
|
|
16697
|
+
}
|
|
16469
16698
|
this.stopScreenshot();
|
|
16470
16699
|
if (this.worker) {
|
|
16471
16700
|
this.worker.terminate();
|