谁说 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

相关推荐
gnip7 小时前
企业级配置式表单组件封装
前端·javascript·vue.js
一只叫煤球的猫8 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
excel9 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼9 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
hj5914_前端新手13 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法13 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku13 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode13 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu13 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu13 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript