Skip to content

SikyChen/simple-react-router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

logo

React-Router-Dom 的简易实现

目录

  1. Router 的功能点分析
    1. 如何修改 URL ?
    2. 如何监听 URL 变化并匹配路由?
  2. 实现-1
    1. 实现 handleLink 功能
    2. 实现 refresh 功能
  3. 重构
    1. 实现 BrowserRouter 容器
    2. 实现 Link 组件
    3. 实现 Routes Route 组件

React Router 的实现原理

本质上,前端路由所提供的,就是一个 修改 URL渲染与 URL 对应组件 的功能集合。

功能分析

首先来看下 Router 实现的基础功能:

  1. 修改 URL ;
  2. 监听 URL 的变化,并根据新的 URL 来匹配对应的路由组件进行渲染;

如何修改 URL ?

修改 URL 很容易,用 a 标签或者修改 window.location.href 都可以实现。但用这些方法修改 URL 后,浏览器会向服务端发送请求,引起页面的刷新。这不符合 Router 功能的需求。

但通过以下两个办法,可以做到修改 URL 且不发送请求:

  1. 修改 hash
  2. 通过 historypushStatereplaceStatego 这几个 API 来修改 URL ;

可以发现,上面两种修改 URL 的方式,刚好对应了 React-Router-Dom 的两种模式: Hash模式History模式

如何监听 URL 变化并匹配路由?

  1. 页面跳转或刷新后,即新打开页面时:遍历所有路由组件,将 location.pathname === path 的路由组件渲染出来即可;
  2. 若是通过 history API 修改的 URL ,则可以在修改 URL 时,同步调用匹配路由的方法,以触发匹配;当然更好的方法时单独管理一份 location 的状态,修改 URL 时更新该 location 状态,所有引用该状态的路由组件,自动触发重新渲染,进而自动重新匹配。

下面是这个过程的一个流程图

下面将通过 history API 来实现一个基础的 History模式Router 功能,并支持如下路由配置:

const routes = [
  {
    path: '/aa',
    component: <AA />,
  },
  {
    path: '/bb',
    component: <BB />,
  },
  {
    path: '/cc',
    component: <CC />,
  },
  {
    path: '/dd',
    component: <DD />,
  },
]

简易实现

对于上述的 routes 配置,搭配如下代码结构进行演示。

export default function App() {

  // 匹配成功后,需要渲染的组件
  const [content, useContent] = useContent(null)

  //...

  return (
    <>
      <div className="menu">
        <ul>
          <li><a href="/aa" onClick={handleLink}>AA</a></li>
          <li><a href="/bb" onClick={handleLink}>BB</a></li>
          <li><a href="/cc" onClick={handleLink}>CC</a></li>
          <li><a href="/dd" onClick={handleLink}>DD</a></li>
        </ul>
      </div>
      <div className="content">
        {content}
      </div>
    </>
  )
}

实现 handleLink

前面功能分析时说过,路由的第一个功能点是修改 URL ,而 handleLink 方法的目的,就是修改 URL 。

handleLink 方法,是绑定在 a 标签上的,点击 a 标签的默认行为,会造成浏览器发起请求进而刷新页面,不符合前端路由的需求。所以这里用 event.preventDefault() 来禁止默认行为;

禁用默认行为后,再通过 history.pushState 方法修改 URL ;

URL 被修改后,需要同步调用匹配路由的方法,以渲染匹配的路由组件,这里给该方法取名为 refresh ,意为匹配路由并渲染。

代码如下:

function handleLink(e) {
  e.preventDefault()
  window.history.pushState({}, '', e.target.href)
  refresh()
}

实现 refresh

匹配路由并渲染的过程如下:

  1. 获取当前 URL 的 path;
  2. 通过 path 跟 routes 中的所有 route.path 进行对比,匹配对应 route;
  3. 将匹配到的 route 对应的组件,通过 setContent 设为当前显示的组件,触发重新渲染;

根据上述过程,代码如下

function refresh() {
  const pathname = window.location.pathname
  const content = routes.find(route => route.path === pathname)
  setContent(content)
}

现在已经基本实现了点击 a 标签,切换路由的功能。不过还差一点,当刷新页面或者前进后退时,会把页面重置为初始状态,这是因为在刷新页面后并未进行匹配,加上对页面刷新的监听即可:

useEffect(() => {
  refresh() // 刷新时触发匹配
  window.addEventListener('popstate', () => { refresh() })  // 浏览器前进后退时触发匹配
}, [])

完整代码可以参考 ReactRouterDemo 组件。

重构

现在来看一下 React-Router-Dom 的用法,参考该用法对我们的路由功能进行封装:

export default function App() {
  return (
    <BrowserRouter>
      <div>
        <div>
          <Link to="/aa">AA</Link>
          <Link to="/bb">BB</Link>
          <Link to="/cc">CC</Link>
          <Link to="/dd">DD</Link>
        </div>

        <Routes>
          <Route path="/aa"><AA /></Route>
          <Route path="/bb"><BB /></Route>
          <Route path="/cc"><CC /></Route>
          <Route path="/dd"><DD /></Route>
        </Routes>
      </div>
    </BrowserRouter>
  );
}

从代码中可以看到,组合使用 BrowserRouter Link Routes Route 这几个组件即可实现一个简单的路由功能,而我们在上面简易实现当中所提到的 handleLink refresh 等逻辑都不需要编写,说明这些逻辑都被封装进组件当中了。

功能分析

重新对 修改 URL监听 URL 变化并匹配路由 的过程进行分析:

点击 Link 组件会改变 URL ,类似我们之前实现过的 handleLink 的逻辑。对于 history.pushState() 对 URL 作出的改变无法监听的问题,这次我们使用状态管理的办法进行解决。

location 作为一份状态进行管理,当 Link 组件对该状态修改后,引入该状态的 Routes 将自动触发重新渲染,进而重新匹配 Route 进行显示。

实现 BrowserRouter 组件

通过上面的分析,可以想到 BrowserRouter 作为容器组件的作用,就是将 location 状态放在 BrowserRouter 中进行初始化,然后通过 context 共享给其子组件。示例代码如下:

// context.js
export const LocationContext = React.createContext(null)

// BrowserRouter.jsx
export default function BrowserRouter(props) {

  // ...

  const [location, setLocation] = useState(window.location)

  return (
    <LocationContext.Provider value={location}>
      {props.children}
    </LocationContext.Provider>
  )
}

由于 handleLink 时,需要对 location 进行修改,所以这里再多定义一个 context 用于将修改 location 的方法传给子组件,并补充上修改调用 history.pushState 方法的相关代码如下:

// context.js
export const LocationContext = React.createContext(null)
export const NavigatorContext = React.createContext(null)

// BrowserRouter.jsx
export default function BrowserRouter(props) {

  // ...

  const [location, setLocation] = useState(window.location)
  const navigator = {
    push: handlePush,
  }

  useEffect(() => {
    // 浏览器前进后退时触发更新
    window.addEventListener('popstate', updateLoaction)
  }, [])

  function updateLoaction() {
    setLocation({...window.location})
  }

  function handlePush(path) {
    // 保留 ?search 和 #hash
    const searchAndHash = location.href.split(location.pathname)[1];
    window.history.pushState({}, '', path + searchAndHash)
    updateLoaction()
  }

  return (
    <LocationContext.Provider value={location}>
      <NavigatorContext.Provider value={navigator}>
        {props.children}
      </NavigatorContext.Provider>
    </LocationContext.Provider>
  )
}

实现 Link 组件

现在只需要在 Link 组件中,调用 navigator.push 即可达到修改 URL 的目的了,所以对 Link 组件的实现如下:

// Link.jsx
export default const Link = React.forwardRef((props, ref) => {

  const navigator = React.useContext(NavigatorContext);

  function handleClick(e) {
    e.preventDefault()
    navigator.push(props.to)
  }

  return <a href={props.to} onClick={handleClick} ref={ref}>{props.children}</a>
})

实现 Routes Route 组件

为了方便使用,将 path 定义在 Route 上了,那么 BrowserRouter 容器组件很难知道都定义了那些 path ,也不方便跨多层控制。所以再新增一个 Routes 容器专门对匹配和渲染 Route 进行管理。

那么 Routes 的功能也呼之欲出了,就是读取 location 状态,拿到 pathname ,然后再遍历所有 Route 组件并比较其 path ,将匹配的 Route 显示即可,代码如下:

// Routes.jsx
export default const Routes = (props) => {

  const location = React.useContext(LocationContext);

  const route = useMemo(() => {
    return props.children.filter(child => child.props.path === location.pathname)
  }, [props.children, location.pathname])

  return route
}

Route 本身则比较简单,只需要将其子组件返回即可:

export default const Route = (props) => props.component

至此,使用组件的方式实现路由功能,就已经完成了。

结语

通过这次学习,揭开了 React Router 神秘的面纱,了解了前端路由的主流程并完成了一个简易实现。

基于这些核心逻辑,再去看 React-Router-Dom 的源码,相信会更容易理解些。

另外,本文所涉及的所有代码,可以在 simple-react-router 中查看。

About

简易实现 react-router-dom

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published