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

Commit b94e8ee

Browse files
committed
feature(Link): support Link import the route modules and fetch route data without refresh.
1 parent ae6f0e8 commit b94e8ee

File tree

3 files changed

+371
-42
lines changed

3 files changed

+371
-42
lines changed

framework/vue/data.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ const createDataProvider = () => {
1919
const ssrContext: SSRContext | undefined = inject("ssrContext");
2020
const url = ssrContext?.url || new URL(window.location?.href);
2121
const defaultDataUrl = url.pathname + url.search;
22-
const dataUrl: string = inject("dataUrl") || defaultDataUrl;
22+
const dataUrl: Ref<string> = inject("dataUrl") || ref(defaultDataUrl);
2323

24-
const cached = dataCache?.get(dataUrl);
24+
const cached = dataCache?.get(dataUrl.value);
2525

2626
if (cached) {
2727
if (cached.data instanceof Error) {
@@ -89,8 +89,8 @@ const createDataProvider = () => {
8989
if (replace && res.ok) {
9090
try {
9191
const data = await res.json();
92-
const dataCacheTtl = dataCache.get(dataUrl)?.dataCacheTtl;
93-
dataCache.set(dataUrl, { data, dataCacheTtl, dataExpires: Date.now() + (dataCacheTtl || 1) * 1000 });
92+
const dataCacheTtl = dataCache.get(dataUrl.value)?.dataCacheTtl;
93+
dataCache.set(dataUrl.value, { data, dataCacheTtl, dataExpires: Date.now() + (dataCacheTtl || 1) * 1000 });
9494
_data.value = data;
9595
} catch (_) {
9696
if (optimistic) {
@@ -112,7 +112,7 @@ const createDataProvider = () => {
112112
console.log("reload");
113113

114114
try {
115-
const res = await fetch(dataUrl, { headers: { "Accept": "application/json" }, signal, redirect: "manual" });
115+
const res = await fetch(dataUrl.value, { headers: { "Accept": "application/json" }, signal, redirect: "manual" });
116116
if (res.type === "opaqueredirect") {
117117
throw new Error("opaque redirect");
118118
}
@@ -124,34 +124,34 @@ const createDataProvider = () => {
124124
const cc = res.headers.get("Cache-Control");
125125
const dataCacheTtl = cc && cc.includes("max-age=") ? parseInt(cc.split("max-age=")[1]) : undefined;
126126
const dataExpires = Date.now() + (dataCacheTtl || 1) * 1000;
127-
dataCache.set(dataUrl, { data, dataExpires });
127+
dataCache.set(dataUrl.value, { data, dataExpires });
128128
_data.value = data;
129129
} catch (_e) {
130130
throw new FetchError(500, {}, "Data must be valid JSON");
131131
}
132132
} catch (error) {
133-
throw new Error(`Failed to reload data for ${dataUrl}: ${error.message}`);
133+
throw new Error(`Failed to reload data for ${dataUrl.value}: ${error.message}`);
134134
}
135135
};
136136

137137
const mutation = {
138138
post: (data?: unknown, update?: UpdateStrategy) => {
139-
return action("post", send("post", dataUrl, data), update ?? "none");
139+
return action("post", send("post", dataUrl.value, data), update ?? "none");
140140
},
141141
put: (data?: unknown, update?: UpdateStrategy) => {
142-
return action("put", send("put", dataUrl, data), update ?? "none");
142+
return action("put", send("put", dataUrl.value, data), update ?? "none");
143143
},
144144
patch: (data?: unknown, update?: UpdateStrategy) => {
145-
return action("patch", send("patch", dataUrl, data), update ?? "none");
145+
return action("patch", send("patch", dataUrl.value, data), update ?? "none");
146146
},
147147
delete: (data?: unknown, update?: UpdateStrategy) => {
148-
return action("delete", send("delete", dataUrl, data), update ?? "none");
148+
return action("delete", send("delete", dataUrl.value, data), update ?? "none");
149149
},
150150
};
151151

152-
watch(() => dataUrl, () => {
152+
watch(() => dataUrl.value, () => {
153153
const now = Date.now();
154-
const cache = dataCache.get(dataUrl);
154+
const cache = dataCache.get(dataUrl.value);
155155
let ac: AbortController | null = null;
156156
if (cache === undefined || cache.dataExpires === undefined || cache.dataExpires < now) {
157157
ac = new AbortController();

framework/vue/link.ts

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { computed, defineComponent, h } from "vue";
12
import { useRouter } from "./router.ts";
2-
import { defineComponent, h } from "vue";
3+
import util from "../../lib/util.ts";
4+
import events from "../core/events.ts";
5+
import { redirect } from "../core/redirect.ts";
6+
7+
const prefetched = new Set<string>();
38

49
export const Link = defineComponent({
510
name: "Link",
@@ -8,15 +13,97 @@ export const Link = defineComponent({
813
type: String,
914
default: "",
1015
},
16+
replace: {
17+
type: Boolean,
18+
default: undefined,
19+
},
1120
},
12-
setup() {
13-
const { url, params } = useRouter();
21+
setup(props) {
22+
const router = useRouter();
23+
const to = props.to;
24+
const pathname = router.value.url.pathname;
25+
const href = computed(() => {
26+
if (!util.isFilledString(to)) {
27+
throw new Error("<Link>: prop `to` is required.");
28+
}
29+
if (util.isLikelyHttpURL(to)) {
30+
return to;
31+
}
32+
let [p, q] = util.splitBy(to, "?");
33+
if (p.startsWith("/")) {
34+
p = util.cleanPath(p);
35+
} else {
36+
p = util.cleanPath(pathname + "/" + p);
37+
}
38+
return [p, q].filter(Boolean).join("?");
39+
});
40+
41+
const onClick = (e: PointerEvent) => {
42+
if (e.defaultPrevented || isModifiedEvent(e)) {
43+
return;
44+
}
45+
e.preventDefault();
46+
redirect(href.value, props?.replace);
47+
};
48+
49+
const prefetch = () => {
50+
if (!util.isLikelyHttpURL(href.value) && !prefetched.has(href.value)) {
51+
events.emit("moduleprefetch", { href });
52+
prefetched.add(href.value);
53+
}
54+
};
55+
56+
let timer: number | undefined | null = undefined;
57+
58+
const onMouseenter = (e: PointerEvent) => {
59+
if (e.defaultPrevented) {
60+
return;
61+
}
62+
if (!timer && !prefetched.has(href.value)) {
63+
timer = setTimeout(() => {
64+
timer = null;
65+
prefetch();
66+
}, 150);
67+
}
68+
};
69+
70+
const onMouseleave = (e: PointerEvent) => {
71+
if (e.defaultPrevented) {
72+
return;
73+
}
74+
if (timer) {
75+
clearTimeout(timer);
76+
timer = null;
77+
}
78+
};
79+
1480
return {
15-
url,
16-
params,
81+
href,
82+
onClick,
83+
onMouseenter,
84+
onMouseleave,
1785
};
1886
},
1987
render() {
20-
return h("a", { href: this.$props.to }, this.$slots.default ? this.$slots.default() : []);
88+
return h(
89+
"a",
90+
{
91+
href: this.href,
92+
onClick: (e: PointerEvent) => {
93+
this.onClick(e);
94+
},
95+
onMouseenter: (e: PointerEvent) => {
96+
this.onMouseenter(e);
97+
},
98+
onMouseleave: (e: PointerEvent) => {
99+
this.onMouseleave(e);
100+
},
101+
},
102+
this.$slots.default ? this.$slots.default() : [],
103+
);
21104
},
22105
});
106+
107+
function isModifiedEvent(event: MouseEvent): boolean {
108+
return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
109+
}

0 commit comments

Comments
 (0)