hookehuyr

perf(PosterBuilder): 优化图片处理和canvas初始化性能

- 引入批量图片处理函数batchGetImageInfo减少网络请求
- 优化图片下载缓存和超时设置
- 减少canvas初始化和生成的延时
- 重构drawImage函数减少重复计算和优化错误处理
......@@ -17,6 +17,7 @@ import {
toRpx,
getRandomId,
getImageInfo,
batchGetImageInfo,
getLinearColor,
} from "./utils/tools"
......@@ -51,24 +52,23 @@ export default defineComponent({
const canvasId = getRandomId()
/**
* step1: 初始化图片资源
* step1: 初始化图片资源(优化版本,使用批量处理)
* @param {Array} images = imgTask
* @return {Promise} downloadImagePromise
*/
const initImages = (images: Image[]) => {
const imagesTemp = images.filter((item) => item.url)
const drawList = imagesTemp.map((item, index) =>
getImageInfo(item, index)
)
return Promise.all(drawList)
// 使用优化的批量处理函数
return batchGetImageInfo ? batchGetImageInfo(imagesTemp) : Promise.all(imagesTemp.map((item, index) => getImageInfo(item, index)))
}
/**
* step2: 初始化 canvas && 获取其 dom 节点和实例
* step2: 初始化 canvas && 获取其 dom 节点和实例(优化版本,减少延时)
* @return {Promise} resolve 里返回其 dom 和实例
*/
const initCanvas = () =>
new Promise<any>((resolve) => {
// 减少延时到100ms,提高响应速度
setTimeout(() => {
const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例
const query = Taro.createSelectorQuery().in(pageInstance) // 确定在当前页面内匹配子元素
......@@ -80,7 +80,7 @@ export default defineComponent({
resolve({ ctx, canvas })
})
.exec()
}, 300)
}, 100)
})
/**
......@@ -201,9 +201,10 @@ export default defineComponent({
ctx.restore() // 恢复之前保存的绘图上下文
}
// 减少延时到150ms,提高生成速度
setTimeout(() => {
getTempFile(canvas) // 需要做延时才能能正常加载图片
}, 300)
}, 150)
}
// start: 初始化 canvas 实例 && 下载图片资源
......
......@@ -460,7 +460,7 @@ export function drawBlock(data, drawOptions) {
}
/**
* @description 渲染图片
* @description 渲染图片(优化版本,减少重复操作)
* @param { object } data
* @param { number } sx - 源图像的矩形选择框的左上角 x 坐标 裁剪
* @param { number } sy - 源图像的矩形选择框的左上角 y 坐标 裁剪
......@@ -496,59 +496,51 @@ export const drawImage = (data, drawOptions) =>
borderRadiusGroup = null
} = data;
// 预先计算转换后的坐标,避免重复调用toPx
const pxSx = toPx(sx);
const pxSy = toPx(sy);
const pxSw = toPx(sw);
const pxSh = toPx(sh);
ctx.save();
if (borderRadius > 0) {
_drawRadiusRect(
{
x,
y,
w,
h,
r: borderRadius
},
drawOptions
);
ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角
ctx.fill();
const img = canvas.createImage(); // 创建图片对象
img.src = imgPath;
img.onload = () => {
ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
if (borderWidth > 0) {
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.stroke();
// 创建图片对象并设置加载完成回调
const img = canvas.createImage();
img.onload = () => {
try {
if (borderRadius > 0) {
_drawRadiusRect({ x, y, w, h, r: borderRadius }, drawOptions);
ctx.clip();
ctx.fill();
ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h);
if (borderWidth > 0) {
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.stroke();
}
} else if (borderRadiusGroup) {
_drawRadiusGroupRect({ x, y, w, h, g: borderRadiusGroup }, drawOptions);
ctx.clip();
ctx.fill();
ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h);
} else {
ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h);
}
resolve();
ctx.restore();
};
} else if (borderRadiusGroup) {
_drawRadiusGroupRect(
{
x,
y,
w,
h,
g: borderRadiusGroup
},
drawOptions
);
ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角
ctx.fill();
const img = canvas.createImage(); // 创建图片对象
img.src = imgPath;
img.onload = () => {
ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
resolve();
} catch (error) {
console.warn('图片绘制失败:', error);
} finally {
ctx.restore();
};
} else {
const img = canvas.createImage(); // 创建图片对象
img.src = imgPath;
img.onload = () => {
ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
resolve();
ctx.restore();
};
}
}
};
// 设置图片加载失败回调
img.onerror = () => {
console.warn('图片加载失败:', imgPath);
ctx.restore();
resolve(); // 即使失败也要resolve,避免阻塞后续绘制
};
// 开始加载图片
img.src = imgPath;
});
......
......@@ -134,46 +134,55 @@ export const toPx = (rpx, factor = getFactor()) =>
export const toRpx = (px, factor = getFactor()) =>
parseInt(String(px / factor), 10);
// 图片缓存对象
const imageCache = new Map<string, string>();
/**
* 下载图片资源
* 下载图片资源(优化版本,支持缓存和更快的超时设置)
* @param { string } url
* @param { number } retryCount - 重试次数
* @returns { Promise }
*/
export function downImage(url: string, retryCount = 2): Promise<string> {
return new Promise<string>((resolve, reject) => {
// 检查缓存
if (imageCache.has(url)) {
resolve(imageCache.get(url)!);
return;
}
// eslint-disable-next-line no-undef
if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) {
// wx.env.USER_DATA_PATH 文件系统中的用户目录路径
const attemptDownload = (attempt = 0) => {
Taro.downloadFile({
url: mapHttpToHttps(url),
timeout: 10000, // 设置10秒超时
timeout: 6000, // 减少超时时间到6秒,提高响应速度
success: (res) => {
if (res.statusCode === 200) {
// 缓存下载结果
imageCache.set(url, res.tempFilePath as string);
resolve(res.tempFilePath as string);
} else {
// console.log('下载失败', res);
if (attempt < retryCount) {
// console.log(`重试下载图片,第${attempt + 1}次重试`);
setTimeout(() => attemptDownload(attempt + 1), 1000);
// 减少重试延时到500ms
setTimeout(() => attemptDownload(attempt + 1), 500);
} else {
reject(res);
}
}
},
fail(err) {
// console.log('下载失败了', err);
// 如果是代理连接问题,尝试使用原始URL
if (err.errMsg && err.errMsg.includes('127.0.0.1:7890')) {
// console.log('检测到代理问题,尝试使用原始URL');
if (attempt < retryCount) {
setTimeout(() => {
Taro.downloadFile({
url: url, // 使用原始URL,不转换https
timeout: 10000,
timeout: 6000,
success: (res) => {
if (res.statusCode === 200) {
imageCache.set(url, res.tempFilePath as string);
resolve(res.tempFilePath as string);
} else {
reject(res);
......@@ -187,13 +196,12 @@ export function downImage(url: string, retryCount = 2): Promise<string> {
}
}
});
}, 1000);
}, 500);
} else {
reject(err);
}
} else if (attempt < retryCount) {
// console.log(`重试下载图片,第${attempt + 1}次重试`);
setTimeout(() => attemptDownload(attempt + 1), 1000);
setTimeout(() => attemptDownload(attempt + 1), 500);
} else {
reject(err);
}
......@@ -208,7 +216,35 @@ export function downImage(url: string, retryCount = 2): Promise<string> {
}
/**
* 下载图片并获取图片信息
* 批量下载图片并获取图片信息(优化版本,支持并发下载)
* @param {Array} images 图片数组
* @returns { Promise } result 整理后的图片信息数组
*/
export const batchGetImageInfo = (images) => {
// 限制并发数量,避免过多请求导致性能问题
const concurrencyLimit = 3;
const chunks = [];
// 将图片数组分块处理
for (let i = 0; i < images.length; i += concurrencyLimit) {
chunks.push(images.slice(i, i + concurrencyLimit));
}
// 串行处理每个块,块内并行处理
return chunks.reduce((promise, chunk) => {
return promise.then(results => {
const chunkPromises = chunk.map((item, index) =>
getImageInfo(item, results.length + index)
);
return Promise.all(chunkPromises).then(chunkResults =>
results.concat(chunkResults)
);
});
}, Promise.resolve([]));
};
/**
* 下载图片并获取图片信息(优化版本)
* @param {} item 图片参数信息
* @param {} index 图片下标
* @returns { Promise } result 整理后的图片信息
......@@ -216,32 +252,44 @@ export function downImage(url: string, retryCount = 2): Promise<string> {
export const getImageInfo = (item, index) =>
new Promise((resolve, reject) => {
const { x, y, width, height, url, zIndex } = item;
downImage(url).then((imgPath) =>
// 预先计算一些固定值,避免重复计算
const borderRadius = item.borderRadius || 0;
const borderWidth = item.borderWidth || 0;
const borderColor = item.borderColor;
const borderRadiusGroup = item.borderRadiusGroup;
const itemZIndex = typeof zIndex !== 'undefined' ? zIndex : index;
downImage(url).then((imgPath) => {
// 使用更快的图片信息获取方式
Taro.getImageInfo({ src: imgPath })
.then((imgInfo) => {
// 获取图片信息
// 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形, 即宽高比不变,截取再拉伸
let sx; // 截图的起点 x 坐标
let sy; // 截图的起点 y 坐标
const borderRadius = item.borderRadius || 0;
const imgWidth = toRpx(imgInfo.width); // 图片真实宽度 单位 px
const imgHeight = toRpx(imgInfo.height); // 图片真实高度 单位 px
// 根据宽高比截取图片
if (imgWidth / imgHeight <= width / height) {
// 优化计算逻辑,减少重复计算
const aspectRatio = imgWidth / imgHeight;
const targetRatio = width / height;
let sx, sy; // 截图的起点坐标
if (aspectRatio <= targetRatio) {
sx = 0;
sy = (imgHeight - (imgWidth / width) * height) / 2;
} else {
sy = 0;
sx = (imgWidth - (imgHeight / height) * width) / 2;
}
// 给 canvas 画图准备参数,详见 ./draw.ts-drawImage
const result = {
type: 'image',
borderRadius,
borderWidth: item.borderWidth,
borderColor: item.borderColor,
borderRadiusGroup: item.borderRadiusGroup,
zIndex: typeof zIndex !== 'undefined' ? zIndex : index,
borderWidth,
borderColor,
borderRadiusGroup,
zIndex: itemZIndex,
imgPath: imgPath, // 使用下载后的临时文件路径
sx,
sy,
......@@ -255,10 +303,9 @@ export const getImageInfo = (item, index) =>
resolve(result);
})
.catch((err) => {
// console.log('读取图片信息失败', err);
reject(err);
})
);
});
}).catch(reject);
});
/**
......