设计器模版底图,一直渲染错误,是因为第一张图变形后内存中图片数据被改了,其他尺码一直错误

这其实是你们现在更需要的组合:不是只看 decode(),而是再确认"这次 decode 对应的还是当前这张图"。再确认"这次 decode 对应的还是当前这张图" 是怎么做到的,详细列举代码

我直接从现在这次改动的代码里,把"确认图片身份"的完整链路列出来,这样你能看到每一步是怎么校验的。

好,现在我完整列举"确认当前 decode 对应的还是那张图"的全套代码。

完整校验链路

1. 三个基础工具函数

这些函数负责把"图片"这个概念标准化成可比较的身份标记:

// ① 把 URL 规范化为绝对路径(解决相对路径、参数顺序不同等问题)
function normalizeImageLoadKey(src?: null | string) {
const value = String(src || '').trim();
if (!value) return '';
if (typeof window === 'undefined') return value;
try {
return new URL(value, window.location.href).href; // 转成绝对 URL
} catch {
return value;
}
}

// ② 把项目内部路径转成绝对 URL(调用 getFullImageUrl 做一层转换)
function resolveBoundImageLoadKey(src?: null | string) {
return normalizeImageLoadKey(getFullImageUrl(src || ''));
}

// ③ 核心判定:三重校验
function isCurrentLoadedImage(
img: HTMLImageElement, // 当前回调收到的 <img> 标签
rawSrcAtLoad: string, // 加载时记录的原始 src
srcKeyAtLoad: string, // 加载时记录的规范化 URL
currentRawSrc: string, // 现在状态里的 src
currentRef: HTMLImageElement | null, // 现在状态里的 imgRef
) {
return (
currentRef === img && // 校验 ①:是同一个 DOM 元素吗?
currentRawSrc === rawSrcAtLoad && // 校验 ②:状态里的 src 还是加载时的那个吗?
normalizeImageLoadKey(img.currentSrc || img.src) === srcKeyAtLoad // 校验 ③:浏览器里的 URL 还是加载时的那个吗?
);
}

2. **加载时的身份记录**(以 effect 图为例)

在 `onEffectImageLoad` 最开头,立即记录三项身份信息:

async function onEffectImageLoad(sharedState?: PositionSharedState) {
const targetState = sharedState || activeSharedState.value;
if (!targetState || !targetState.effect.imgRef) return;

const img = targetState.effect.imgRef;
const imageAtLoad = String(targetState.effect.image || ''); // 原始 src
const srcKeyAtLoad = resolveBoundImageLoadKey(imageAtLoad); // 规范化 URL

// ... decode ...
try {
await img.decode();
} catch {}

// ❌ 如果这时候图片已经被切走了,就直接返回,不往下执行
if (
!isCurrentLoadedImage(
img,
imageAtLoad,
srcKeyAtLoad,
String(targetState.effect.image || ''), // 取最新的
targetState.effect.imgRef, // 取最新的
)
) {
return; // 这次 decode 的不是当前这张图,不做任何状态改动
}

// ✅ 到这里,确认是当前这张图,才安全改状态
targetState.effect.imgReady = true;
// ...
}

3. **每个异步分支里都要校验**(以 `nextTick` 为例)

在每个 `nextTick`、`ResizeObserver` 里再判一次:

nextTick(() => {

// ❌ 如果已经被新图替换,就不继续

if (

!isCurrentLoadedImage(

img,

imageAtLoad,

srcKeyAtLoad,

String(targetState.effect.image || ''),

targetState.effect.imgRef,

)

) {

return;

}

// ✅ 安全继续

measureEffectImage(targetState);

if (isMeasuredEffectStageReady(targetState)) {

initAllSubAreaLayers(targetState, resolvePositionKeyBySharedState(targetState) || viewPositionKey.value);

switchSubAreaLayers(viewPositionKey.value, viewSubAreaKey.value);

}

// ...

});

4. **ResizeObserver 里也要校验**

const observer = new ResizeObserver(() => {

if (

!isCurrentLoadedImage(

img,

imageAtLoad,

srcKeyAtLoad,

String(targetState.effect.image || ''),

targetState.effect.imgRef,

)

) {

observer.disconnect(); // 新图已经来了,这个 observer 就停掉

return;

}

if (wrapper.clientWidth && wrapper.clientHeight) {

measureEffectImage(targetState);

// ...

}

});

5. **错误处理也要校验**

function onEffectImageError(sharedState?: PositionSharedState, event?: Event) {

const targetState = sharedState || activeSharedState.value;

if (!targetState) return;

const img = event?.target as HTMLImageElement | null;

if (

img &&

!isCurrentLoadedImage(

img,

String(targetState.effect.image || ''),

resolveBoundImageLoadKey(targetState.effect.image || ''),

String(targetState.effect.image || ''),

targetState.effect.imgRef,

)

) {

return; // 错误的是旧图,不用处理

}

// ✅ 确认是当前这张图失败了,才改状态

targetState.effect.imgReady = false;

targetState.effect.imgRect = { left: 0, top: 0, width: 0, height: 0 };

finishEffectPanelLoading(targetState);

}

核心设计思路

  1. **在加载回调开头**,立即锁定三样:`img` DOM、原始 `src`、规范化后的 URL

  2. **在每个异步分支**(`await`、`nextTick`、`ResizeObserver`)前都过一遍 `isCurrentLoadedImage`

  3. **如果发现身份不符**,就早返回,不改任何状态

  4. **如果身份符合**,才继续执行这次回调的逻辑

这样即使有多张图片快速切换、或 decode 还在进行时新图已经替换,每次回调都能精确判断"我现在改的是不是应该改的"。

User: 规范化后的 URL 是什么东西,为什么到对比URL, 底图的src不都是一样的吗

GitHub Copilot: 好问题。看起来一样,但实际上不一样的情况比你想象的多得多,尤其是在这套代码的场景下。

我用具体例子来说明:

为什么 src 看起来一样但实际不同

1. **相对路径 vs 绝对路径**

```javascript

// 状态里存的可能是相对路径

targetState.effect.image = '/api/image/123.jpg'

// 但浏览器实际加载时会转成绝对 URL

img.src = 'https://localhost:56688/api/image/123.jpg'

// 如果直接比 string,它们不相等

'/api/image/123.jpg' === 'https://localhost:56688/api/image/123.jpg' // ❌ false

```

所以需要 `normalizeImageLoadKey` 把两个都转成绝对 URL,才能准确对比。

2. **查询参数变化导致的"新图"**

```javascript

// 时刻 1:底图加载

const imageAtLoad = '/api/image/123.jpg?timestamp=1000'

const srcKeyAtLoad = 'https://localhost:56688/api/image/123.jpg?timestamp=1000'

// ... decode() 还在进行中 ...

// 时刻 2:用户切换了尺码,系统自动重新请求底图

targetState.effect.image = '/api/image/123.jpg?timestamp=2000'

// 这时候 HTML <img> 的 src 会被更新

img.src = 'https://localhost:56688/api/image/123.jpg?timestamp=2000'

// 时刻 3:老的 decode() 终于结束了,回调来执行

if (

!isCurrentLoadedImage(

img, // 当前的 <img>

imageAtLoad, // '/api/image/123.jpg?timestamp=1000'

srcKeyAtLoad, // 'https://localhost:56688/api/image/123.jpg?timestamp=1000'

String(targetState.effect.image || ''), // '/api/image/123.jpg?timestamp=2000'(已经变了)

targetState.effect.imgRef

)

) {

return; // ✅ 发现 src 从 1000 变成了 2000,直接返回,不改状态

}

3. **COS 代理路由的切换**

```javascript

// 开发环境:COS 图通过同源代理访问

const imageAtLoad = '/__cos_image_proxy/user/2026-05-09/base.png'

const srcKeyAtLoad = 'https://localhost:56688/__cos_image_proxy/user/2026-05-09/base.png'

// 生产环境:直接用 COS 原始 URL(可能已经切了环境)

targetState.effect.image = 'https://llkj-prod-erp-1322125355.cos.ap-guangzhou.myqcloud.com/user/2026-05-09/base.png'

// 这两个虽然指向同一张图片,但 URL 完全不同

// 如果不规范化对比,就会误以为是同一张图在加载,结果会把新的状态覆盖了

```

4. **合色流程中的 dataUrl 切换**(最常见的)

```javascript

// 时刻 1:加载原图

const imageAtLoad = '/api/image/123.jpg' // 普通 URL

const srcKeyAtLoad = 'https://localhost:56688/api/image/123.jpg'

// ... 图片加载、decode 完成 ...

// 时刻 2:触发合色,生成 dataUrl

targetState.effect.image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEU...' // ⚠️ 完全不同的格式!

// 时刻 3:如果旧回调(使用 imageAtLoad)还要继续执行,就必须检查

if (

!isCurrentLoadedImage(

img,

imageAtLoad, // '/api/image/123.jpg'

srcKeyAtLoad, // 'https://localhost:56688/api/image/123.jpg'

String(targetState.effect.image || ''), // 'data:image/png;base64,...'(已经切成 dataUrl)

targetState.effect.imgRef

)

) {

return; // ✅ src 从普通 URL 变成 dataUrl,说明合色已经有新结果了,这次旧回调不用继续

}

```

为什么"规范化"很重要

`new URL(value, window.location.href).href` 做的事是:

```javascript

// 输入不同的格式,输出统一的绝对 URL

normalizeImageLoadKey('/api/image/123.jpg?a=1&b=2')

// → 'https://localhost:56688/api/image/123.jpg?a=1\&b=2'

normalizeImageLoadKey('api/image/123.jpg?b=2&a=1')

// → 'https://localhost:56688/api/image/123.jpg?b=2\&a=1'

// 虽然参数顺序不同,但至少都变成了绝对路径,便于后续更精确的对比

实际场景中的流程

// 1️⃣ 用户打开 effect 区,底图开始加载

onEffectImageLoad() {

const img = targetState.effect.imgRef

const imageAtLoad = '/api/effect/123.jpg' // 记录原始

const srcKeyAtLoad = normalizeImageLoadKey(imageAtLoad) // 记录规范化后的

await img.decode() // 假设这需要 500ms

// 2️⃣ 在这 500ms 内,用户快速切换了合色

// targetState.effect.image 现在可能是 'data:image/png;base64,...'

// 3️⃣ 500ms 后,回调执行到这里

if (!isCurrentLoadedImage(

img,

imageAtLoad, // 还是旧的 '/api/effect/123.jpg'

srcKeyAtLoad, // 还是旧的规范化 URL

String(targetState.effect.image || ''), // 现在是新的 dataUrl

targetState.effect.imgRef

)) {

return // ✅ 发现状态已经变了,旧的 decode() 回调就不继续执行

}

// 不走到这里,避免用旧的 imgReady、baseSize、測量结果去覆盖新合色的状态

}

总结

> **底图的 src 不都是一样的** --------- 虽然逻辑上都在显示同一个 position 的图,但实际 URL 可能:

> - 从相对变绝对

> - 从 `timestamp=1000` 变成 `timestamp=2000`

> - 从 COS 代理变成原始 COS URL

> - 从原图 URL 变成合色后的 dataUrl

规范化 URL 就是为了在这些变化中精确捕捉"这次回调对应的还是不是当前这张图",防止旧回调污染新状态。

相关推荐
京东云开发者9 分钟前
正式上线!京东云AI智能渗透测试服务
前端
zzzzzz31014 分钟前
当甲方说'logo放大的同时再缩小一点'时,我用 AI 把这个需求做出来了
javascript·css·程序员
AprChell14 分钟前
低代码设计器和低代码设计引擎架构综述
前端·vue.js·低代码
Hilaku19 分钟前
Node.js 还能再战十年?给你一个不换引擎的理由
前端·javascript·程序员
用户2986985301422 分钟前
Word 文档字符级格式化:Java 实现方案详解
java·后端
颜进强28 分钟前
AI性能参数-截断、延迟与流式输出
前端·后端·ai编程
spmcor35 分钟前
React 架构师之路:Next.js 全栈革命(第八篇)
前端·react.js
英勇无比的消炎药35 分钟前
TinyRobot 源码深度分析:OpenTiny 的 AI 对话组件库
前端·vue.js·github
假如让我当三天老蒯36 分钟前
React基础、进阶(学习用)
前端·react.js·面试
风骏时光牛马38 分钟前
HTML十大经典实战代码案例合集
前端