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

这其实是你们现在更需要的组合:不是只看 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 就是为了在这些变化中精确捕捉"这次回调对应的还是不是当前这张图",防止旧回调污染新状态。

相关推荐
Mike117.1 小时前
GBase 8c 会话、锁等待和长 SQL 的日常巡检写法
java·数据库·sql
逻辑驱动的ken1 小时前
Java高频面试考点场景题28
java·开发语言·面试·职场和发展·求职招聘
user_admin_god1 小时前
Spring Boot 3 + WebFlux 企业级流式SSE接口最佳实践
java·spring boot·后端
小粉粉hhh1 小时前
Node.js(四)——npm与包
前端·npm·node.js
雨落在了我的手上1 小时前
初识java(四):程序逻辑控制
java·开发语言·前端
UXbot1 小时前
Vibecoding 工具如何一次性生成 Web + iOS + Android 三端 APP?功能架构深度解读
android·前端·ui·ios·交互·软件构建·ai编程
她说彩礼65万1 小时前
C# WIFI连接状态检测方法
java·spring·c#
_Evan_Yao1 小时前
责任链模式在Agent编排中的应用:让AI Agent学会“踢皮球”
java·人工智能·后端·责任链模式
小周技术驿站1 小时前
Linux 基础命令详解
linux·前端·chrome·ubuntu·centos