大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
什么是 ForesightJS
ForesightJS 是一个轻量级 JavaScript 库,完全支持 TypeScript,能够根据鼠标移动、滚动和键盘导航预测用户意图。其通过分析光标 / 滚动轨迹和 Tab 键顺序,预测用户可能与哪些元素交互,从而允许开发者在实际悬停或点击之前触发操作,例如:预加载。
ForesightJS 的核心作用包括:
- 判断预先加载哪些资源或数据
- 加载方式和缓存策略
- 开始加载的最佳时机
典型特征包括:
- 光标预测:根据用户光标移动方向(而非当前悬停位置)预取数据
- 键盘预测:当用户距离元素 N 个 Tab 键时预取数据,Shift-Tab 键也有效
- 完全可定制:配置预测的力度,调整元素周围的点击区域等等
- 支持调试模式:内置可视化调试功能,包含轨迹可视化、点击区域叠加和交互式控制面板,可实时调整配置
ForesightJS 弥补了浪费资源的视口预取和基本的悬停预取之间的差距,ForesightManager 通过分析鼠标轨迹模式、滚动方向和键盘导航序列来预测用户交互。允许开发者在最佳时间预取资源以提升性能,同时又能有的放矢地避免浪费。
目前 ForesightJS 在 Github 通过 MIT 协议开源,短短时间内已经有超过 0.5k 的 star,是一个值得关注的前端开源项目。
如何使用 ForesightJS
首先安装相应的依赖:
pnpm add js.foresight
// # or
npm install js.foresight
// # or
yarn add js.foresight
然后就可以直接导入使用:
import { ForesightManager } from "foresightjs";
// 如果需要自定义全局设置,请初始化管理器(在应用启动时执行一次)
// 如果不需要全局设置,则无需初始化管理器
ForesightManager.initialize({
debug: false,
// Set to true to see visualization
trajectoryPredictionTime: 80,
// How far ahead (in milliseconds) to predict the mouse trajectory
});
// 跟踪某一个元素
const myButton = document.getElementById("my-button");
const { isTouchDevice, unregister } = ForesightManager.instance.register({
element: myButton,
callback: () => {
// 这里就是预取的逻辑
},
hitSlop: 20,
// 可选:“hit slop”,以像素为单位。覆盖 defaultHitSlop
});
// Later, when done with this element:
unregister();
ForesightJS 全局设置在初始化 ForesightManager 时指定,而且只需要在应用程序的入口完成一次。 如果想要默认的全局选项,则无需初始化 ForesightManager。
import { ForesightManager } from "foresightjs";
// 如果想要自定义设置只需要在应用顶部初始化一次
ForesightManager.initialize({
enableMousePrediction: true,
positionHistorySize: 8,
trajectoryPredictionTime: 80,
defaultHitSlop: 10,
debug: false,
debuggerSettings: {
isControlPanelDefaultMinimized: false,
showNameTags: true,
sortElementList: "visibility",
},
enableTabPrediction: true,
tabOffset: 3,
enableScrollPrediction: true,
scrollMargin: 150,
onAnyCallbackFired: (
elementData: ForesightElementData,
managerData: ForesightManagerData
) => {
console.log(`Callback hit from: ${elementData.name}`);
console.log(`Total tab hits: ${managerData.globalCallbackHits.tab}`);
console.log(`total mouse hits ${managerData.globalCallbackHits.mouse}`);
},
});
当向 ForesightManager 注册元素时,开发者还可以提供特定于每个元素的配置:
const myElement = document.getElementById("my-element");
const { unregister, isTouchDevice } = ForesightManager.instance.register({
element: myElement,
// The element to monitor
callback: () => {
console.log("prefetching");
},
// Function that executes when interaction is predicted or occurs
hitSlop: { top: 10, left: 50, right: 50, bottom: 100 },
// Fully invisible "slop" around the element. Basically increases the hover hitbox
name: "My button name",
// A descriptive name, useful in debug mode
unregisterOnCallback: false,
// Should the callback be ran more than ones?
});
// its best practice to unregister the element if you are done with it (return of an useEffect in React for example)
unregister(element);
最后值得一提的是,ForesightJS 专注于利用鼠标移动进行预取,因此需要针对手机和平板电脑等触控设备制定自己的预取方案。
ForesightManager.instance.register() 方法返回一个 isTouchDevice 布尔值,可以使用它来创建单独的逻辑。即使在触控设备上,也可以安全地调用 register() ,因为 Foresight 管理器会弹出触控设备,以避免不必要的处理。
下面是在 Next.js 或 React Router ForesightLink 组件中如何使用触控设备的示例:
"use client"
import type { LinkProps } from "next/link"
import Link from "next/link"
import { useEffect, useRef, useState } from "react"
import { ForesightManager, type ForesightRect } from "js.foresight"
import { useRouter } from "next/navigation"
interface ForesightLinkProps extends Omit<LinkProps, "prefetch"> {
children: React.ReactNode
className?: string
hitSlop?: number | ForesightRect
unregisterOnCallback?: boolean
name?: string
}
export function ForesightLink({
children,
className,
hitSlop = 0,
unregisterOnCallback = true,
name = "",
...props
}: ForesightLinkProps) {
const LinkRef = useRef<HTMLAnchorElement>(null)
const [isTouchDevice, setIsTouchDevice] = useState(false)
const router = useRouter()
// import from "next/navigation" not "next/router"
useEffect(() => {
if (!LinkRef.current) {
return
}
const { unregister, isTouchDevice } = ForesightManager.instance.register({
element: LinkRef.current,
callback: () => router.prefetch(props.href.toString()),
hitSlop,
name,
unregisterOnCallback,
})
setIsTouchDevice(isTouchDevice)
return () => {
unregister()
}
}, [LinkRef, router, props.href, hitSlop, name])
return (
<Link {...props} prefetch={isTouchDevice} ref={LinkRef} className={className}>
{children}
</Link>
)
}
参考资料
https://github.com/spaansba/ForesightJS
https://foresightjs.com/docs/Behind_the_Scenes