From e657e37dd47008dafc91a759f555d0642c7e801e Mon Sep 17 00:00:00 2001 From: augushong Date: Thu, 7 May 2026 20:30:34 +0800 Subject: [PATCH] =?UTF-8?q?perf(phone-image):=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=88=AA=E5=9B=BE=E6=95=B0=E6=8D=AE=E9=81=BF=E5=85=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8Dhtml2canvas=E8=B0=83=E7=94=A8=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0render=E5=B9=B6=E5=8F=91=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 convertedBlockCache + simpleHash 缓存已转换的表格/代码块图片 - 首次render截图后缓存,后续调字号/切尺寸/插分页直接复用 - render() 添加 _locked/_pending 并发锁,防止多次渲染同时执行 - insertPageBreak/removePageBreak 加锁检查,渲染中延迟重试 --- public/static/js/phone-image.js | 117 ++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index 4fc9b09..35a448e 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -39,6 +39,25 @@ var PhoneImageEngine = (function () { // ===== 分页结果 ===== var pages = []; + // ===== 截图缓存 ===== + var convertedBlockCache = {}; // key: simpleHash(block.html), value: { imgHtml: '...' } + + /** + * 简单字符串哈希 (djb2 变体) + * @param {string} str + * @returns {string} 哈希字符串 + */ + function simpleHash(str) { + var hash = 0; + if (!str) return '0'; + for (var i = 0; i < str.length; i++) { + var char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // 转32位整数 + } + return '' + hash; + } + /** * 确保隐藏渲染区域存在 */ @@ -101,10 +120,20 @@ var PhoneImageEngine = (function () { /** * 渲染排版预览 - 生成分页后的HTML * 管线: preprocess -> parse -> renderContentFlow -> measure -> paginate -> generate -> renderThumbnails + * 带并发锁:防止多次 render 同时执行造成数据混乱 * @returns {jQuery Deferred} resolves with pages array */ function render() { var deferred = $.Deferred(); + + // 防止并发渲染 + if (render._locked) { + // 标记有待处理的渲染请求,当前渲染完成后会自动重试 + render._pending = true; + return deferred.reject('rendering').promise(); + } + render._locked = true; + pages = []; var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu; var pageHeight = sizeConfig.height; @@ -148,11 +177,21 @@ var PhoneImageEngine = (function () { // 渲染缩略图(右侧)- 异步截图 renderThumbnails(sizeConfig).then(function () { + render._locked = false; + // 如果渲染期间有新的渲染请求排队,自动触发 + if (render._pending) { + render._pending = false; + render(); + } deferred.resolve(pages); }).catch(function (err) { + render._locked = false; deferred.reject(err); }); }); + }).catch(function (err) { + render._locked = false; + deferred.reject(err); }); }); @@ -970,29 +1009,53 @@ var PhoneImageEngine = (function () { /** * 在中间栏 #content-flow 中将表格和代码块截图转为图片 * 在 renderContentFlow(blocks) 之后、measureBlockHeights(blocks) 之前调用 + * 使用 convertedBlockCache 缓存已转换的图片,仅对新增/变更的 block 执行截图 * @param {Array} blocks - parseHtmlToBlocks 返回的块数组 * @returns {jQuery Deferred} resolves with blocks array */ function convertFlowBlocksToImages(blocks) { var deferred = $.Deferred(); var $flow = $('#content-flow'); + var blocksToConvert = []; // 需要重新截图的 block 索引 - function convertNextBlock(blockIdx) { - // 找到下一个需要转换的 block - while (blockIdx < blocks.length) { - if (blocks[blockIdx].type === 'table' || blocks[blockIdx].type === 'pre') break; - blockIdx++; + // 第一遍:检查缓存,命中则直接复用 + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].type !== 'table' && blocks[i].type !== 'pre') continue; + + var cacheKey = simpleHash(blocks[i].html); + if (convertedBlockCache[cacheKey]) { + // 命中缓存 + blocks[i].html = convertedBlockCache[cacheKey].imgHtml; + blocks[i].type = 'img'; + // 更新中间栏显示 + var $cachedEl = $flow.find('.content-flow-block[data-index="' + i + '"]'); + if ($cachedEl.length) $cachedEl.html(blocks[i].html); + } else { + blocksToConvert.push(i); } - if (blockIdx >= blocks.length) { + } + + if (blocksToConvert.length === 0) { + deferred.resolve(blocks); + return deferred.promise(); + } + + // 第二遍:串行截图未缓存的 block + var convertIdx = 0; + + function convertNext() { + if (convertIdx >= blocksToConvert.length) { deferred.resolve(blocks); return; } + var blockIdx = blocksToConvert[convertIdx]; var block = blocks[blockIdx]; - // 在 content-flow 中找到对应的 .content-flow-block var $blockEl = $flow.find('.content-flow-block[data-index="' + blockIdx + '"]'); + if (!$blockEl.length) { - convertNextBlock(blockIdx + 1); + convertIdx++; + convertNext(); return; } @@ -1002,30 +1065,34 @@ var PhoneImageEngine = (function () { $blockEl.find('pre code').each(function () { Prism.highlightElement(this); }); } - // html2canvas 截图 - var targetEl = $blockEl[0]; - html2canvas(targetEl, { + html2canvas($blockEl[0], { scale: 2, useCORS: true, backgroundColor: block.type === 'table' ? '#ffffff' : '#f5f5f5', logging: false }).then(function (canvas) { var imgData = canvas.toDataURL('image/jpeg', 0.92); - // 替换 block 的 html 和 type - block.html = ''; + var imgHtml = ''; + + // 存入缓存(用截图前的原始HTML做key) + var originalHtml = block.html; + var cacheKey = simpleHash(originalHtml); + convertedBlockCache[cacheKey] = { imgHtml: imgHtml }; + + block.html = imgHtml; block.type = 'img'; + $blockEl.html(imgHtml); - // 更新中间栏中的显示 - $blockEl.html(block.html); - - convertNextBlock(blockIdx + 1); + convertIdx++; + convertNext(); }).catch(function () { // 截图失败,保留原样 - convertNextBlock(blockIdx + 1); + convertIdx++; + convertNext(); }); } - convertNextBlock(0); + convertNext(); return deferred.promise(); } @@ -1094,6 +1161,12 @@ var PhoneImageEngine = (function () { * @param {number} blockIndex - 在哪个块之后插入 (对应 break-inserter 的 data-after-index) */ function insertPageBreak(blockIndex) { + // 如果正在渲染,延迟重试 + if (render._locked) { + setTimeout(function () { insertPageBreak(blockIndex); }, 200); + return; + } + var cleanHtml = preprocessContent(postData.content_html); var $temp = $('
').html(cleanHtml); var children = $temp.children(); @@ -1116,6 +1189,12 @@ var PhoneImageEngine = (function () { * @param {number} blockIndex - 分页标记的 data-index */ function removePageBreak(blockIndex) { + // 如果正在渲染,延迟重试 + if (render._locked) { + setTimeout(function () { removePageBreak(blockIndex); }, 200); + return; + } + // 统计这是第几个 page-break var cleanHtml = preprocessContent(postData.content_html); var $temp = $('
').html(cleanHtml);