@@ -22,7 +22,7 @@ export type RouterProps = {
22
22
readonly suspense ?: boolean ;
23
23
} ;
24
24
25
- type DataCache = {
25
+ type RouteData = {
26
26
data ?: unknown ;
27
27
dataCacheTtl ?: number ;
28
28
dataExpires ?: number ;
@@ -35,7 +35,7 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
35
35
const [ url , setUrl ] = useState ( ( ) => ssrContext ?. url || new URL ( window . location ?. href ) ) ;
36
36
const [ modules , setModules ] = useState ( ( ) => ssrContext ?. routeModules || loadSSRModulesFromTag ( ) ) ;
37
37
const dataCache = useMemo ( ( ) => {
38
- const cache = new Map < string , DataCache > ( ) ;
38
+ const cache = new Map < string , RouteData > ( ) ;
39
39
modules . forEach ( ( { url, data, dataCacheTtl } ) => {
40
40
cache . set ( url . pathname + url . search , {
41
41
data,
@@ -108,7 +108,7 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
108
108
} ;
109
109
const isSuspense = document . body . getAttribute ( "data-suspense" ) ?? suspense ;
110
110
const prefetchData = async ( dataUrl : string ) => {
111
- const cache : DataCache = { } ;
111
+ const rd : RouteData = { } ;
112
112
const fetchData = async ( ) => {
113
113
const res = await fetch ( dataUrl , { headers : { "Accept" : "application/json" } , redirect : "manual" } ) ;
114
114
if ( res . status === 404 || res . status === 405 ) {
@@ -125,16 +125,16 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
125
125
throw new FetchError ( 500 , { } , "Missing the `Location` header" ) ;
126
126
}
127
127
const cc = res . headers . get ( "Cache-Control" ) ;
128
- cache . dataCacheTtl = cc ?. includes ( "max-age=" ) ? parseInt ( cc . split ( "max-age=" ) [ 1 ] ) : undefined ;
129
- cache . dataExpires = Date . now ( ) + ( cache . dataCacheTtl || 1 ) * 1000 ;
128
+ rd . dataCacheTtl = cc ?. includes ( "max-age=" ) ? parseInt ( cc . split ( "max-age=" ) [ 1 ] ) : undefined ;
129
+ rd . dataExpires = Date . now ( ) + ( rd . dataCacheTtl || 1 ) * 1000 ;
130
130
return await res . json ( ) ;
131
131
} ;
132
132
if ( isSuspense ) {
133
- cache . data = fetchData ;
133
+ rd . data = fetchData ;
134
134
} else {
135
- cache . data = await fetchData ( ) ;
135
+ rd . data = await fetchData ( ) ;
136
136
}
137
- dataCache . set ( dataUrl , cache ) ;
137
+ dataCache . set ( dataUrl , rd ) ;
138
138
} ;
139
139
const onmoduleprefetch = ( e : Record < string , unknown > ) => {
140
140
const pageUrl = new URL ( e . href as string , location . href ) ;
@@ -152,6 +152,12 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
152
152
const onpopstate = async ( e : Record < string , unknown > ) => {
153
153
const url = ( e . url as URL | undefined ) || new URL ( window . location . href ) ;
154
154
const matches = matchRoutes ( url , routes ) ;
155
+ const loadingBar = getLoadingBar ( ) ;
156
+ let loading : number | null = setTimeout ( ( ) => {
157
+ loading = null ;
158
+ loadingBar . style . opacity = "1" ;
159
+ loadingBar . style . width = "50%" ;
160
+ } , 200 ) ;
155
161
const modules = await Promise . all ( matches . map ( async ( [ ret , meta ] ) => {
156
162
const { filename } = meta ;
157
163
const rmod : RouteModule = {
@@ -173,12 +179,24 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
173
179
} ) ) ;
174
180
setModules ( modules ) ;
175
181
setUrl ( url ) ;
176
- if ( e . url ) {
177
- if ( e . replace ) {
178
- history . replaceState ( null , "" , e . url as URL ) ;
182
+ setTimeout ( ( ) => {
183
+ if ( loading ) {
184
+ clearTimeout ( loading ) ;
185
+ loadingBar . remove ( ) ;
179
186
} else {
180
- history . pushState ( null , "" , e . url as URL ) ;
187
+ const fadeOutTime = 1.0 ;
188
+ loadingBar . style . transition = `opacity ${ fadeOutTime } s ease-in-out, width ${ fadeOutTime } s ease-in-out` ;
189
+ setTimeout ( ( ) => {
190
+ loadingBar . style . opacity = "0" ;
191
+ loadingBar . style . width = "100%" ;
192
+ } , 0 ) ;
193
+ global . __loading_bar_remove_timer = setTimeout ( ( ) => {
194
+ global . __loading_bar_remove_timer = null ;
195
+ loadingBar . remove ( ) ;
196
+ } , fadeOutTime * 1000 ) ;
181
197
}
198
+ } , 0 ) ;
199
+ if ( e . url ) {
182
200
window . scrollTo ( 0 , 0 ) ;
183
201
}
184
202
} ;
@@ -291,6 +309,30 @@ function loadSSRModulesFromTag(): RouteModule[] {
291
309
return [ ] ;
292
310
}
293
311
312
+ function getLoadingBar ( ) : HTMLDivElement {
313
+ if ( typeof global . __loading_bar_remove_timer === "number" ) {
314
+ clearTimeout ( global . __loading_bar_remove_timer ) ;
315
+ global . __loading_bar_remove_timer = null ;
316
+ }
317
+ let bar = ( document . getElementById ( "loading-bar" ) as HTMLDivElement | null ) ;
318
+ if ( ! bar ) {
319
+ bar = document . createElement ( "div" ) ;
320
+ bar . id = "loading-bar" ;
321
+ document . body . appendChild ( bar ) ;
322
+ }
323
+ Object . assign ( bar . style , {
324
+ position : "fixed" ,
325
+ top : "0" ,
326
+ left : "0" ,
327
+ width : "0" ,
328
+ height : "1px" ,
329
+ opacity : "0" ,
330
+ background : "rgba(128, 128, 128, 0.9)" ,
331
+ transition : "opacity 0.6s ease-in-out, width 3s ease-in-out" ,
332
+ } ) ;
333
+ return bar ;
334
+ }
335
+
294
336
function getRouteModules ( ) : Record < string , { defaultExport ?: unknown ; withData ?: boolean } > {
295
337
return global . __ROUTE_MODULES || ( global . __ROUTE_MODULES = { } ) ;
296
338
}
0 commit comments