返回 slidev
usePreloadImages.ts
根目录 / packages / client / composables / usePreloadImages.ts
1 import type { SlideRoute } from '@slidev/types'
2 import type { ComputedRef, Ref } from 'vue'
3 import { computed, ref, watchEffect } from 'vue'
4 import configs from '#slidev/configs'
5
6 const loaded = new Set<string>()
7 const loading = new Set<string>()
8 const RE_TRAILING_SLASH = /\/$/
9
10 function resolveUrl(url: string): string {
11 if (url.startsWith('http') || url.startsWith('//'))
12 return url
13 const base = (import.meta.env.BASE_URL || '/').replace(RE_TRAILING_SLASH, '')
14 return `${base}${url.startsWith('/') ? url : `/${url}`}`
15 }
16
17 const RETRY_LIMIT = 2
18 const retries = new Map<string, number>()
19
20 const preloadedCount = ref(0)
21 const totalImagesCount = ref(0)
22
23 /**
24 * Progress of slide-image preloading, from 0 to 1 (1 when every preloadable
25 * image in the deck has loaded, or when the deck has no images).
26 */
27 export const preloadProgress = computed(() =>
28 totalImagesCount.value === 0 ? 1 : preloadedCount.value / totalImagesCount.value,
29 )
30
31 /** Whether all preloadable slide images have finished loading. */
32 export const preloadComplete = computed(() => preloadProgress.value >= 1)
33
34 function preloadImage(url: string): void {
35 const resolved = resolveUrl(url)
36 if (loaded.has(resolved) || loading.has(resolved))
37 return
38 loading.add(resolved)
39 const img = new Image()
40 img.onload = () => {
41 loading.delete(resolved)
42 loaded.add(resolved)
43 retries.delete(resolved)
44 preloadedCount.value = loaded.size
45 }
46 img.onerror = () => {
47 loading.delete(resolved)
48 // Retry transient preload failures (e.g. flaky network) instead of silently
49 // giving up; bounded so genuinely-missing assets aren't hammered.
50 const attempts = retries.get(resolved) ?? 0
51 if (attempts < RETRY_LIMIT) {
52 retries.set(resolved, attempts + 1)
53 setTimeout(preloadImage, 1000 * (attempts + 1), url)
54 }
55 }
56 img.src = resolved
57 }
58
59 function preloadSlideImages(route: SlideRoute): void {
60 const images = route.meta?.slide?.images
61 if (images?.length) {
62 for (const url of images)
63 preloadImage(url)
64 }
65 }
66
67 export function usePreloadImages(
68 currentRoute: ComputedRef<SlideRoute>,
69 prevRoute: ComputedRef<SlideRoute>,
70 nextRoute: ComputedRef<SlideRoute>,
71 slides: Ref<SlideRoute[]>,
72 ): void {
73 const config = configs.preloadImages
74 if (config === false)
75 return
76
77 const ahead = (typeof config === 'object' && config?.ahead) || 3
78
79 // Track total preloadable images across the deck for progress reporting
80 watchEffect(() => {
81 const all = slides.value
82 if (!all?.length)
83 return
84 const urls = new Set<string>()
85 for (const route of all) {
86 for (const url of route.meta?.slide?.images ?? [])
87 urls.add(resolveUrl(url))
88 }
89 totalImagesCount.value = urls.size
90 })
91
92 // Preload current + prev + next + look-ahead window
93 watchEffect(() => {
94 const current = currentRoute.value
95 const all = slides.value
96 if (!current || !all?.length)
97 return
98
99 preloadSlideImages(current)
100 preloadSlideImages(prevRoute.value)
101 preloadSlideImages(nextRoute.value)
102
103 // Preload ahead window
104 const currentIdx = current.no - 1
105 for (let i = 1; i <= ahead; i++) {
106 const idx = currentIdx + i
107 if (idx < all.length)
108 preloadSlideImages(all[idx])
109 }
110 })
111
112 // Preload all remaining slides after 3s
113 watchEffect((onCleanup) => {
114 const all = slides.value
115 const timeout = setTimeout(() => {
116 if (all?.length) {
117 for (const route of all)
118 preloadSlideImages(route)
119 }
120 }, 3000)
121 onCleanup(() => clearTimeout(timeout))
122 })
123 }
124
124 lines TYPESCRIPT