-
Notifications
You must be signed in to change notification settings - Fork 58
chore: production build #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
chore(deps): update dependency @types/node to v24
…-next-3.x fix(deps): update dependency vue-waterfall-plugin-next to v3
chore(deps): update build (major)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
审阅者指南重构并加固流式 ZIP 下载器和 Ugoira 播放器,扩展图片代理以支持 Range 请求,调整懒加载图片和 Ugoira UI 行为,并更新用于生产构建的路径和依赖。 流式 Ugoira 下载与播放的时序图sequenceDiagram
actor User
participant UgoiraViewer
participant UgoiraPlayer
participant ZipDownloader
participant RangeRequestManager
participant RemoteImageServer
User->>UgoiraViewer: Open Ugoira artwork
UgoiraViewer->>UgoiraPlayer: new UgoiraPlayer(illust, options)
UgoiraViewer->>UgoiraPlayer: setupCanvas(canvas)
UgoiraViewer->>UgoiraPlayer: start()
UgoiraPlayer->>UgoiraPlayer: fetchMeta()
UgoiraPlayer->>ZipDownloader: setUrl(zipUrl).setOptions(...)
UgoiraPlayer->>ZipDownloader: streamingDownload({ onFileComplete, onProgress })
loop Progressive ZIP download
ZipDownloader->>RangeRequestManager: headSize(signal)
RangeRequestManager->>RemoteImageServer: HEAD / Range request
RemoteImageServer-->>RangeRequestManager: size headers
RangeRequestManager-->>ZipDownloader: contentLength
ZipDownloader->>RangeRequestManager: range(start, end, signal)
RangeRequestManager->>RemoteImageServer: GET bytes=start-end
RemoteImageServer-->>RangeRequestManager: 206 Partial Content
RangeRequestManager-->>ZipDownloader: Uint8Array(chunk)
ZipDownloader->>ZipDownloader: parse central directory / local headers
ZipDownloader-->>UgoiraPlayer: onFileComplete(entryWithData, info)
UgoiraPlayer->>UgoiraPlayer: files[fileName] = data
UgoiraPlayer->>UgoiraViewer: onDownloadProgress(progress, index, total)
alt Progressive render enabled
UgoiraPlayer->>UgoiraPlayer: frameReady[index] = true
UgoiraPlayer->>UgoiraPlayer: scheduleNextFrame()
UgoiraPlayer->>UgoiraPlayer: renderFrameToCanvas(index, frame)
end
end
ZipDownloader-->>UgoiraPlayer: streamingDownload result
UgoiraPlayer->>UgoiraPlayer: isDownloadComplete = true
UgoiraPlayer-->>UgoiraViewer: onDownloadComplete()
User->>UgoiraViewer: Click Play
UgoiraViewer->>UgoiraPlayer: play()
loop Animation loop
UgoiraPlayer->>UgoiraPlayer: drawFrame()
UgoiraPlayer->>UgoiraPlayer: getVisual(currentFrame)
UgoiraPlayer->>UgoiraPlayer: drawImage on canvas
end
更新后的 ZipDownloader 与 UgoiraPlayer 的类图classDiagram
class ZipDownloader {
-string url
-Required_ZipDownloaderOptions opts
-ByteLRU cache
-ConcurrencyLimiter limiter
-RangeRequestManager rangeHelper
-ZipParser parser
-Map~string, Promise_any~~ inflight
-ZipOverview overview
-DataRange[] dataRanges
-Map~string, number~ pathToIndex
+ZipDownloader(url string, options ZipDownloaderOptions)
+setOptions(partial ZipDownloaderOptions) ZipDownloader
+getSize(signal AbortSignal) Promise~number~
+getCentralDirectory(signal AbortSignal) Promise~ZipOverview~
+downloadByIndex(index number, signal AbortSignal) Promise_any
+downloadByPath(path string, signal AbortSignal) Promise_any
+getDataRanges(signal AbortSignal) Promise~DataRange[]~
+streamingDownload(params StreamingDownloadParams) Promise~StreamingDownloadResult~
+cleanup() ZipDownloader
+setUrl(url string) ZipDownloader
}
class RangeRequestManager {
-string url
-FetchLike fetcher
-number timeoutMs
-number retries
-ConcurrencyLimiter limiter
-ByteLRU cache
-Map~string, Promise_Uint8Array~~ inflight
+RangeRequestManager(url string, fetcher FetchLike, timeoutMs number, retries number, limiter ConcurrencyLimiter, cache ByteLRU)
+setUrl(url string) void
+setTimeout(ms number) void
+setRetries(n number) void
+setLimiter(limiter ConcurrencyLimiter) void
+setCache(cache ByteLRU) void
+headSize(signal AbortSignal) Promise~number~
+range(start number, end number, signal AbortSignal) Promise~Uint8Array~
}
class ZipParser {
-RangeRequestManager rangeHelper
-ParserOptions opts
+ZipParser(rangeHelper RangeRequestManager, opts ParserOptions)
+overview(signal AbortSignal, url string) Promise~ZipOverview~
+probeLocalHeader(entry ZipEntry, signal AbortSignal) Promise~DataRange~
}
class ConcurrencyLimiter {
-number running
-Array~Function~ q
-number max
+ConcurrencyLimiter(max number)
+acquire() Promise~void~
+release() void
+run(fn Function) Promise_any
}
class ByteLRU {
-Map~string, Uint8Array~ map
-number total
-number maxBytes
+ByteLRU(maxBytes number)
+get(k string) Uint8Array
+set(k string, v Uint8Array) void
+resize(n number) void
}
class ZipOverview {
+string url
+number contentLength
+number centralDirectoryOffset
+number centralDirectorySize
+number entryCount
+ZipEntry[] entries
}
class ZipEntry {
+number index
+string fileName
+number compressedSize
+number uncompressedSize
+number crc32
+number compressionMethod
+number generalPurposeBitFlag
+number localHeaderOffset
+number centralHeaderOffset
+boolean requiresZip64
+string mimeType
}
class DataRange {
+number index
+string fileName
+number dataStart
+number dataLength
}
class UgoiraPlayerOptions {
+Function onDownloadProgress
+Function onDownloadComplete
+Function onDownloadError
+ZipDownloaderOptions zipDownloaderOptions
+number requestTimeoutMs
+boolean preferImageBitmap
+number playbackRate
+boolean progressiveRender
}
class PlayerState {
<<enum>>
+Idle
+Downloading
+Ready
+Playing
+Paused
+Destroyed
}
class CachedVisual {
+HTMLImageElement img
+ImageBitmap bitmap
+string url
+Uint8Array buf
}
class UgoiraFrame {
+string file
+number delay
}
class UgoiraMeta {
+UgoiraFrame[] frames
+string mime_type
+string originalSrc
+string src
}
class UgoiraPlayer {
-HTMLCanvasElement _canvas
-Artwork _illust
-UgoiraMeta _meta
-PlayerState state
-boolean isPlaying
-number curFrame
-number lastFrameTime
-number nextFrameDue
-Map~string, CachedVisual~ cached
-Set~string~ objectURLs
-Record~string, Uint8Array~ files
-ZipDownloader zipDownloader
-AbortController aborter
-number downloadProgress
-boolean isDownloading
-boolean isDownloadComplete
-number downloadStartTime
-number[] frameDownloadTimes
-boolean[] frameReady
-number lastRenderedFrameIndex
-number renderTimer
-number _playbackRate
-boolean _preferImageBitmap
-boolean _progressiveRender
+UgoiraPlayer(illust Artwork, options UgoiraPlayerOptions)
+reset(illust Artwork) void
+setupCanvas(canvas HTMLCanvasElement) void
+get isReady() boolean
+get canExport() boolean
+get initWidth() number
+get initHeight() number
+get totalFrames() number
+get meta() UgoiraMeta
+get canvas() HTMLCanvasElement
+get now() number
+get downloadStats() any
+get mimeType() string
+get playbackRate() number
+set playbackRate(v number) void
+fetchMeta() Promise~void~
+start() Promise~UgoiraPlayer~
+streamingFetchAndDrawFrames(originalQuality boolean) Promise~UgoiraPlayer~
+play() void
+pause() void
+cancelDownload() void
+destroy() void
+getRealFrameSize() any
+renderGif() Promise~Blob~
+renderMp4() Promise~Blob~
-scheduleNextFrame() void
-renderFrameToCanvas(frameIndex number, frame UgoiraFrame) Promise~void~
-getVisual(fileName string) Promise~CachedVisual~
-getImage(fileName string) HTMLImageElement
-getImageAsync(fileName string) Promise~HTMLImageElement~
-drawFrame() void
-genGifEncoder() Promise_any
}
ZipDownloader --> RangeRequestManager : uses
ZipDownloader --> ZipParser : uses
ZipDownloader --> ConcurrencyLimiter : uses
ZipDownloader --> ByteLRU : caches ranges
RangeRequestManager --> ConcurrencyLimiter : throttles
RangeRequestManager --> ByteLRU : caches
ZipParser --> RangeRequestManager : range fetches
ZipParser --> ZipOverview : builds
ZipParser --> DataRange : probes
UgoiraPlayer --> ZipDownloader : downloads ZIP
UgoiraPlayer --> UgoiraMeta : uses metadata
UgoiraPlayer --> CachedVisual : caches frames
UgoiraPlayer --> PlayerState : tracks state
LazyLoad 组件行为的状态图stateDiagram-v2
[*] --> Idle
Idle --> Observing: mount
Observing --> Loading: element intersects viewport
state Loading {
[*] --> Requesting
Requesting --> Loaded: image onload
Requesting --> Error: image onerror
}
Loading --> Loaded
Loading --> Error
Loaded --> [*]
Error --> [*]
文件级变更
技巧与命令与 Sourcery 交互
自定义使用体验访问你的 控制面板 以:
获取帮助Original review guide in EnglishReviewer's GuideRefactors and hardens the streaming ZIP downloader and Ugoira player, expands the image proxy to support ranged requests, adjusts lazy image loading and Ugoira UI behavior, and updates paths and dependencies for a production build. Sequence diagram for streaming Ugoira download and playbacksequenceDiagram
actor User
participant UgoiraViewer
participant UgoiraPlayer
participant ZipDownloader
participant RangeRequestManager
participant RemoteImageServer
User->>UgoiraViewer: Open Ugoira artwork
UgoiraViewer->>UgoiraPlayer: new UgoiraPlayer(illust, options)
UgoiraViewer->>UgoiraPlayer: setupCanvas(canvas)
UgoiraViewer->>UgoiraPlayer: start()
UgoiraPlayer->>UgoiraPlayer: fetchMeta()
UgoiraPlayer->>ZipDownloader: setUrl(zipUrl).setOptions(...)
UgoiraPlayer->>ZipDownloader: streamingDownload({ onFileComplete, onProgress })
loop Progressive ZIP download
ZipDownloader->>RangeRequestManager: headSize(signal)
RangeRequestManager->>RemoteImageServer: HEAD / Range request
RemoteImageServer-->>RangeRequestManager: size headers
RangeRequestManager-->>ZipDownloader: contentLength
ZipDownloader->>RangeRequestManager: range(start, end, signal)
RangeRequestManager->>RemoteImageServer: GET bytes=start-end
RemoteImageServer-->>RangeRequestManager: 206 Partial Content
RangeRequestManager-->>ZipDownloader: Uint8Array(chunk)
ZipDownloader->>ZipDownloader: parse central directory / local headers
ZipDownloader-->>UgoiraPlayer: onFileComplete(entryWithData, info)
UgoiraPlayer->>UgoiraPlayer: files[fileName] = data
UgoiraPlayer->>UgoiraViewer: onDownloadProgress(progress, index, total)
alt Progressive render enabled
UgoiraPlayer->>UgoiraPlayer: frameReady[index] = true
UgoiraPlayer->>UgoiraPlayer: scheduleNextFrame()
UgoiraPlayer->>UgoiraPlayer: renderFrameToCanvas(index, frame)
end
end
ZipDownloader-->>UgoiraPlayer: streamingDownload result
UgoiraPlayer->>UgoiraPlayer: isDownloadComplete = true
UgoiraPlayer-->>UgoiraViewer: onDownloadComplete()
User->>UgoiraViewer: Click Play
UgoiraViewer->>UgoiraPlayer: play()
loop Animation loop
UgoiraPlayer->>UgoiraPlayer: drawFrame()
UgoiraPlayer->>UgoiraPlayer: getVisual(currentFrame)
UgoiraPlayer->>UgoiraPlayer: drawImage on canvas
end
Class diagram for updated ZipDownloader and UgoiraPlayerclassDiagram
class ZipDownloader {
-string url
-Required_ZipDownloaderOptions opts
-ByteLRU cache
-ConcurrencyLimiter limiter
-RangeRequestManager rangeHelper
-ZipParser parser
-Map~string, Promise_any~~ inflight
-ZipOverview overview
-DataRange[] dataRanges
-Map~string, number~ pathToIndex
+ZipDownloader(url string, options ZipDownloaderOptions)
+setOptions(partial ZipDownloaderOptions) ZipDownloader
+getSize(signal AbortSignal) Promise~number~
+getCentralDirectory(signal AbortSignal) Promise~ZipOverview~
+downloadByIndex(index number, signal AbortSignal) Promise_any
+downloadByPath(path string, signal AbortSignal) Promise_any
+getDataRanges(signal AbortSignal) Promise~DataRange[]~
+streamingDownload(params StreamingDownloadParams) Promise~StreamingDownloadResult~
+cleanup() ZipDownloader
+setUrl(url string) ZipDownloader
}
class RangeRequestManager {
-string url
-FetchLike fetcher
-number timeoutMs
-number retries
-ConcurrencyLimiter limiter
-ByteLRU cache
-Map~string, Promise_Uint8Array~~ inflight
+RangeRequestManager(url string, fetcher FetchLike, timeoutMs number, retries number, limiter ConcurrencyLimiter, cache ByteLRU)
+setUrl(url string) void
+setTimeout(ms number) void
+setRetries(n number) void
+setLimiter(limiter ConcurrencyLimiter) void
+setCache(cache ByteLRU) void
+headSize(signal AbortSignal) Promise~number~
+range(start number, end number, signal AbortSignal) Promise~Uint8Array~
}
class ZipParser {
-RangeRequestManager rangeHelper
-ParserOptions opts
+ZipParser(rangeHelper RangeRequestManager, opts ParserOptions)
+overview(signal AbortSignal, url string) Promise~ZipOverview~
+probeLocalHeader(entry ZipEntry, signal AbortSignal) Promise~DataRange~
}
class ConcurrencyLimiter {
-number running
-Array~Function~ q
-number max
+ConcurrencyLimiter(max number)
+acquire() Promise~void~
+release() void
+run(fn Function) Promise_any
}
class ByteLRU {
-Map~string, Uint8Array~ map
-number total
-number maxBytes
+ByteLRU(maxBytes number)
+get(k string) Uint8Array
+set(k string, v Uint8Array) void
+resize(n number) void
}
class ZipOverview {
+string url
+number contentLength
+number centralDirectoryOffset
+number centralDirectorySize
+number entryCount
+ZipEntry[] entries
}
class ZipEntry {
+number index
+string fileName
+number compressedSize
+number uncompressedSize
+number crc32
+number compressionMethod
+number generalPurposeBitFlag
+number localHeaderOffset
+number centralHeaderOffset
+boolean requiresZip64
+string mimeType
}
class DataRange {
+number index
+string fileName
+number dataStart
+number dataLength
}
class UgoiraPlayerOptions {
+Function onDownloadProgress
+Function onDownloadComplete
+Function onDownloadError
+ZipDownloaderOptions zipDownloaderOptions
+number requestTimeoutMs
+boolean preferImageBitmap
+number playbackRate
+boolean progressiveRender
}
class PlayerState {
<<enum>>
+Idle
+Downloading
+Ready
+Playing
+Paused
+Destroyed
}
class CachedVisual {
+HTMLImageElement img
+ImageBitmap bitmap
+string url
+Uint8Array buf
}
class UgoiraFrame {
+string file
+number delay
}
class UgoiraMeta {
+UgoiraFrame[] frames
+string mime_type
+string originalSrc
+string src
}
class UgoiraPlayer {
-HTMLCanvasElement _canvas
-Artwork _illust
-UgoiraMeta _meta
-PlayerState state
-boolean isPlaying
-number curFrame
-number lastFrameTime
-number nextFrameDue
-Map~string, CachedVisual~ cached
-Set~string~ objectURLs
-Record~string, Uint8Array~ files
-ZipDownloader zipDownloader
-AbortController aborter
-number downloadProgress
-boolean isDownloading
-boolean isDownloadComplete
-number downloadStartTime
-number[] frameDownloadTimes
-boolean[] frameReady
-number lastRenderedFrameIndex
-number renderTimer
-number _playbackRate
-boolean _preferImageBitmap
-boolean _progressiveRender
+UgoiraPlayer(illust Artwork, options UgoiraPlayerOptions)
+reset(illust Artwork) void
+setupCanvas(canvas HTMLCanvasElement) void
+get isReady() boolean
+get canExport() boolean
+get initWidth() number
+get initHeight() number
+get totalFrames() number
+get meta() UgoiraMeta
+get canvas() HTMLCanvasElement
+get now() number
+get downloadStats() any
+get mimeType() string
+get playbackRate() number
+set playbackRate(v number) void
+fetchMeta() Promise~void~
+start() Promise~UgoiraPlayer~
+streamingFetchAndDrawFrames(originalQuality boolean) Promise~UgoiraPlayer~
+play() void
+pause() void
+cancelDownload() void
+destroy() void
+getRealFrameSize() any
+renderGif() Promise~Blob~
+renderMp4() Promise~Blob~
-scheduleNextFrame() void
-renderFrameToCanvas(frameIndex number, frame UgoiraFrame) Promise~void~
-getVisual(fileName string) Promise~CachedVisual~
-getImage(fileName string) HTMLImageElement
-getImageAsync(fileName string) Promise~HTMLImageElement~
-drawFrame() void
-genGifEncoder() Promise_any
}
ZipDownloader --> RangeRequestManager : uses
ZipDownloader --> ZipParser : uses
ZipDownloader --> ConcurrencyLimiter : uses
ZipDownloader --> ByteLRU : caches ranges
RangeRequestManager --> ConcurrencyLimiter : throttles
RangeRequestManager --> ByteLRU : caches
ZipParser --> RangeRequestManager : range fetches
ZipParser --> ZipOverview : builds
ZipParser --> DataRange : probes
UgoiraPlayer --> ZipDownloader : downloads ZIP
UgoiraPlayer --> UgoiraMeta : uses metadata
UgoiraPlayer --> CachedVisual : caches frames
UgoiraPlayer --> PlayerState : tracks state
State diagram for LazyLoad component behaviorstateDiagram-v2
[*] --> Idle
Idle --> Observing: mount
Observing --> Loading: element intersects viewport
state Loading {
[*] --> Requesting
Requesting --> Loaded: image onload
Requesting --> Error: image onerror
}
Loading --> Loaded
Loading --> Error
Loaded --> [*]
Error --> [*]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - 我发现了 3 个问题,并给了一些整体反馈:
- ZipDownloader 和 UgoiraPlayer 现在会输出大量的 console.log/console.warn/console.error 信息(包括在重试和范围请求操作中);最好把这些日志放在现有的 debug 标志后面,或者在生产环境中直接移除,以避免日志过于嘈杂。
- LazyLoad 组件的新实现会创建一个不在 DOM 中的 Image 对象并把它赋值给 imgRef,而模板仍然渲染一个带自己 DOM 元素的动态组件 :is="...";这种 ref 的重新赋值并不会更新实际渲染元素的 src,看起来是错误的——建议让 ref 始终绑定到真实的 DOM 节点,并直接更新该节点的 src,而不是把 Image 实例存进 imgRef。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- ZipDownloader and UgoiraPlayer now emit a large number of console.log/console.warn/console.error messages (including within retry and range operations); it would be better to guard these behind the existing debug flag or remove them to avoid noisy logs in production.
- The LazyLoad component’s new implementation creates an off-DOM Image and assigns it to imgRef, while the template still renders a dynamic Component :is="..." with its own DOM element; this ref reassignment won’t update the actual rendered element’s src and looks incorrect—consider keeping the ref bound to the DOM node and updating that node’s src instead of storing the Image instance in imgRef.
## Individual Comments
### Comment 1
<location> `src/components/LazyLoad.vue:2-10` </location>
<code_context>
<template lang="pug">
-img(
+Component(
:class='{ lazyload: true, isLoading: !loaded && !error, isLoaded: loaded, isError: error }',
:height='height',
- :is='error ? "svg" : "img"',
+ :is='loaded ? "img" : "svg"',
:key='src',
:src='src',
:width='width'
- loading='lazy'
ref='imgRef'
+ role='img'
)
</template>
</code_context>
<issue_to_address>
**issue (bug_risk):** The new lazy-loading implementation never updates the rendered DOM image and misuses the template ref, so images may not appear as expected.
Two implementation details are breaking lazy loading:
1. `imgRef` is bound to the root template node but then reassigned to a programmatically created `Image`, which is never inserted into the DOM. Once you overwrite `imgRef`, the currently rendered element (the `<svg>`/`<img>` in the template) is no longer observed, so it never receives the loaded image and may stay stuck on the placeholder.
2. The template uses `Component` instead of the built-in `<component>` tag, so Vue will look for a registered `Component` instead of using dynamic element rendering.
Consider keeping `imgRef` always pointing at the real DOM element, and drive loading via reactive state (e.g. a `displaySrc` ref that is set to `props.src` only when the element is in view and bound with `:src="displaySrc || placeholderSrc"`). This lets `IntersectionObserver` work correctly while the browser handles the actual image loading and decoding.
</issue_to_address>
### Comment 2
<location> `src/utils/ZipDownloader.ts:842-801` </location>
<code_context>
+ this.rangeHelper.setLimiter(this.limiter)
+ }
+
+ this.rangeHelper.setTimeout(this.opts.timeoutMs)
+ this.rangeHelper.setRetries(this.opts.retries)
+ this.parser = new ZipParser(this.rangeHelper, {
+ initialTailSize: this.opts.initialTailSize,
+ maxTailSize: this.opts.maxTailSize,
</code_context>
<issue_to_address>
**issue (bug_risk):** Updating options does not propagate a new fetch implementation into RangeRequestManager, so `fetch` overrides may be ignored.
`RangeRequestManager` still uses the original `fetcher` from its constructor. When `setOptions` is called with a new `zipDownloaderOptions.fetch`, `this.opts.fetch` is updated but `RangeRequestManager` is not, so subsequent requests keep using the old fetch.
To ensure `fetch` overrides take effect, either expose a `setFetcher(fetcher: FetchLike)` on `RangeRequestManager` and call it from `setOptions`, or recreate `RangeRequestManager` when `fetch` changes.
</issue_to_address>
### Comment 3
<location> `src/utils/ZipDownloader.ts:873` </location>
<code_context>
}
}
- private async _ensureDataRanges(signal?: AbortSignal): Promise<DataRange[]> {
- if (this.dataRanges) return this.dataRanges
+ async probeLocalHeader(
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing logging behind a debug-aware logger and simplifying `_ensureDataRanges` to a single linear probing flow to reduce noise and incidental complexity.
The main added complexity here comes from (1) logging sprinkled everywhere and (2) the “smart probing” logic in `_ensureDataRanges`. Both can be simplified without losing any features.
### 1. Centralize logging behind a debug-aware logger
Right now `console.log/warn/error` are hardcoded in multiple places (`withRetries`, `maybeDecompress`, `RangeRequestManager`, `ZipParser`, `ZipDownloader`). This makes the core logic noisy and ignores the `debug` flag in many places.
You can centralize logging once in `ZipDownloader` and inject a logger into helpers:
```ts
// shared at top-level
interface Logger {
debug: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
}
function createLogger(enabled: boolean): Logger {
const noop = () => {}
if (!enabled) {
return { debug: noop, warn: noop, error: noop }
}
return {
debug: (...args) => console.log('[ZipDownloader]', ...args),
warn: (...args) => console.warn('[ZipDownloader]', ...args),
error: (...args) => console.error('[ZipDownloader]', ...args),
}
}
```
In `ZipDownloader`:
```ts
class ZipDownloader {
private logger: Logger
constructor(url: string, options: ZipDownloaderOptions = {}) {
// ...
this.opts = { /* existing defaults */ }
this.logger = createLogger(this.opts.debug)
// pass logger down
this.rangeHelper = new RangeRequestManager(
this.url,
this.opts.fetch,
this.opts.timeoutMs,
this.opts.retries,
this.limiter,
this.cache,
this.logger
)
this.parser = new ZipParser(this.rangeHelper, {
/* existing opts */,
tryDecompress: this.opts.tryDecompress,
logger: this.logger,
})
}
setOptions(partial: ZipDownloaderOptions): this {
const next = { ...this.opts, ...partial, fetch: partial.fetch ?? this.opts.fetch }
this.opts = next
this.logger = createLogger(this.opts.debug)
this.rangeHelper.setLogger(this.logger)
this.parser.setLogger(this.logger)
// existing concurrency/cache updates...
return this
}
}
```
Then in helpers, replace direct `console.*` calls with the injected logger:
```ts
class RangeRequestManager {
constructor(
private url: string,
private fetcher: FetchLike,
private timeoutMs: number,
private retries: number,
private limiter: ConcurrencyLimiter,
private cache: ByteLRU,
private logger: Logger
) {}
setLogger(logger: Logger) {
this.logger = logger
}
async range(start: number, end: number, signal?: AbortSignal) {
const cacheKey = `r:${start}-${end}`
const hit = this.cache.get(cacheKey)
if (hit) {
this.logger.debug(`缓存命中: ${start}-${end} (${hit.length} 字节)`)
return hit
}
// ...
this.logger.debug(`开始下载范围: ${start}-${end}`)
// ...
}
}
```
Similarly for `ZipParser` and `maybeDecompress`:
```ts
async function maybeDecompress(
method: number,
data: Uint8Array,
tryDecompress: boolean,
logger: Logger
) {
if (!tryDecompress) {
logger.debug(`跳过解压 (方法: ${method}, 数据大小: ${data.length})`)
return { bytes: data, isDecompressed: false, method }
}
// ...
}
```
And update calls to `maybeDecompress` to pass `this.logger`.
For `withRetries`, accept a logger (or optional callback) instead of hardcoding `console`:
```ts
async function withRetries<T>(
fn: () => Promise<T>,
retries: number,
logger?: Logger,
baseDelay = 200
): Promise<T> {
let n = 0
for (;;) {
try {
return await fn()
} catch (e) {
if (n++ >= retries) {
logger?.error?.(`重试失败,已达到最大重试次数 ${retries}:`, e)
throw e
}
const delay = baseDelay * 2 ** (n - 1) * (1 + Math.random() * 0.2)
logger?.warn?.(`请求失败,${delay.toFixed(0)}ms 后重试 (${n}/${retries}):`, e)
await sleep(delay)
}
}
}
```
This keeps the core control flow readable and ensures `debug` actually controls verbosity everywhere.
---
### 2. Simplify `_ensureDataRanges` “smart probing” control flow
The current `_ensureDataRanges` builds `needsProbe` and `skipProbe` arrays, logs multiple stages, and introduces another `ConcurrencyLimiter` + batching. You can keep the “probe only for large stored files above `probeThreshold`” behaviour while making the code linear and using a single limiter.
Key behavioural requirements to preserve:
- Files with `compressedSize < probeThreshold` or `compressionMethod !== 0` use the default `localHeaderOffset + 30`.
- Other files are probed via `parser.probeLocalHeader` with some concurrency.
A simpler version:
```ts
private async _ensureDataRanges(signal?: AbortSignal): Promise<DataRange[]> {
if (this.dataRanges) {
this.logger.debug('使用缓存的数据范围')
return this.dataRanges
}
const k = 'ranges'
if (!this.inflight.has(k)) {
this.logger.debug('开始探测数据范围...')
this.inflight.set(
k,
(async () => {
const ov = await this.getCentralDirectory(signal)
const out: DataRange[] = new Array(ov.entryCount)
// reuse maxConcurrentRequests as the probe limiter
const limit = Math.min(this.opts.parallelProbe, this.opts.maxConcurrentRequests)
const sem = new ConcurrencyLimiter(limit)
await Promise.all(
ov.entries.map((entry, idx) =>
sem.run(async () => {
const shouldProbe =
entry.compressionMethod === 0 &&
entry.compressedSize >= this.opts.probeThreshold
if (!shouldProbe) {
// default range
out[idx] = {
index: entry.index,
fileName: entry.fileName,
dataStart: entry.localHeaderOffset + 30,
dataLength: entry.compressedSize,
}
return
}
// precise probe
out[idx] = await this.parser.probeLocalHeader(entry, signal)
})
)
)
this.dataRanges = out
this.logger.debug(`数据范围探测完成: ${out.length} 个文件`)
return out
})().catch((e) => {
this.logger.error('数据范围探测失败:', e)
this.inflight.delete(k)
throw e
})
)
} else {
this.logger.debug('等待进行中的数据范围探测...')
}
return this.inflight.get(k)!
}
```
This:
- Keeps the `probeThreshold` feature and “only stored + big files are probed” logic.
- Avoids `needsProbe`/`skipProbe` arrays, batch counters, and extra logging noise.
- Uses a single limiter and a single `Promise.all` over entries, which is easier to read and maintain.
Both changes reduce incidental complexity while preserving your new capabilities (debugging, probe threshold, smarter probing).
</issue_to_address>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据反馈改进后续的评审质量。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- ZipDownloader and UgoiraPlayer now emit a large number of console.log/console.warn/console.error messages (including within retry and range operations); it would be better to guard these behind the existing debug flag or remove them to avoid noisy logs in production.
- The LazyLoad component’s new implementation creates an off-DOM Image and assigns it to imgRef, while the template still renders a dynamic Component :is="..." with its own DOM element; this ref reassignment won’t update the actual rendered element’s src and looks incorrect—consider keeping the ref bound to the DOM node and updating that node’s src instead of storing the Image instance in imgRef.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- ZipDownloader and UgoiraPlayer now emit a large number of console.log/console.warn/console.error messages (including within retry and range operations); it would be better to guard these behind the existing debug flag or remove them to avoid noisy logs in production.
- The LazyLoad component’s new implementation creates an off-DOM Image and assigns it to imgRef, while the template still renders a dynamic Component :is="..." with its own DOM element; this ref reassignment won’t update the actual rendered element’s src and looks incorrect—consider keeping the ref bound to the DOM node and updating that node’s src instead of storing the Image instance in imgRef.
## Individual Comments
### Comment 1
<location> `src/components/LazyLoad.vue:2-10` </location>
<code_context>
<template lang="pug">
-img(
+Component(
:class='{ lazyload: true, isLoading: !loaded && !error, isLoaded: loaded, isError: error }',
:height='height',
- :is='error ? "svg" : "img"',
+ :is='loaded ? "img" : "svg"',
:key='src',
:src='src',
:width='width'
- loading='lazy'
ref='imgRef'
+ role='img'
)
</template>
</code_context>
<issue_to_address>
**issue (bug_risk):** The new lazy-loading implementation never updates the rendered DOM image and misuses the template ref, so images may not appear as expected.
Two implementation details are breaking lazy loading:
1. `imgRef` is bound to the root template node but then reassigned to a programmatically created `Image`, which is never inserted into the DOM. Once you overwrite `imgRef`, the currently rendered element (the `<svg>`/`<img>` in the template) is no longer observed, so it never receives the loaded image and may stay stuck on the placeholder.
2. The template uses `Component` instead of the built-in `<component>` tag, so Vue will look for a registered `Component` instead of using dynamic element rendering.
Consider keeping `imgRef` always pointing at the real DOM element, and drive loading via reactive state (e.g. a `displaySrc` ref that is set to `props.src` only when the element is in view and bound with `:src="displaySrc || placeholderSrc"`). This lets `IntersectionObserver` work correctly while the browser handles the actual image loading and decoding.
</issue_to_address>
### Comment 2
<location> `src/utils/ZipDownloader.ts:842-801` </location>
<code_context>
+ this.rangeHelper.setLimiter(this.limiter)
+ }
+
+ this.rangeHelper.setTimeout(this.opts.timeoutMs)
+ this.rangeHelper.setRetries(this.opts.retries)
+ this.parser = new ZipParser(this.rangeHelper, {
+ initialTailSize: this.opts.initialTailSize,
+ maxTailSize: this.opts.maxTailSize,
</code_context>
<issue_to_address>
**issue (bug_risk):** Updating options does not propagate a new fetch implementation into RangeRequestManager, so `fetch` overrides may be ignored.
`RangeRequestManager` still uses the original `fetcher` from its constructor. When `setOptions` is called with a new `zipDownloaderOptions.fetch`, `this.opts.fetch` is updated but `RangeRequestManager` is not, so subsequent requests keep using the old fetch.
To ensure `fetch` overrides take effect, either expose a `setFetcher(fetcher: FetchLike)` on `RangeRequestManager` and call it from `setOptions`, or recreate `RangeRequestManager` when `fetch` changes.
</issue_to_address>
### Comment 3
<location> `src/utils/ZipDownloader.ts:873` </location>
<code_context>
}
}
- private async _ensureDataRanges(signal?: AbortSignal): Promise<DataRange[]> {
- if (this.dataRanges) return this.dataRanges
+ async probeLocalHeader(
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing logging behind a debug-aware logger and simplifying `_ensureDataRanges` to a single linear probing flow to reduce noise and incidental complexity.
The main added complexity here comes from (1) logging sprinkled everywhere and (2) the “smart probing” logic in `_ensureDataRanges`. Both can be simplified without losing any features.
### 1. Centralize logging behind a debug-aware logger
Right now `console.log/warn/error` are hardcoded in multiple places (`withRetries`, `maybeDecompress`, `RangeRequestManager`, `ZipParser`, `ZipDownloader`). This makes the core logic noisy and ignores the `debug` flag in many places.
You can centralize logging once in `ZipDownloader` and inject a logger into helpers:
```ts
// shared at top-level
interface Logger {
debug: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
}
function createLogger(enabled: boolean): Logger {
const noop = () => {}
if (!enabled) {
return { debug: noop, warn: noop, error: noop }
}
return {
debug: (...args) => console.log('[ZipDownloader]', ...args),
warn: (...args) => console.warn('[ZipDownloader]', ...args),
error: (...args) => console.error('[ZipDownloader]', ...args),
}
}
```
In `ZipDownloader`:
```ts
class ZipDownloader {
private logger: Logger
constructor(url: string, options: ZipDownloaderOptions = {}) {
// ...
this.opts = { /* existing defaults */ }
this.logger = createLogger(this.opts.debug)
// pass logger down
this.rangeHelper = new RangeRequestManager(
this.url,
this.opts.fetch,
this.opts.timeoutMs,
this.opts.retries,
this.limiter,
this.cache,
this.logger
)
this.parser = new ZipParser(this.rangeHelper, {
/* existing opts */,
tryDecompress: this.opts.tryDecompress,
logger: this.logger,
})
}
setOptions(partial: ZipDownloaderOptions): this {
const next = { ...this.opts, ...partial, fetch: partial.fetch ?? this.opts.fetch }
this.opts = next
this.logger = createLogger(this.opts.debug)
this.rangeHelper.setLogger(this.logger)
this.parser.setLogger(this.logger)
// existing concurrency/cache updates...
return this
}
}
```
Then in helpers, replace direct `console.*` calls with the injected logger:
```ts
class RangeRequestManager {
constructor(
private url: string,
private fetcher: FetchLike,
private timeoutMs: number,
private retries: number,
private limiter: ConcurrencyLimiter,
private cache: ByteLRU,
private logger: Logger
) {}
setLogger(logger: Logger) {
this.logger = logger
}
async range(start: number, end: number, signal?: AbortSignal) {
const cacheKey = `r:${start}-${end}`
const hit = this.cache.get(cacheKey)
if (hit) {
this.logger.debug(`缓存命中: ${start}-${end} (${hit.length} 字节)`)
return hit
}
// ...
this.logger.debug(`开始下载范围: ${start}-${end}`)
// ...
}
}
```
Similarly for `ZipParser` and `maybeDecompress`:
```ts
async function maybeDecompress(
method: number,
data: Uint8Array,
tryDecompress: boolean,
logger: Logger
) {
if (!tryDecompress) {
logger.debug(`跳过解压 (方法: ${method}, 数据大小: ${data.length})`)
return { bytes: data, isDecompressed: false, method }
}
// ...
}
```
And update calls to `maybeDecompress` to pass `this.logger`.
For `withRetries`, accept a logger (or optional callback) instead of hardcoding `console`:
```ts
async function withRetries<T>(
fn: () => Promise<T>,
retries: number,
logger?: Logger,
baseDelay = 200
): Promise<T> {
let n = 0
for (;;) {
try {
return await fn()
} catch (e) {
if (n++ >= retries) {
logger?.error?.(`重试失败,已达到最大重试次数 ${retries}:`, e)
throw e
}
const delay = baseDelay * 2 ** (n - 1) * (1 + Math.random() * 0.2)
logger?.warn?.(`请求失败,${delay.toFixed(0)}ms 后重试 (${n}/${retries}):`, e)
await sleep(delay)
}
}
}
```
This keeps the core control flow readable and ensures `debug` actually controls verbosity everywhere.
---
### 2. Simplify `_ensureDataRanges` “smart probing” control flow
The current `_ensureDataRanges` builds `needsProbe` and `skipProbe` arrays, logs multiple stages, and introduces another `ConcurrencyLimiter` + batching. You can keep the “probe only for large stored files above `probeThreshold`” behaviour while making the code linear and using a single limiter.
Key behavioural requirements to preserve:
- Files with `compressedSize < probeThreshold` or `compressionMethod !== 0` use the default `localHeaderOffset + 30`.
- Other files are probed via `parser.probeLocalHeader` with some concurrency.
A simpler version:
```ts
private async _ensureDataRanges(signal?: AbortSignal): Promise<DataRange[]> {
if (this.dataRanges) {
this.logger.debug('使用缓存的数据范围')
return this.dataRanges
}
const k = 'ranges'
if (!this.inflight.has(k)) {
this.logger.debug('开始探测数据范围...')
this.inflight.set(
k,
(async () => {
const ov = await this.getCentralDirectory(signal)
const out: DataRange[] = new Array(ov.entryCount)
// reuse maxConcurrentRequests as the probe limiter
const limit = Math.min(this.opts.parallelProbe, this.opts.maxConcurrentRequests)
const sem = new ConcurrencyLimiter(limit)
await Promise.all(
ov.entries.map((entry, idx) =>
sem.run(async () => {
const shouldProbe =
entry.compressionMethod === 0 &&
entry.compressedSize >= this.opts.probeThreshold
if (!shouldProbe) {
// default range
out[idx] = {
index: entry.index,
fileName: entry.fileName,
dataStart: entry.localHeaderOffset + 30,
dataLength: entry.compressedSize,
}
return
}
// precise probe
out[idx] = await this.parser.probeLocalHeader(entry, signal)
})
)
)
this.dataRanges = out
this.logger.debug(`数据范围探测完成: ${out.length} 个文件`)
return out
})().catch((e) => {
this.logger.error('数据范围探测失败:', e)
this.inflight.delete(k)
throw e
})
)
} else {
this.logger.debug('等待进行中的数据范围探测...')
}
return this.inflight.get(k)!
}
```
This:
- Keeps the `probeThreshold` feature and “only stored + big files are probed” logic.
- Avoids `needsProbe`/`skipProbe` arrays, batch counters, and extra logging noise.
- Uses a single limiter and a single `Promise.all` over entries, which is easier to read and maintain.
Both changes reduce incidental complexity while preserving your new capabilities (debugging, probe threshold, smarter probing).
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| Component( | ||
| :class='{ lazyload: true, isLoading: !loaded && !error, isLoaded: loaded, isError: error }', | ||
| :height='height', | ||
| :is='error ? "svg" : "img"', | ||
| :is='loaded ? "img" : "svg"', | ||
| :key='src', | ||
| :src='src', | ||
| :width='width' | ||
| loading='lazy' | ||
| ref='imgRef' | ||
| role='img' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): 新的懒加载实现从未更新已经渲染的 DOM 图片,并且误用了模板 ref,因此图片可能不会按预期显示。
有两个实现细节破坏了懒加载逻辑:
-
imgRef绑定在模板根节点上,但随后被重新指向一个通过代码创建的Image对象,而该对象从未被插入 DOM。一旦你重写了imgRef,当前渲染的元素(模板中的<svg>/<img>)就不再被观察,因此不会接收到已加载的图片,可能会一直停留在占位图上。 -
模板使用的是
Component而不是内置的<component>标签,因此 Vue 会去查找一个已注册的Component组件,而不是使用动态元素渲染。
建议让 imgRef 始终指向真实的 DOM 元素,并通过响应式状态来驱动加载(例如使用 displaySrc 这个 ref,当元素进入视口时才设置为 props.src,并在模板里用 :src="displaySrc || placeholderSrc" 绑定)。这样可以让 IntersectionObserver 正常工作,同时由浏览器负责实际的图片加载和解码。
Original comment in English
issue (bug_risk): The new lazy-loading implementation never updates the rendered DOM image and misuses the template ref, so images may not appear as expected.
Two implementation details are breaking lazy loading:
-
imgRefis bound to the root template node but then reassigned to a programmatically createdImage, which is never inserted into the DOM. Once you overwriteimgRef, the currently rendered element (the<svg>/<img>in the template) is no longer observed, so it never receives the loaded image and may stay stuck on the placeholder. -
The template uses
Componentinstead of the built-in<component>tag, so Vue will look for a registeredComponentinstead of using dynamic element rendering.
Consider keeping imgRef always pointing at the real DOM element, and drive loading via reactive state (e.g. a displaySrc ref that is set to props.src only when the element is in view and bound with :src="displaySrc || placeholderSrc"). This lets IntersectionObserver work correctly while the browser handles the actual image loading and decoding.
| this.limiter, | ||
| this.cache | ||
| ) | ||
| this.parser = new ZipParser(this.rangeHelper, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): 更新配置时,没有把新的 fetch 实现传递给 RangeRequestManager,因此 fetch 覆盖可能会被忽略。
RangeRequestManager 仍然使用构造函数中传入的初始 fetcher。当 setOptions 被调用并传入新的 zipDownloaderOptions.fetch 时,this.opts.fetch 虽然被更新了,但 RangeRequestManager 并没有同步更新,因此后续请求仍然会使用旧的 fetch。
为了确保 fetch 覆盖生效,可以考虑:要么在 RangeRequestManager 上暴露一个 setFetcher(fetcher: FetchLike) 方法并在 setOptions 中调用它,要么在 fetch 发生变化时重新创建 RangeRequestManager。
Original comment in English
issue (bug_risk): Updating options does not propagate a new fetch implementation into RangeRequestManager, so fetch overrides may be ignored.
RangeRequestManager still uses the original fetcher from its constructor. When setOptions is called with a new zipDownloaderOptions.fetch, this.opts.fetch is updated but RangeRequestManager is not, so subsequent requests keep using the old fetch.
To ensure fetch overrides take effect, either expose a setFetcher(fetcher: FetchLike) on RangeRequestManager and call it from setOptions, or recreate RangeRequestManager when fetch changes.
Summary by Sourcery
改进 ZIP 下载、ugoira 播放、图片代理以及用于生产部署的类型/组件声明。
新功能:
probeThreshold,以更智能地探测本地文件头。playbackRate、渐进式流式播放,以及基于 ImageBitmap 的渲染选项。/api/image代理以支持范围请求(Range Requests),并转发关键的缓存/校验相关请求头。缺陷修复:
改进优化:
streamingDownload流式下载 API。ZipDownloader.streamingDownload流式加载帧、缓存解码后的画面,并更好地管理生命周期和中止(abort)逻辑。src/目录结构和导出的类型。构建:
src/前缀),以便支持生产环境打包。部署:
杂务(Chores):
packageManager版本元数据,以适配生产环境。Original summary in English
Summary by Sourcery
Improve ZIP download, ugoira playback, image proxying, and type/component declarations for production deployment.
New Features:
Bug Fixes:
Enhancements:
Build:
Deployment:
Chores: