使

使用事件钩子在 React 中构建客户端路由器(pt1:事件)

2025-06-09

使用事件钩子在 React 中构建客户端路由器(pt1:事件)

TLDR;

我正在制作一个可插拔的小部件组件,它包含前端和无服务器后端部分。本文是本系列的第一篇,介绍了如何在 React 中使用自定义事件来构建路由器。

  • 在 React 中处理事件
  • 引发自定义事件

概述

我正在着手一个合作项目,为最近加入的4C内容创建者社区构建一个无服务器小部件。

这个项目首先需要一个客户端路由器,而我原本打算用 React,所以第一个想到的就是 React Router。但后来我又想,它只是一个客户端路由器,这或许是一个深入探究问题核心、让我更深入理解路由器的有趣机会。

React Router 还有一点我不太喜欢。我总是会写一个包装器来包装它,这样就能以声明式的方式动态注册路由,而不是在 JSX 中命令式地写路由。

// What I want

import "./something-that-declares-routes.js"

register("/some/route/:id", <SomeComponent color="blue"/>)

export default function App() {
    return <Router />
}

Enter fullscreen mode Exit fullscreen mode
// Rather than

import "./something-that-declares-routes.js"
import {declaredRoutes} from "./declared-routes.js"

export default function App() {
     return <Router>
         <SomeComponent color="blue" path="/some/route/:id" />
         {declaredRoutes.map((route) => (<route.Component 
            key={route.path} path={route.path}/>)}
    </Router>
}
Enter fullscreen mode Exit fullscreen mode

什么是路由器?

那么,我们想从路由器中得到什么呢?我们希望能够指定提供给应用的 URL 模式,以便将它们转换为可以调用的函数。该函数还应该能够从路由中获取参数,如下所示:

   /some/:id/route?search&sort
Enter fullscreen mode Exit fullscreen mode

使用参数调用一些已注册的函数或组件id,例如searchsort/some/abc123/route?search=something&sort=name,desc

register("/some/:id/route?search&sort", <ShowInfo color="blue"/>)

function ShowInfo({id, search, sort, color}) {
   return /* something */
}
Enter fullscreen mode Exit fullscreen mode

URL

因此,为了使路线正常工作,我们必须处理window.location对象并知道它何时发生变化......要么是因为我们自己导航,要么是用户按下了“后退”“前进”按钮。

从中location我们需要根据匹配路由pathname并从中提取变量pathnamesearch属性来传递给我们的组件。

当用户使用按钮导航时,浏览器会给我们一个onpopstate事件,但是没有导航到新 URL 的事件,所以我们必须自己处理。

活动

onpopstate当用户使用链接浏览我们的应用程序时,让我们通过伪造事件来保持代码简单。

我喜欢事件,我在代码中处处使用事件来松散耦合组件。我们上面已经看到,我们需要频繁地触发和处理事件,因此第一步就是构建一些工具来辅助这个过程。

在本文的第一部分中,我们将创建一些有用的函数来引发和处理 React 组件内部和外部的事件。

计划

因为我们正在处理浏览器标准事件,所以我决定直接将现有的方法直接应用window到服务中。但是,我希望能够将自定义属性作为附加参数传递给处理函数,而不是创建几十个自定义事件,所以我们将使用Event随事件传递的参数来装饰标准实例,这样做是为了避免意外地与任何标准属性冲突。

 处理事件

我们的第一个函数是:附加一个处理程序并处理这些额外的属性,然后返回一个方法以便稍后分离处理程序。

export function handle(eventName, handler) {
  const innerHandler = (e) => handler(e, ...(e._parameters || []))
  window.addEventListener(eventName, innerHandler)
  return () => window.removeEventListener(eventName, innerHandler)
}
Enter fullscreen mode Exit fullscreen mode

在这里,我们创建一个内部处理程序,它使用_parameters事件对象上的属性将附加参数传递给处理程序。

将其变成 React 的钩子就简单多了:

export function useEvent(eventName, handler) {
  useLayoutEffect(() => {
    return handle(eventName, handler)
  }, [eventName, handler])
}
Enter fullscreen mode Exit fullscreen mode

引发事件

编写一个函数来使用自定义参数引发这些事件也很容易:

export function raise(eventName, ...params) {
  const event = new Event(eventName)
  event._parameters = params
  window.dispatchEvent(event)
  return params[0]
}
Enter fullscreen mode Exit fullscreen mode

注意我们如何返回第一个参数 - 这是一个控制反转助手,我们可能会引发寻找返回值的事件,这为我们提供了一种简单的方法。

handle("get-stuff", (list)=>list.push("I'm here"))
// ...
handle("get-stuff", (list)=>list.push("Another choice"))
// ...
for(let stuff of raise("get-stuff", [])) {
   console.log(stuff)
}
Enter fullscreen mode Exit fullscreen mode

通过返回第一个参数,我们可以少写很多样板代码。

当我们处理类似事件时,onPopState我们还想用参数来装饰事件对象(例如state),location所以我们确实需要另一个函数来处理这种情况,我们会不时地使用它:

export function raiseWithOptions(eventName, options, ...params) {
  const event = new Event(eventName)
  Object.assign(event, options)
  event._parameters = params
  window.dispatchEvent(event)
  return params[0]
}
Enter fullscreen mode Exit fullscreen mode

这个非常相似,只是它用传入的选项对象装饰自定义事件。

奖励:事件发生时重新绘制事物

我们可能希望让 React 组件根据改变全局状态的事件进行重绘。有一个简单的方法可以实现这一点:使用一个useRefresh钩子,它可以触发刷新,或者注册一个函数,在调用子函数后刷新。

import { useEffect, useMemo, useRef, useState } from "react"

export function useRefresh(...functions) {
    const [, refresh] = useState(0)
    const mounted = useRef(true)
    useEffect(() => {
        mounted.current = true
        return () => (mounted.current = false)
    }, [])
    const refreshFunction = useMemo(
        () =>
            (...params) => {
                if (params.length === 1 && typeof params[0] === "function") {
                    return async (...subParams) => {
                        await params[0](...subParams)
                        refreshFunction()
                    }
                }
                for (let fn of functions) {
                    if (fn) {
                        fn(...params)
                    }
                }
                if (mounted.current) {
                    refresh((i) => i + 1)
                }
            },
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [...functions]
    )
    return refreshFunction
}

Enter fullscreen mode Exit fullscreen mode

这为我们创建了一个实用函数,用于使 React 重绘组件。它在很多情况下都很方便,但在这里我们只用它来执行事件刷新:

function Component() {
   const refresh = useRefresh()
   useEvent("onPopState", refresh)
   return null
}
Enter fullscreen mode Exit fullscreen mode

useRefresh函数接受一个要调用的其他函数列表。这有时很有用,尤其是在调试时

    const refresh = useRefresh(()=>console.log("Redrawing X"))
Enter fullscreen mode Exit fullscreen mode

并且返回的函数可以包装某些内容进行刷新:

function Component() {
     const refresh = useRefresh()
     // do something with global state on window.location.search
     return <button onClick={refresh(()=>window.location.search = "?x"}>Set X</button>
}
Enter fullscreen mode Exit fullscreen mode

结论

在第一部分中,我们了解了如何在 React 中轻松触发和处理事件。下面是使用这些技术的运行小部件。

链接:https://dev.to/miketalbot/building-a-client-side-router-in-react-with-event-hooks-pt1-events-56m7
PREV
使用 Firebase 的无服务器应用程序
NEXT
初级开发人员的时间管理