mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-01 16:22:49 +08:00
perf(phone-image): 缓存截图数据避免重复html2canvas调用,添加render并发锁
- 新增 convertedBlockCache + simpleHash 缓存已转换的表格/代码块图片 - 首次render截图后缓存,后续调字号/切尺寸/插分页直接复用 - render() 添加 _locked/_pending 并发锁,防止多次渲染同时执行 - insertPageBreak/removePageBreak 加锁检查,渲染中延迟重试
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user