html2canvas如何使用Service Worker缓存实现离线截图
html2canvas 1.4.1 默认采用iframe沙箱克隆页面结构生成截图,它创建的临时iframe初始地址为about:blank,根据浏览器ServiceWorker的作用域规则,这类非注册域下的文档不会被当前站点的SW接管,因此iframe内发起的所有图片请求不会自动复用SW缓存。即使部分请求通过兜底逻辑命中SW,也会因为iframe加载时序问题、缓存响应缺少匹配的CORS头,导致图片被判定为跨域污染资源,最终不会被绘制到生成的画布上。
按离线场景稳定性从高到低排序:
方案1:预替换图片为base64(离线场景首选,零兼容性问题)
完全绕开iframe内的资源请求逻辑,在调用html2canvas之前,直接从SW缓存读取所有待渲染图片转成base64编码,临时替换页面内图片的src属性,截图完成后再还原原始地址即可。
核心实现代码:
// 从SW缓存读取图片转base64 async function getImageBase64(imgUrl) { const cache = await caches.open('你在Workbox中配置的图片缓存名'); const cachedResp = await cache.match(imgUrl); const imgBlob = cachedResp ? await cachedResp.blob() : await (await fetch(imgUrl)).blob(); return new Promise(resolve => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(imgBlob); }); } // 截图调用方法 async function buildScreenshot(targetDom) { // 收集所有待处理图片,暂存原始地址 const imgList = Array.from(targetDom.querySelectorAll('img')); const srcMap = new Map(); for (const img of imgList) { srcMap.set(img, img.src); img.src = await getImageBase64(img.src); await new Promise(res => img.onload = res); } // 调用html2canvas,关闭不必要的跨域配置 const resCanvas = await html2canvas(targetDom, { useCORS: true, allowTaint: false, removeContainer: true }); // 还原原始图片地址 for (const [img, originSrc] of srcMap) { img.src = originSrc; } return resCanvas; }
这个方案完全不依赖iframe的SW接管逻辑,只要SW缓存里存了对应图片,断网状态下也能100%正常生成截图。
方案2:强制临时iframe受SW接管
如果不想做图片预处理,可以覆写html2canvas内部创建iframe的逻辑,不使用默认的about:blank地址,而是将iframe的src设为当前站点的同源地址(比如当前页面的URL),等iframe加载完成、SW成功接管上下文后再执行DOM克隆和截图逻辑。
注意1.4.1版本需要手动覆写内部的CloneDocument方法,确保iframe加载完成后再执行后续逻辑,同时开启useCORS配置。
方案3:调整Workbox缓存规则适配iframe请求
如果不想修改html2canvas的调用逻辑,可以调整Workbox的路由匹配规则,将iframe上下文发起的图片请求也纳入缓存匹配范围,同时给缓存的响应补充对应CORS头,避免跨域污染问题:
import { registerRoute } from 'workbox-routing'; import { CacheFirst } from 'workbox-strategies'; import { CacheableResponsePlugin } from 'workbox-cacheable-response'; registerRoute( ({ url, request }) => { // 匹配图片API域名下的所有图片请求,包含iframe上下文发起的请求 return url.origin === '你的图片API服务域名' && request.destination === 'image'; }, new CacheFirst({ cacheName: 'api-image-cache', plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), // 给缓存响应补全CORS头,避免画布污染 { cachedResponseWillBeUsed: async ({ cachedResponse }) => { const newHeaders = new Headers(cachedResponse.headers); newHeaders.set('Access-Control-Allow-Origin', self.location.origin); return new Response(cachedResponse.body, { status: cachedResponse.status, headers: newHeaders }); } } ] }) );
这个方案的缺点是依赖浏览器对about:blank iframe的SW接管逻辑,部分旧版本浏览器可能存在兼容问题。
注意:所有方案都需要确保html2canvas的
allowTaint参数设为false,否则跨域图片会导致画布被污染,无法正常导出。
内容的提问来源于stack exchange,提问作者klues

