Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit c52da02

Browse files
committed
Add a top loading bar when redirecting page out 0.2s
1 parent 8cc94db commit c52da02

File tree

2 files changed

+66
-18
lines changed

2 files changed

+66
-18
lines changed

framework/core/redirect.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import util from "../../lib/util.ts";
22
import events from "./events.ts";
33

44
let routerReady = false;
5-
let preRedirect: { url: URL; replace?: boolean } | null = null;
5+
let preRedirect: URL | null = null;
66

77
const onrouterready = (_: Record<string, unknown>) => {
88
events.off("routerready", onrouterready);
99
if (preRedirect) {
10-
events.emit("popstate", { type: "popstate", ...preRedirect });
10+
events.emit("popstate", { type: "popstate", url: preRedirect });
1111
preRedirect = null;
1212
}
1313
routerReady = true;
@@ -26,14 +26,20 @@ export function redirect(url: string, replace?: boolean) {
2626
return;
2727
}
2828

29-
const next = new URL(url, location.href);
30-
if (next.href === location.href) {
29+
const to = new URL(url, location.href);
30+
if (to.href === location.href) {
3131
return;
3232
}
3333

34+
if (replace) {
35+
history.replaceState(null, "", to);
36+
} else {
37+
history.pushState(null, "", to);
38+
}
39+
3440
if (routerReady) {
35-
events.emit("popstate", { type: "popstate", url: next, replace });
41+
events.emit("popstate", { type: "popstate", url: to });
3642
} else {
37-
preRedirect = { url: next, replace };
43+
preRedirect = to;
3844
}
3945
}

framework/react/router.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export type RouterProps = {
2222
readonly suspense?: boolean;
2323
};
2424

25-
type DataCache = {
25+
type RouteData = {
2626
data?: unknown;
2727
dataCacheTtl?: number;
2828
dataExpires?: number;
@@ -35,7 +35,7 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
3535
const [url, setUrl] = useState(() => ssrContext?.url || new URL(window.location?.href));
3636
const [modules, setModules] = useState(() => ssrContext?.routeModules || loadSSRModulesFromTag());
3737
const dataCache = useMemo(() => {
38-
const cache = new Map<string, DataCache>();
38+
const cache = new Map<string, RouteData>();
3939
modules.forEach(({ url, data, dataCacheTtl }) => {
4040
cache.set(url.pathname + url.search, {
4141
data,
@@ -108,7 +108,7 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
108108
};
109109
const isSuspense = document.body.getAttribute("data-suspense") ?? suspense;
110110
const prefetchData = async (dataUrl: string) => {
111-
const cache: DataCache = {};
111+
const rd: RouteData = {};
112112
const fetchData = async () => {
113113
const res = await fetch(dataUrl, { headers: { "Accept": "application/json" }, redirect: "manual" });
114114
if (res.status === 404 || res.status === 405) {
@@ -125,16 +125,16 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
125125
throw new FetchError(500, {}, "Missing the `Location` header");
126126
}
127127
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;
130130
return await res.json();
131131
};
132132
if (isSuspense) {
133-
cache.data = fetchData;
133+
rd.data = fetchData;
134134
} else {
135-
cache.data = await fetchData();
135+
rd.data = await fetchData();
136136
}
137-
dataCache.set(dataUrl, cache);
137+
dataCache.set(dataUrl, rd);
138138
};
139139
const onmoduleprefetch = (e: Record<string, unknown>) => {
140140
const pageUrl = new URL(e.href as string, location.href);
@@ -152,6 +152,12 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
152152
const onpopstate = async (e: Record<string, unknown>) => {
153153
const url = (e.url as URL | undefined) || new URL(window.location.href);
154154
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);
155161
const modules = await Promise.all(matches.map(async ([ret, meta]) => {
156162
const { filename } = meta;
157163
const rmod: RouteModule = {
@@ -173,12 +179,24 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
173179
}));
174180
setModules(modules);
175181
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();
179186
} 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);
181197
}
198+
}, 0);
199+
if (e.url) {
182200
window.scrollTo(0, 0);
183201
}
184202
};
@@ -291,6 +309,30 @@ function loadSSRModulesFromTag(): RouteModule[] {
291309
return [];
292310
}
293311

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+
294336
function getRouteModules(): Record<string, { defaultExport?: unknown; withData?: boolean }> {
295337
return global.__ROUTE_MODULES || (global.__ROUTE_MODULES = {});
296338
}

0 commit comments

Comments
 (0)