perf(phone-image): 缓存截图数据避免重复html2canvas调用,添加render并发锁

- 新增 convertedBlockCache + simpleHash 缓存已转换的表格/代码块图片
- 首次render截图后缓存,后续调字号/切尺寸/插分页直接复用
- render() 添加 _locked/_pending 并发锁,防止多次渲染同时执行
- insertPageBreak/removePageBreak 加锁检查,渲染中延迟重试
This commit is contained in:
augushong
2026-05-07 20:30:34 +08:00
parent 404f4d8a22
commit e657e37dd4

View File

@@ -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 = '<img src="' + imgData + '" data-converted="true" style="max-width:100%;height:auto;display:block;margin:10px 0;" />';
var imgHtml = '<img src="' + imgData + '" data-converted="true" style="max-width:100%;height:auto;display:block;margin:10px 0;" />';
// 存入缓存用截图前的原始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 = $('<div>').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 = $('<div>').html(cleanHtml);