设计器模版底图,一直渲染错误,是因为第一张图变形后内存中图片数据被改了,其他尺码一直错误
2026/5/13 22:27:25 网站建设 项目流程

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询