谁说 fre 没有 router? 100 行代码实现 router

如题,大家好久不贱,这是一篇关于前端路由的文章

前情提要

前端路由的跳转有两种

预加载模式:一种是点击一个按钮,显示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 同样适用

github.com/frejs/fre

俺现在有书读了,fre 最近也更新了一些,支持了 Suspense 和 lazy,大家感兴趣可以点个 star

相关推荐
拾光拾趣录14 分钟前
🔥99%人答不全的安全链!第5问必翻车?💥
前端·面试
IH_LZH18 分钟前
kotlin小记(1)
android·java·前端·kotlin
lwlcode26 分钟前
前端大数据渲染性能优化 - 分时函数的封装
前端·javascript
Java技术小馆27 分钟前
MCP是怎么和大模型交互
前端·面试·架构
玲小珑31 分钟前
Next.js 教程系列(二十二)代码分割与打包优化
前端·next.js
coding随想40 分钟前
HTML5插入标记的秘密:如何高效操控DOM而不踩坑?
前端·html
༺ཌༀ傲世万物ༀད༻40 分钟前
前端与后端部署大冒险:Java、Go、C++三剑客
java·前端·golang
源力祁老师1 小时前
Odoo OWL前端框架全面学习指南 (后端开发者视角)
前端框架
TheRedAce1 小时前
状态未保存,拦截页面跳转通用方法
前端
袁煦丞1 小时前
小雅全家桶+cpolar影音库自由随身:cpolar内网穿透实验室第519个成功挑战
前端·程序员·远程工作