如题,大家好久不贱,这是一篇关于前端路由的文章
前情提要
前端路由的跳转有两种
预加载模式:一种是点击一个按钮,显示loading条,等全都准备好了,再跳过去,比如github就是这种,这在wordpress+htmx的世界里也比较常见,也就是先加载,后跳转
即时跳转模式:另一种是点击按钮就立即跳转,然后数据没准备好就展示一个loading按钮,这种在国内的 spa 中比较常见,也就是先跳转,后加载
知道这一点,我们对路由跳转就大概有数了,我们今天就来探讨这两种跳转模式的结合方式
其中即时跳转模式,可以通过 useEffect(数据) + Suepense(lazy组件) 实现,而预加载模式,通常在 react 的语境里,需要使用类似 startTransition 等实现
remix loader api
remix 提供了一种新的思路,就是给每一个路由提供一个 loader 函数,我们可以在路由中先请求 loader 函数,再渲染组件,以此来做到"预加载模式"
好的,复习结束,接下来上代码
实现
js
const routes = [
{
path: '/login',
element: import('./login/login'), // without loader
},
{
path: '/',
element: import('./home/home'),
loader: homeLoader // loader
},
{
path: '/uu/:id',
element: import('./user/user'),
loader: userLoader
},
]
const App = () => {
return <main>
<Router routes={routes}/>
</main>
}
如上,login 组件没有 loader,那就立即跳转,而 home 和 user 组件有 loader,则需要先预加载,再跳转
fre-router 最终代码
js
import { useState, useEffect, Suspense, lazy, useMemo, useRef } from 'fre'
function pathSlice(path) {
const slice = [
new RegExp(
`${path.substr(0, 1) === '*' ? '' : '^'}${path
.replace(/:[a-zA-Z]+/g, '([^/]+)')
.replace(/\*/g, '')}${path.substr(-1) === '*' ? '' : '$'}`
)
]
const params = path.match(/:[a-zA-Z#0-9]+/g)
slice.push(params ? params.map(name => name.substr(1)) : [])
return slice
}
export function useRouter(routes) {
const [url, setUrl] = useState(window.location.pathname)
const parsedRoutes = useMemo(() => {
return routes.map(item => ({
route: pathSlice(item.path),
handler: lazy(() => item.element),
loader: item.loader
}))
}, [routes])
useEffect(() => {
const handlePopState = () => {
setUrl(window.location.pathname)
}
window.addEventListener('popstate', handlePopState)
return () => window.removeEventListener('popstate', handlePopState)
}, [])
return { url, routes: parsedRoutes }
}
export default function Router({ routes, fallback }) {
const { url, routes: parsedRoutes } = useRouter(routes)
const [match, setMatch] = useState(null)
const [pending, setPending] = useState(false)
const requestId = useRef(0)
useEffect(() => {
const currentUrl = url === '' ? '/' : url
const currentRequestId = ++requestId.current
const matchedRoute = parsedRoutes.find(({ route }) => {
const [reg] = route
return currentUrl.match(reg)
})
if (!matchedRoute) {
setMatch(null)
setPending(false)
return
}
const [reg, params] = matchedRoute.route
const res = currentUrl.match(reg)
const newProps = {}
if (params.length > 0 && res) {
params.forEach((prop, index) => (newProps[prop] = res[index + 1]))
}
const loadRoute = async () => {
if (currentRequestId !== requestId.current) return
setPending(true)
let data = null
if (typeof matchedRoute.loader === 'function') {
try {
data = await matchedRoute.loader(newProps)
} catch (error) {
console.error('Loader error:', error)
}
}
if (currentRequestId === requestId.current) {
setMatch({
Component: matchedRoute.handler,
props: { data, ...newProps },
})
setPending(false)
}
}
loadRoute()
}, [url, parsedRoutes])
return (
<Suspense fallback={fallback}>
{pending && fallback}
{match && <match.Component {...match.props} />}
</Suspense>
)
}
export const push = (path) => {
if (path.startsWith('.')) {
path = window.location.pathname + path.slice(1)
}
if (window.location.pathname !== path) {
history.pushState({}, '', path)
window.dispatchEvent(new PopStateEvent('popstate'))
}
}
export const getPath = () => window.location.pathname
最终效果:www.ixipi.net
大家可以自己跳转试试看,效果还是很赞的,虽然是 fre 的代码,但 react 同样适用
俺现在有书读了,fre 最近也更新了一些,支持了 Suspense 和 lazy,大家感兴趣可以点个 star