发布/编辑媒体在覆盖与卸载时释放本地预览 blob URL
- applyVersionMediaDefaults 用线上默认值覆盖前先 revoke 被替换的本地预览 - GamePublishPage / GameWorkMetadataEditor 用 ref 跟踪最新图片,卸载时兜底 revoke - 补「线上默认值覆盖本地选择」的 revoke 断言测试
This commit is contained in:
@@ -28,7 +28,10 @@ import {
|
||||
parseGameTagsInput,
|
||||
resolveGameInputModes,
|
||||
} from './gameMetadataOptions';
|
||||
import { type GamePublishImage } from './gamePublishAssets';
|
||||
import {
|
||||
type GamePublishImage,
|
||||
revokeGamePublishImagePreview,
|
||||
} from './gamePublishAssets';
|
||||
import { GamePublishClientGuide } from './GamePublishClientGuide';
|
||||
import {
|
||||
clearPublishDraft,
|
||||
@@ -99,6 +102,27 @@ export function GamePublishPage({
|
||||
const [screenshotImages, setScreenshotImages] = useState<GamePublishImage[]>(
|
||||
[],
|
||||
);
|
||||
/**
|
||||
* 卸载时释放仍挂在界面上的本地预览 blob URL。回调里拿不到最新 state,用 ref 跟踪;显式
|
||||
* 移除/替换的那一份已由 fieldset 处理器 revoke,这里兜住卸载与线上默认值整体覆盖。
|
||||
*/
|
||||
const coverImageRef = useRef<GamePublishImage | null>(null);
|
||||
const screenshotImagesRef = useRef<GamePublishImage[]>([]);
|
||||
useEffect(() => {
|
||||
coverImageRef.current = coverImage;
|
||||
}, [coverImage]);
|
||||
useEffect(() => {
|
||||
screenshotImagesRef.current = screenshotImages;
|
||||
}, [screenshotImages]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
revokeGamePublishImagePreview(coverImageRef.current);
|
||||
for (const image of screenshotImagesRef.current) {
|
||||
revokeGamePublishImagePreview(image);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
// 发布灰度:`checking` 期间不渲染表单,避免白名单外的作者先上传再被后端拒绝。
|
||||
const [publishGate, setPublishGate] = useState<
|
||||
'checking' | 'allowed' | 'blocked'
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
parseGameTagsInput,
|
||||
resolveGameInputModes,
|
||||
} from './gameMetadataOptions';
|
||||
import { type GamePublishImage } from './gamePublishAssets';
|
||||
import {
|
||||
type GamePublishImage,
|
||||
revokeGamePublishImagePreview,
|
||||
} from './gamePublishAssets';
|
||||
import { applyVersionMediaDefaults } from './gamePublishMediaDefaults';
|
||||
import { GamePublishMediaFieldset } from './GamePublishMediaFieldset';
|
||||
import { resolvePublishMetadataError } from './gamePublishMetadata';
|
||||
@@ -84,6 +87,27 @@ export function GameWorkMetadataEditor({
|
||||
const [screenshotImages, setScreenshotImages] = useState<GamePublishImage[]>(
|
||||
[],
|
||||
);
|
||||
/**
|
||||
* 卸载时释放仍挂在界面上的本地预览 blob URL。回调里拿不到最新 state,用 ref 跟踪;显式
|
||||
* 移除/替换的那一份已由 fieldset 处理器 revoke,这里兜住卸载与线上默认值整体覆盖。
|
||||
*/
|
||||
const coverImageRef = useRef<GamePublishImage | null>(null);
|
||||
const screenshotImagesRef = useRef<GamePublishImage[]>([]);
|
||||
useEffect(() => {
|
||||
coverImageRef.current = coverImage;
|
||||
}, [coverImage]);
|
||||
useEffect(() => {
|
||||
screenshotImagesRef.current = screenshotImages;
|
||||
}, [screenshotImages]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
revokeGamePublishImagePreview(coverImageRef.current);
|
||||
for (const image of screenshotImagesRef.current) {
|
||||
revokeGamePublishImagePreview(image);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const [isLoadingMedia, setIsLoadingMedia] = useState(
|
||||
Boolean(game.latestVersion),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { GameDistributionVersionDetail } from '../../../packages/shared/src/contracts/gameDistribution';
|
||||
import { type GamePublishImage } from './gamePublishAssets';
|
||||
import { applyVersionMediaDefaults } from './gamePublishMediaDefaults';
|
||||
|
||||
function localImage(id: string, name: string): GamePublishImage {
|
||||
return {
|
||||
id,
|
||||
file: new File(['image-bytes'], name, { type: 'image/png' }),
|
||||
src: `blob:${id}`,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
describe('applyVersionMediaDefaults', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('用线上默认值覆盖本地选择前先释放旧 blob 预览', async () => {
|
||||
const revokeObjectURL = vi.fn();
|
||||
Object.defineProperty(URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => 'blob:unused',
|
||||
});
|
||||
Object.defineProperty(URL, 'revokeObjectURL', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: revokeObjectURL,
|
||||
});
|
||||
|
||||
let cover: GamePublishImage | null = localImage('local-cover', '封面.png');
|
||||
let screenshots: GamePublishImage[] = [
|
||||
localImage('local-shot', '截图.png'),
|
||||
];
|
||||
// 线上没有可沿用的媒体:覆盖结果为空,但被替换掉的本地预览仍必须释放。
|
||||
const detail = {
|
||||
game: { id: 'game-1', title: '线上作品', screenshots: [] },
|
||||
version: { frozenMetadata: null },
|
||||
} as unknown as GameDistributionVersionDetail;
|
||||
|
||||
await applyVersionMediaDefaults(detail, {
|
||||
setCover: (updater) => {
|
||||
cover = updater(cover);
|
||||
},
|
||||
setScreenshots: (updater) => {
|
||||
screenshots = updater(screenshots);
|
||||
},
|
||||
isCancelled: () => false,
|
||||
});
|
||||
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith('blob:local-cover');
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith('blob:local-shot');
|
||||
expect(cover).toBeNull();
|
||||
expect(screenshots).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createGamePublishImageId,
|
||||
type GamePublishImage,
|
||||
resolveGamePublishImagePreview,
|
||||
revokeGamePublishImagePreview,
|
||||
} from './gamePublishAssets';
|
||||
|
||||
type VersionMediaApplyTarget = {
|
||||
@@ -46,15 +47,20 @@ export async function applyVersionMediaDefaults(
|
||||
.map((objectKey) => objectKey?.trim() ?? '')
|
||||
.filter(Boolean);
|
||||
if (apply.isCancelled()) return;
|
||||
apply.setCover(() => (coverObjectKey ? buildCover('') : null));
|
||||
apply.setScreenshots(() =>
|
||||
screenshotObjectKeys.map((objectKey, index) => ({
|
||||
// 覆盖前先释放被替换掉的本地预览 blob URL:这一步之后旧对象不再有人引用,不 revoke 就泄漏。
|
||||
apply.setCover((current) => {
|
||||
revokeGamePublishImagePreview(current);
|
||||
return coverObjectKey ? buildCover('') : null;
|
||||
});
|
||||
apply.setScreenshots((current) => {
|
||||
for (const image of current) revokeGamePublishImagePreview(image);
|
||||
return screenshotObjectKeys.map((objectKey, index) => ({
|
||||
id: createGamePublishImageId(),
|
||||
objectKey,
|
||||
src: '',
|
||||
name: `截图 ${index + 1}`,
|
||||
})),
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
const coverPreview = await resolveGamePublishImagePreview(
|
||||
coverObjectKey,
|
||||
|
||||
Reference in New Issue
Block a user