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

相关推荐
骑驴看星星a28 分钟前
Vue中的scoped属性
前端·javascript·vue.js
四月_h35 分钟前
在 Vue 3 + TypeScript 项目中实现主题切换功能
前端·vue.js·typescript
qq_4275060841 分钟前
vue3写一个简单的时间轴组件
前端·javascript·vue.js
雨枪幻。2 小时前
spring boot开发:一些基础知识
开发语言·前端·javascript
lecepin2 小时前
AI Coding 资讯 2025.8.27
前端·ai编程
TimelessHaze3 小时前
拆解字节面试题:async/await 到底是什么?底层实现 + 最佳实践全解析
前端·javascript·trae
执键行天涯3 小时前
从双重检查锁定的设计意图、锁的作用、第一次检查提升性能的原理三个角度,详细拆解单例模式的逻辑
java·前端·github
青青子衿越3 小时前
微信小程序web-view嵌套H5,小程序与H5通信
前端·微信小程序·小程序
OpenTiny社区3 小时前
TinyEngine 2.8版本正式发布:AI能力、区块管理、Docker部署一键强化,迈向智能时代!
前端·vue.js·低代码
qfZYG4 小时前
Trae 编辑器在 Python 环境缺少 Pylance,怎么解决
前端·vue.js·编辑器