hookehuyr

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

- 引入批量图片处理函数batchGetImageInfo减少网络请求
- 优化图片下载缓存和超时设置
- 减少canvas初始化和生成的延时
- 重构drawImage函数减少重复计算和优化错误处理
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
17 toRpx, 17 toRpx,
18 getRandomId, 18 getRandomId,
19 getImageInfo, 19 getImageInfo,
20 + batchGetImageInfo,
20 getLinearColor, 21 getLinearColor,
21 } from "./utils/tools" 22 } from "./utils/tools"
22 23
...@@ -51,24 +52,23 @@ export default defineComponent({ ...@@ -51,24 +52,23 @@ export default defineComponent({
51 const canvasId = getRandomId() 52 const canvasId = getRandomId()
52 53
53 /** 54 /**
54 - * step1: 初始化图片资源 55 + * step1: 初始化图片资源(优化版本,使用批量处理)
55 * @param {Array} images = imgTask 56 * @param {Array} images = imgTask
56 * @return {Promise} downloadImagePromise 57 * @return {Promise} downloadImagePromise
57 */ 58 */
58 const initImages = (images: Image[]) => { 59 const initImages = (images: Image[]) => {
59 const imagesTemp = images.filter((item) => item.url) 60 const imagesTemp = images.filter((item) => item.url)
60 - const drawList = imagesTemp.map((item, index) => 61 + // 使用优化的批量处理函数
61 - getImageInfo(item, index) 62 + return batchGetImageInfo ? batchGetImageInfo(imagesTemp) : Promise.all(imagesTemp.map((item, index) => getImageInfo(item, index)))
62 - )
63 - return Promise.all(drawList)
64 } 63 }
65 64
66 /** 65 /**
67 - * step2: 初始化 canvas && 获取其 dom 节点和实例 66 + * step2: 初始化 canvas && 获取其 dom 节点和实例(优化版本,减少延时)
68 * @return {Promise} resolve 里返回其 dom 和实例 67 * @return {Promise} resolve 里返回其 dom 和实例
69 */ 68 */
70 const initCanvas = () => 69 const initCanvas = () =>
71 new Promise<any>((resolve) => { 70 new Promise<any>((resolve) => {
71 + // 减少延时到100ms,提高响应速度
72 setTimeout(() => { 72 setTimeout(() => {
73 const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例 73 const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例
74 const query = Taro.createSelectorQuery().in(pageInstance) // 确定在当前页面内匹配子元素 74 const query = Taro.createSelectorQuery().in(pageInstance) // 确定在当前页面内匹配子元素
...@@ -80,7 +80,7 @@ export default defineComponent({ ...@@ -80,7 +80,7 @@ export default defineComponent({
80 resolve({ ctx, canvas }) 80 resolve({ ctx, canvas })
81 }) 81 })
82 .exec() 82 .exec()
83 - }, 300) 83 + }, 100)
84 }) 84 })
85 85
86 /** 86 /**
...@@ -201,9 +201,10 @@ export default defineComponent({ ...@@ -201,9 +201,10 @@ export default defineComponent({
201 ctx.restore() // 恢复之前保存的绘图上下文 201 ctx.restore() // 恢复之前保存的绘图上下文
202 } 202 }
203 203
204 + // 减少延时到150ms,提高生成速度
204 setTimeout(() => { 205 setTimeout(() => {
205 getTempFile(canvas) // 需要做延时才能能正常加载图片 206 getTempFile(canvas) // 需要做延时才能能正常加载图片
206 - }, 300) 207 + }, 150)
207 } 208 }
208 209
209 // start: 初始化 canvas 实例 && 下载图片资源 210 // start: 初始化 canvas 实例 && 下载图片资源
......
...@@ -460,7 +460,7 @@ export function drawBlock(data, drawOptions) { ...@@ -460,7 +460,7 @@ export function drawBlock(data, drawOptions) {
460 } 460 }
461 461
462 /** 462 /**
463 - * @description 渲染图片 463 + * @description 渲染图片(优化版本,减少重复操作)
464 * @param { object } data 464 * @param { object } data
465 * @param { number } sx - 源图像的矩形选择框的左上角 x 坐标 裁剪 465 * @param { number } sx - 源图像的矩形选择框的左上角 x 坐标 裁剪
466 * @param { number } sy - 源图像的矩形选择框的左上角 y 坐标 裁剪 466 * @param { number } sy - 源图像的矩形选择框的左上角 y 坐标 裁剪
...@@ -496,59 +496,51 @@ export const drawImage = (data, drawOptions) => ...@@ -496,59 +496,51 @@ export const drawImage = (data, drawOptions) =>
496 borderRadiusGroup = null 496 borderRadiusGroup = null
497 } = data; 497 } = data;
498 498
499 + // 预先计算转换后的坐标,避免重复调用toPx
500 + const pxSx = toPx(sx);
501 + const pxSy = toPx(sy);
502 + const pxSw = toPx(sw);
503 + const pxSh = toPx(sh);
504 +
499 ctx.save(); 505 ctx.save();
500 - if (borderRadius > 0) { 506 +
501 - _drawRadiusRect( 507 + // 创建图片对象并设置加载完成回调
502 - { 508 + const img = canvas.createImage();
503 - x, 509 + img.onload = () => {
504 - y, 510 + try {
505 - w, 511 + if (borderRadius > 0) {
506 - h, 512 + _drawRadiusRect({ x, y, w, h, r: borderRadius }, drawOptions);
507 - r: borderRadius 513 + ctx.clip();
508 - }, 514 + ctx.fill();
509 - drawOptions 515 + ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h);
510 - ); 516 + if (borderWidth > 0) {
511 - ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角 517 + ctx.strokeStyle = borderColor;
512 - ctx.fill(); 518 + ctx.lineWidth = borderWidth;
513 - const img = canvas.createImage(); // 创建图片对象 519 + ctx.stroke();
514 - img.src = imgPath; 520 + }
515 - img.onload = () => { 521 + } else if (borderRadiusGroup) {
516 - ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h); 522 + _drawRadiusGroupRect({ x, y, w, h, g: borderRadiusGroup }, drawOptions);
517 - if (borderWidth > 0) { 523 + ctx.clip();
518 - ctx.strokeStyle = borderColor; 524 + ctx.fill();
519 - ctx.lineWidth = borderWidth; 525 + ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h);
520 - ctx.stroke(); 526 + } else {
527 + ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h);
521 } 528 }
522 - resolve(); 529 + } catch (error) {
523 - ctx.restore(); 530 + console.warn('图片绘制失败:', error);
524 - }; 531 + } finally {
525 - } else if (borderRadiusGroup) {
526 - _drawRadiusGroupRect(
527 - {
528 - x,
529 - y,
530 - w,
531 - h,
532 - g: borderRadiusGroup
533 - },
534 - drawOptions
535 - );
536 - ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角
537 - ctx.fill();
538 - const img = canvas.createImage(); // 创建图片对象
539 - img.src = imgPath;
540 - img.onload = () => {
541 - ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
542 - resolve();
543 ctx.restore(); 532 ctx.restore();
544 - };
545 - } else {
546 - const img = canvas.createImage(); // 创建图片对象
547 - img.src = imgPath;
548 - img.onload = () => {
549 - ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
550 resolve(); 533 resolve();
551 - ctx.restore(); 534 + }
552 - }; 535 + };
553 - } 536 +
537 + // 设置图片加载失败回调
538 + img.onerror = () => {
539 + console.warn('图片加载失败:', imgPath);
540 + ctx.restore();
541 + resolve(); // 即使失败也要resolve,避免阻塞后续绘制
542 + };
543 +
544 + // 开始加载图片
545 + img.src = imgPath;
554 }); 546 });
......
...@@ -134,46 +134,55 @@ export const toPx = (rpx, factor = getFactor()) => ...@@ -134,46 +134,55 @@ export const toPx = (rpx, factor = getFactor()) =>
134 export const toRpx = (px, factor = getFactor()) => 134 export const toRpx = (px, factor = getFactor()) =>
135 parseInt(String(px / factor), 10); 135 parseInt(String(px / factor), 10);
136 136
137 +// 图片缓存对象
138 +const imageCache = new Map<string, string>();
139 +
137 /** 140 /**
138 - * 下载图片资源 141 + * 下载图片资源(优化版本,支持缓存和更快的超时设置)
139 * @param { string } url 142 * @param { string } url
140 * @param { number } retryCount - 重试次数 143 * @param { number } retryCount - 重试次数
141 * @returns { Promise } 144 * @returns { Promise }
142 */ 145 */
143 export function downImage(url: string, retryCount = 2): Promise<string> { 146 export function downImage(url: string, retryCount = 2): Promise<string> {
144 return new Promise<string>((resolve, reject) => { 147 return new Promise<string>((resolve, reject) => {
148 + // 检查缓存
149 + if (imageCache.has(url)) {
150 + resolve(imageCache.get(url)!);
151 + return;
152 + }
153 +
145 // eslint-disable-next-line no-undef 154 // eslint-disable-next-line no-undef
146 if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) { 155 if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) {
147 // wx.env.USER_DATA_PATH 文件系统中的用户目录路径 156 // wx.env.USER_DATA_PATH 文件系统中的用户目录路径
148 const attemptDownload = (attempt = 0) => { 157 const attemptDownload = (attempt = 0) => {
149 Taro.downloadFile({ 158 Taro.downloadFile({
150 url: mapHttpToHttps(url), 159 url: mapHttpToHttps(url),
151 - timeout: 10000, // 设置10秒超时 160 + timeout: 6000, // 减少超时时间到6秒,提高响应速度
152 success: (res) => { 161 success: (res) => {
153 if (res.statusCode === 200) { 162 if (res.statusCode === 200) {
163 + // 缓存下载结果
164 + imageCache.set(url, res.tempFilePath as string);
154 resolve(res.tempFilePath as string); 165 resolve(res.tempFilePath as string);
155 } else { 166 } else {
156 - // console.log('下载失败', res);
157 if (attempt < retryCount) { 167 if (attempt < retryCount) {
158 - // console.log(`重试下载图片,第${attempt + 1}次重试`); 168 + // 减少重试延时到500ms
159 - setTimeout(() => attemptDownload(attempt + 1), 1000); 169 + setTimeout(() => attemptDownload(attempt + 1), 500);
160 } else { 170 } else {
161 reject(res); 171 reject(res);
162 } 172 }
163 } 173 }
164 }, 174 },
165 fail(err) { 175 fail(err) {
166 - // console.log('下载失败了', err);
167 // 如果是代理连接问题,尝试使用原始URL 176 // 如果是代理连接问题,尝试使用原始URL
168 if (err.errMsg && err.errMsg.includes('127.0.0.1:7890')) { 177 if (err.errMsg && err.errMsg.includes('127.0.0.1:7890')) {
169 - // console.log('检测到代理问题,尝试使用原始URL');
170 if (attempt < retryCount) { 178 if (attempt < retryCount) {
171 setTimeout(() => { 179 setTimeout(() => {
172 Taro.downloadFile({ 180 Taro.downloadFile({
173 url: url, // 使用原始URL,不转换https 181 url: url, // 使用原始URL,不转换https
174 - timeout: 10000, 182 + timeout: 6000,
175 success: (res) => { 183 success: (res) => {
176 if (res.statusCode === 200) { 184 if (res.statusCode === 200) {
185 + imageCache.set(url, res.tempFilePath as string);
177 resolve(res.tempFilePath as string); 186 resolve(res.tempFilePath as string);
178 } else { 187 } else {
179 reject(res); 188 reject(res);
...@@ -187,13 +196,12 @@ export function downImage(url: string, retryCount = 2): Promise<string> { ...@@ -187,13 +196,12 @@ export function downImage(url: string, retryCount = 2): Promise<string> {
187 } 196 }
188 } 197 }
189 }); 198 });
190 - }, 1000); 199 + }, 500);
191 } else { 200 } else {
192 reject(err); 201 reject(err);
193 } 202 }
194 } else if (attempt < retryCount) { 203 } else if (attempt < retryCount) {
195 - // console.log(`重试下载图片,第${attempt + 1}次重试`); 204 + setTimeout(() => attemptDownload(attempt + 1), 500);
196 - setTimeout(() => attemptDownload(attempt + 1), 1000);
197 } else { 205 } else {
198 reject(err); 206 reject(err);
199 } 207 }
...@@ -208,7 +216,35 @@ export function downImage(url: string, retryCount = 2): Promise<string> { ...@@ -208,7 +216,35 @@ export function downImage(url: string, retryCount = 2): Promise<string> {
208 } 216 }
209 217
210 /** 218 /**
211 - * 下载图片并获取图片信息 219 + * 批量下载图片并获取图片信息(优化版本,支持并发下载)
220 + * @param {Array} images 图片数组
221 + * @returns { Promise } result 整理后的图片信息数组
222 + */
223 +export const batchGetImageInfo = (images) => {
224 + // 限制并发数量,避免过多请求导致性能问题
225 + const concurrencyLimit = 3;
226 + const chunks = [];
227 +
228 + // 将图片数组分块处理
229 + for (let i = 0; i < images.length; i += concurrencyLimit) {
230 + chunks.push(images.slice(i, i + concurrencyLimit));
231 + }
232 +
233 + // 串行处理每个块,块内并行处理
234 + return chunks.reduce((promise, chunk) => {
235 + return promise.then(results => {
236 + const chunkPromises = chunk.map((item, index) =>
237 + getImageInfo(item, results.length + index)
238 + );
239 + return Promise.all(chunkPromises).then(chunkResults =>
240 + results.concat(chunkResults)
241 + );
242 + });
243 + }, Promise.resolve([]));
244 +};
245 +
246 +/**
247 + * 下载图片并获取图片信息(优化版本)
212 * @param {} item 图片参数信息 248 * @param {} item 图片参数信息
213 * @param {} index 图片下标 249 * @param {} index 图片下标
214 * @returns { Promise } result 整理后的图片信息 250 * @returns { Promise } result 整理后的图片信息
...@@ -216,32 +252,44 @@ export function downImage(url: string, retryCount = 2): Promise<string> { ...@@ -216,32 +252,44 @@ export function downImage(url: string, retryCount = 2): Promise<string> {
216 export const getImageInfo = (item, index) => 252 export const getImageInfo = (item, index) =>
217 new Promise((resolve, reject) => { 253 new Promise((resolve, reject) => {
218 const { x, y, width, height, url, zIndex } = item; 254 const { x, y, width, height, url, zIndex } = item;
219 - downImage(url).then((imgPath) => 255 +
256 + // 预先计算一些固定值,避免重复计算
257 + const borderRadius = item.borderRadius || 0;
258 + const borderWidth = item.borderWidth || 0;
259 + const borderColor = item.borderColor;
260 + const borderRadiusGroup = item.borderRadiusGroup;
261 + const itemZIndex = typeof zIndex !== 'undefined' ? zIndex : index;
262 +
263 + downImage(url).then((imgPath) => {
264 + // 使用更快的图片信息获取方式
220 Taro.getImageInfo({ src: imgPath }) 265 Taro.getImageInfo({ src: imgPath })
221 .then((imgInfo) => { 266 .then((imgInfo) => {
222 // 获取图片信息 267 // 获取图片信息
223 // 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形, 即宽高比不变,截取再拉伸 268 // 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形, 即宽高比不变,截取再拉伸
224 - let sx; // 截图的起点 x 坐标
225 - let sy; // 截图的起点 y 坐标
226 - const borderRadius = item.borderRadius || 0;
227 const imgWidth = toRpx(imgInfo.width); // 图片真实宽度 单位 px 269 const imgWidth = toRpx(imgInfo.width); // 图片真实宽度 单位 px
228 const imgHeight = toRpx(imgInfo.height); // 图片真实高度 单位 px 270 const imgHeight = toRpx(imgInfo.height); // 图片真实高度 单位 px
229 - // 根据宽高比截取图片 271 +
230 - if (imgWidth / imgHeight <= width / height) { 272 + // 优化计算逻辑,减少重复计算
273 + const aspectRatio = imgWidth / imgHeight;
274 + const targetRatio = width / height;
275 +
276 + let sx, sy; // 截图的起点坐标
277 + if (aspectRatio <= targetRatio) {
231 sx = 0; 278 sx = 0;
232 sy = (imgHeight - (imgWidth / width) * height) / 2; 279 sy = (imgHeight - (imgWidth / width) * height) / 2;
233 } else { 280 } else {
234 sy = 0; 281 sy = 0;
235 sx = (imgWidth - (imgHeight / height) * width) / 2; 282 sx = (imgWidth - (imgHeight / height) * width) / 2;
236 } 283 }
284 +
237 // 给 canvas 画图准备参数,详见 ./draw.ts-drawImage 285 // 给 canvas 画图准备参数,详见 ./draw.ts-drawImage
238 const result = { 286 const result = {
239 type: 'image', 287 type: 'image',
240 borderRadius, 288 borderRadius,
241 - borderWidth: item.borderWidth, 289 + borderWidth,
242 - borderColor: item.borderColor, 290 + borderColor,
243 - borderRadiusGroup: item.borderRadiusGroup, 291 + borderRadiusGroup,
244 - zIndex: typeof zIndex !== 'undefined' ? zIndex : index, 292 + zIndex: itemZIndex,
245 imgPath: imgPath, // 使用下载后的临时文件路径 293 imgPath: imgPath, // 使用下载后的临时文件路径
246 sx, 294 sx,
247 sy, 295 sy,
...@@ -255,10 +303,9 @@ export const getImageInfo = (item, index) => ...@@ -255,10 +303,9 @@ export const getImageInfo = (item, index) =>
255 resolve(result); 303 resolve(result);
256 }) 304 })
257 .catch((err) => { 305 .catch((err) => {
258 - // console.log('读取图片信息失败', err);
259 reject(err); 306 reject(err);
260 - }) 307 + });
261 - ); 308 + }).catch(reject);
262 }); 309 });
263 310
264 /** 311 /**
......