diff --git a/docs/docs/docs/api/api.en-US.md b/docs/docs/docs/api/api.en-US.md
index b3b99b46e91b..789ec30069fc 100644
--- a/docs/docs/docs/api/api.en-US.md
+++ b/docs/docs/docs/api/api.en-US.md
@@ -187,7 +187,8 @@ Type definition is as follows:
```ts
declare function Link(props: {
- prefetch?: boolean;
+ prefetch?: boolean | 'intent' | 'render' | 'viewport' | 'none';
+ prefetchTimeout?: number;
to: string | Partial<{ pathname: string; search: string; hash: string }>;
replace?: boolean;
state?: any;
@@ -207,7 +208,7 @@ function IndexPage({ user }) {
`` supports relative path navigation; `` does not do routing navigation and is equivalent to the jump behavior of ``.
-If `prefetch` is enabled, then when the user hovers over the component, Umi will automatically start preloading the component js files and data for the routing jump. (Note: Use this feature when `routePrefetch` and `manifest` are enabled)
+If `prefetch` is enabled, then when the user hovers over the component, Umi will automatically start preloading the component js files and data for the routing jump. (Note: Use this feature when `routePrefetch` is enabled)
### matchPath
diff --git a/docs/docs/docs/api/api.md b/docs/docs/docs/api/api.md
index 4139c76bbb32..dfe941a6a055 100644
--- a/docs/docs/docs/api/api.md
+++ b/docs/docs/docs/api/api.md
@@ -186,7 +186,8 @@ unlisten();
```ts
declare function Link(props: {
- prefetch?: boolean;
+ prefetch?: boolean | 'intent' | 'render' | 'viewport' | 'none';
+ prefetchTimeout?: number;
to: string | Partial<{ pathname: string; search: string; hash: string }>;
replace?: boolean;
state?: any;
@@ -206,7 +207,7 @@ function IndexPage({ user }) {
`` 支持相对路径跳转;`` 不做路由跳转,等同于 `` 的跳转行为。
-若开启了 `prefetch` 则当用户将鼠标放到该组件上方时,Umi 就会自动开始进行跳转路由的组件 js 文件和数据预加载。(注:使用此功能请同时开启 `routePrefetch` 和 `manifest` 配置)
+若开启了 `prefetch` 则当用户将鼠标放到该组件上方时,Umi 就会自动开始进行跳转路由的组件 js 文件和数据预加载。(注:使用此功能请开启 `routePrefetch` 配置)
### matchPath
diff --git a/docs/docs/docs/api/config.en-US.md b/docs/docs/docs/api/config.en-US.md
index 8b844153553c..2166ae697874 100644
--- a/docs/docs/docs/api/config.en-US.md
+++ b/docs/docs/docs/api/config.en-US.md
@@ -1304,7 +1304,7 @@ Configure how routes are loaded. Setting moduleType to 'cjs' will load route com
## routePrefetch
-- Type: `boolean`
+- Type: `{ defaultPrefetch: 'none' | 'intent' | 'render' | 'viewport', defaultPrefetchTimeout: number } | false`
- Default: `false`
Enable route preloading functionality.
diff --git a/docs/docs/docs/api/config.md b/docs/docs/docs/api/config.md
index fce4a49dc6a1..05b2ce19610a 100644
--- a/docs/docs/docs/api/config.md
+++ b/docs/docs/docs/api/config.md
@@ -1311,7 +1311,7 @@ proxy: {
## routePrefetch
-- 类型:`boolean`
+- 类型:`{ defaultPrefetch: 'none' | 'intent' | 'render' | 'viewport', defaultPrefetchTimeout: number } | false`
- 默认值:`false`
启用路由预加载功能。
diff --git a/packages/preset-umi/src/features/routePrefetch/routePrefetch.ts b/packages/preset-umi/src/features/routePrefetch/routePrefetch.ts
index dffb8f526368..173e21f45e30 100644
--- a/packages/preset-umi/src/features/routePrefetch/routePrefetch.ts
+++ b/packages/preset-umi/src/features/routePrefetch/routePrefetch.ts
@@ -4,13 +4,27 @@ export default (api: IApi) => {
api.describe({
config: {
schema({ zod }) {
- return zod.object({});
+ return zod.object({
+ defaultPrefetch: zod
+ .enum(['none', 'intent', 'render', 'viewport'])
+ .optional(),
+ defaultPrefetchTimeout: zod.number().optional(),
+ });
},
},
enableBy: api.EnableBy.config,
});
api.addEntryCodeAhead(() => {
- return `if(typeof window !== 'undefined') window.__umi_route_prefetch__ = true;`;
+ return `if(typeof window !== 'undefined') window.__umi_route_prefetch__ =
+ {
+ defaultPrefetch: ${JSON.stringify(
+ api.config.routePrefetch.defaultPrefetch || 'none',
+ )},
+ defaultPrefetchTimeout: ${JSON.stringify(
+ api.config.routePrefetch.defaultPrefetchTimeout || 50,
+ )},
+ };
+ `;
});
};
diff --git a/packages/renderer-react/src/link.tsx b/packages/renderer-react/src/link.tsx
index c518fc978d15..3a44e6ab8c23 100644
--- a/packages/renderer-react/src/link.tsx
+++ b/packages/renderer-react/src/link.tsx
@@ -1,27 +1,111 @@
-import React, { PropsWithChildren } from 'react';
+import React, { PropsWithChildren, useLayoutEffect } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { useAppData } from './appContext';
+import { useIntersectionObserver } from './useIntersectionObserver';
-export function LinkWithPrefetch(
- props: PropsWithChildren<
- {
- prefetch?: boolean;
- } & LinkProps &
- React.RefAttributes
- >,
-) {
- const { prefetch, ...linkProps } = props;
- const appData = useAppData();
- const to = typeof props.to === 'string' ? props.to : props.to?.pathname;
- // compatible with old code
- // which to might be undefined
- if (!to) return null;
- return (
- prefetch && to && appData.preloadRoute?.(to)}
- {...linkProps}
- >
- {props.children}
-
- );
+function useForwardedRef(ref?: React.ForwardedRef) {
+ const innerRef = React.useRef(null);
+ React.useEffect(() => {
+ if (!ref) return;
+ if (typeof ref === 'function') {
+ ref(innerRef.current);
+ } else {
+ ref.current = innerRef.current;
+ }
+ });
+ return innerRef;
}
+
+export const LinkWithPrefetch = React.forwardRef(
+ (
+ props: PropsWithChildren<
+ {
+ prefetch?: boolean | 'intent' | 'render' | 'viewport' | 'none';
+ prefetchTimeout?: number;
+ } & LinkProps &
+ React.RefAttributes
+ >,
+ forwardedRef,
+ ) => {
+ const { prefetch: prefetchProp, ...linkProps } = props;
+ const { defaultPrefetch, defaultPrefetchTimeout } = (typeof window !==
+ 'undefined' && // @ts-ignore
+ window.__umi_route_prefetch__) || {
+ defaultPrefetch: 'none',
+ defaultPrefetchTimeout: 50,
+ };
+
+ const prefetch =
+ (prefetchProp === true
+ ? 'intent'
+ : prefetchProp === false
+ ? 'none'
+ : prefetchProp) || defaultPrefetch;
+ if (!['intent', 'render', 'viewport', 'none'].includes(prefetch)) {
+ throw new Error(
+ `Invalid prefetch value ${prefetch} found in Link component`,
+ );
+ }
+ const appData = useAppData();
+ const to = typeof props.to === 'string' ? props.to : props.to?.pathname;
+ const hasRenderFetched = React.useRef(false);
+ const ref = useForwardedRef(forwardedRef);
+ // prefetch intent
+ const handleMouseEnter = (e: React.MouseEvent) => {
+ if (prefetch !== 'intent') return;
+ const eventTarget = (e.target || {}) as HTMLElement & {
+ preloadTimeout?: NodeJS.Timeout | null;
+ };
+ if (eventTarget.preloadTimeout) return;
+ eventTarget.preloadTimeout = setTimeout(() => {
+ eventTarget.preloadTimeout = null;
+ appData.preloadRoute?.(to!);
+ }, props.prefetchTimeout || defaultPrefetchTimeout);
+ };
+ const handleMouseLeave = (e: React.MouseEvent) => {
+ if (prefetch !== 'intent') return;
+ const eventTarget = (e.target || {}) as HTMLElement & {
+ preloadTimeout?: NodeJS.Timeout | null;
+ };
+ if (eventTarget.preloadTimeout) {
+ clearTimeout(eventTarget.preloadTimeout);
+ eventTarget.preloadTimeout = null;
+ }
+ };
+
+ // prefetch render
+ useLayoutEffect(() => {
+ if (prefetch === 'render' && !hasRenderFetched.current) {
+ appData.preloadRoute?.(to!);
+ hasRenderFetched.current = true;
+ }
+ }, [prefetch, to]);
+
+ // prefetch viewport
+ useIntersectionObserver(
+ ref as React.RefObject,
+ (entry) => {
+ if (entry?.isIntersecting) {
+ appData.preloadRoute?.(to!);
+ }
+ },
+ { rootMargin: '100px' },
+ { disabled: prefetch !== 'viewport' },
+ );
+
+ // compatible with old code
+ // which to might be undefined
+ if (!to) return null;
+
+ return (
+ }
+ {...linkProps}
+ >
+ {props.children}
+
+ );
+ },
+);
diff --git a/packages/renderer-react/src/useIntersectionObserver.ts b/packages/renderer-react/src/useIntersectionObserver.ts
new file mode 100644
index 000000000000..978f565a1be5
--- /dev/null
+++ b/packages/renderer-react/src/useIntersectionObserver.ts
@@ -0,0 +1,33 @@
+import React from 'react';
+
+export function useIntersectionObserver(
+ ref: React.RefObject,
+ callback: (entry: IntersectionObserverEntry | undefined) => void,
+ intersectionObserverOptions: IntersectionObserverInit = {},
+ options: { disabled?: boolean } = {},
+): IntersectionObserver | null {
+ // check if IntersectionObserver is available
+ if (typeof IntersectionObserver !== 'function') return null;
+
+ const isIntersectionObserverAvailable = React.useRef(
+ typeof IntersectionObserver === 'function',
+ );
+ const observerRef = React.useRef(null);
+ React.useEffect(() => {
+ if (
+ !ref.current ||
+ !isIntersectionObserverAvailable.current ||
+ options.disabled
+ ) {
+ return;
+ }
+ observerRef.current = new IntersectionObserver(([entry]) => {
+ callback(entry);
+ }, intersectionObserverOptions);
+ observerRef.current.observe(ref.current);
+ return () => {
+ observerRef.current?.disconnect();
+ };
+ }, [callback, intersectionObserverOptions, options.disabled, ref]);
+ return observerRef.current;
+}