perf(PosterBuilder): 优化图片处理和canvas初始化性能
- 引入批量图片处理函数batchGetImageInfo减少网络请求 - 优化图片下载缓存和超时设置 - 减少canvas初始化和生成的延时 - 重构drawImage函数减少重复计算和优化错误处理
Showing
3 changed files
with
115 additions
and
75 deletions
| ... | @@ -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(); |
| 506 | + | ||
| 507 | + // 创建图片对象并设置加载完成回调 | ||
| 508 | + const img = canvas.createImage(); | ||
| 509 | + img.onload = () => { | ||
| 510 | + try { | ||
| 500 | if (borderRadius > 0) { | 511 | if (borderRadius > 0) { |
| 501 | - _drawRadiusRect( | 512 | + _drawRadiusRect({ x, y, w, h, r: borderRadius }, drawOptions); |
| 502 | - { | 513 | + ctx.clip(); |
| 503 | - x, | ||
| 504 | - y, | ||
| 505 | - w, | ||
| 506 | - h, | ||
| 507 | - r: borderRadius | ||
| 508 | - }, | ||
| 509 | - drawOptions | ||
| 510 | - ); | ||
| 511 | - ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角 | ||
| 512 | ctx.fill(); | 514 | ctx.fill(); |
| 513 | - const img = canvas.createImage(); // 创建图片对象 | 515 | + ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h); |
| 514 | - img.src = imgPath; | ||
| 515 | - img.onload = () => { | ||
| 516 | - ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h); | ||
| 517 | if (borderWidth > 0) { | 516 | if (borderWidth > 0) { |
| 518 | ctx.strokeStyle = borderColor; | 517 | ctx.strokeStyle = borderColor; |
| 519 | ctx.lineWidth = borderWidth; | 518 | ctx.lineWidth = borderWidth; |
| 520 | ctx.stroke(); | 519 | ctx.stroke(); |
| 521 | } | 520 | } |
| 522 | - resolve(); | ||
| 523 | - ctx.restore(); | ||
| 524 | - }; | ||
| 525 | } else if (borderRadiusGroup) { | 521 | } else if (borderRadiusGroup) { |
| 526 | - _drawRadiusGroupRect( | 522 | + _drawRadiusGroupRect({ x, y, w, h, g: borderRadiusGroup }, drawOptions); |
| 527 | - { | 523 | + ctx.clip(); |
| 528 | - x, | ||
| 529 | - y, | ||
| 530 | - w, | ||
| 531 | - h, | ||
| 532 | - g: borderRadiusGroup | ||
| 533 | - }, | ||
| 534 | - drawOptions | ||
| 535 | - ); | ||
| 536 | - ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角 | ||
| 537 | ctx.fill(); | 524 | ctx.fill(); |
| 538 | - const img = canvas.createImage(); // 创建图片对象 | 525 | + ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h); |
| 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(); | ||
| 544 | - }; | ||
| 545 | } else { | 526 | } else { |
| 546 | - const img = canvas.createImage(); // 创建图片对象 | 527 | + ctx.drawImage(img, pxSx, pxSy, pxSw, pxSh, x, y, w, h); |
| 547 | - img.src = imgPath; | 528 | + } |
| 548 | - img.onload = () => { | 529 | + } catch (error) { |
| 549 | - ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h); | 530 | + console.warn('图片绘制失败:', error); |
| 531 | + } finally { | ||
| 532 | + ctx.restore(); | ||
| 550 | resolve(); | 533 | resolve(); |
| 534 | + } | ||
| 535 | + }; | ||
| 536 | + | ||
| 537 | + // 设置图片加载失败回调 | ||
| 538 | + img.onerror = () => { | ||
| 539 | + console.warn('图片加载失败:', imgPath); | ||
| 551 | ctx.restore(); | 540 | ctx.restore(); |
| 541 | + resolve(); // 即使失败也要resolve,避免阻塞后续绘制 | ||
| 552 | }; | 542 | }; |
| 553 | - } | 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 | /** | ... | ... |
-
Please register or login to post a comment