前言
断断续续停更了好久,但是看着大家还是觉得我写的很不错,之前在简书平台的粉丝给我留言,问我为啥不写了,只想说,博主变懒了,现在再次恢复更新,重新换了一个平台,希望对大家理解和学习有帮助
React-Router 源码实现
由于最近刚刚上手React技术栈,在使用React-router 的时候经常忘记一些api,所以就有了这一篇,自己动手实现一些官方的api, 从根本上去了解底层的原理,本文使用的history模式,对于hash模式之后有时间可以补上一些实现,基本原理都差不多,实现以下api和一些hooks
BrowserRouterRouterSwitch独占路由Route路由器Link链接Redirect重定向
以及下面一些hooks
withRouteruseHistoryuseLocationuseRouteMatchuseParams
例子代码
<div className="App">
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户中心</Link>
<Link to="/login">登录</Link>
<Link to="/product/123">商品</Link>
<Switch>
<Route
exact
path="/"
// children={children}
component={HomePage}
render={render}
>
</Route>
<Route path="/user" component={UserPage}/>
<Route path="/login" component={LoginPage}/>
<Route path="/product/:id" render={() => <Product/>}/>
<Route component={_404Page}/>
</Switch>
</Router>
</div>
基本介绍
React-router 一切皆为组件的思想
- route渲染优先级 children > component > render , 三种情况都可以接受同样的[route props], 包括match, location 和 history, 如果不匹配的时候,match为null (可以做一些缓存机制)
- 如果一个route 没有path, 表示一定匹配
- Router 是所有组件的入口文件,提供了route 所需要的参数, 应用程序只会有一个高阶Router
实现基本的Link 和 Route、 BrowserRouter
BrowserRouter
BrowserRouter 作为路由的实现的一种方式,提供操作history的一些功能,简单的包括一层参数给组件
Router
import React from 'react'
import { createBrowserHistory } from 'history'
import Router from './Router'
function BrowserRouter(props) {
const history = createBrowserHistory()
return (
<Router history = {history} children={props.children} />
)
}
export default BrowserRouter
- 这里可能会想为什么需要包裹一层
Router组件,因为之前我们说过,history模式只是前端路由的一种实现方式还有hash模式等,但是对外提供的组件需要保持一致。
Router
Router 作为提供服务的最外层组件,首先我们需要明白它的作用是那些
- 对子组件route 提供match, location, history 等参数
- 全局监听路由的变化,给location重新赋值,然后让订阅了location的组件发生变化 (这里就说明了要使用Context)
import React, { useEffect, useState } from 'react'
import { RouterContext } from './Context'
function Router(props) {
const computedMatch = (pathname) => {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' }
}
const { history, children } = props
const [location, setLocation] = useState(history.location)
useEffect(() => {
let unListen = history.listen((location) => {
setLocation(location)
})
return () => {
unListen && unListen()
}
}, [history])
return (
<RouterContext.Provider value={{
location,
history,
match: computedMatch(location.pathname)
}}>
{children}
</RouterContext.Provider>
)
}
Route
Route 直接决定了要渲染哪一个组件,以及渲染的先后顺序的判断
- 渲染规则:
- 如果匹配 children > component > render > null
- 如果不匹配, 存在children就渲染它 children(function) > null
- 如果指定了path, 匹配后渲染对应的组件,如果没有指定path,则需要默认渲染
import { RouterContext } from './Context'
import matchPath from '../hcc-react-router-dom/matchPath'
function Route(props) {
const context = useContext(RouterContext)
const { location } = context
const { children, component, render, path } = props
// 由于没有path的时候,要渲染,所以必须要给没有path的组件渲染的时候一个默认值
const match = path ? matchPath(location.pathname, props) : context.match
// 更新渲染组件的props
const newProps = {
context,
...match
}
// match children > component > render > null
// no match children(func) > null
return (
match ? (children ? (typeof children === 'function' ? children(newProps) : children)
: (component ? React.createElement(component, newProps)
: render ? render(newProps) : null))
: ((typeof children === 'function') ? children(newProps) : null)
)
}
export default Route
到这里我们就基本上实现了路由之前的切换的时候渲染对应的组件, 基本原理就是根据通过history来监听url变化,如果匹配 成功就返回对应的组件,如果失败就返回null, 由于react对于null元素不进行渲染,所以就实现了只渲染匹配到的元素。
Switch
Switch 独占路由,它会渲染于地址匹配的第一个子节点<Route> 或者 <Redirect>
- Switch 的内部实现肯定会遍历子节点,然后根据location.pathname和子节点的props进行匹配, 会将多个Route子节点转换成 一个子节点,所以Route只会渲染被匹配的,其他的Router不会被渲染
// 如果添加了Switch, Route子节点只会渲染匹配的一次,如果没有的话,Route会渲染5次,然后根据规则渲染对应的Route
<Switch>
<Route
exact
path="/"
// children={children}
component={HomePage}
render={render}
>
</Route>
<Route path="/user" component={UserPage}/>
<Route path="/login" component={LoginPage}/>
<Route path="/product/:id" render={() => <Product/>}/>
<Route component={_404Page}/>
</Switch>
- 如果都没有匹配,渲染没有path的route子节点
- 由于Switch的匹配规则不一样,所以对于Route的渲染需要增加一个参数,来判断是否引入了Switch, 如果引入了就需要按照Switch 的规则 (computedMatch)
import React, { useContext } from 'react'
import matchPath from '../hcc-react-router-dom/matchPath'
import { RouterContext } from './Context'
function Switch(props) {
// 这里可以使用React.Children.forEach
const context = useContext(RouterContext)
let match, element
React.Children.forEach(props.children, (child) => {
// 如果没有匹配,并且child是合法的react元素
if (!match && React.isValidElement(child)) {
const { path } = child.props
element = child
// 如果存在path, 看看是否匹配path,如果全部都不存在的话,给默认值,用于渲染没有path的子节点
match = path ? matchPath(context.location.pathname, child.props) : context.match
}
})
return (
match ? React.cloneElement(element, {
computedMatch: match
}) : null
)
}
export default Switch
// route需要这样修改
- const match = path ? matchPath(location.pathname, props) : context.match
+ const match = computedMatch ? computedMatch : path ? matchPath(location.pathname, props) : context.match
Redirect
Redirect 使导航到一个新的地址,通过push选项来确定是否替换到路由栈中的地址
import React, { useContext, useEffect } from 'react'
import { RouterContext } from './Context'
/**
* @return {null}
*/
function Redirect(props) {
console.log('-redirect-a')
const {history} = useContext(RouterContext)
const {to, push = false} = props
useEffect(() => {
push ? history.push(to) : history.replace(to)
}, [])
return null
}
export default Redirect
Hook部分和withRouter
在我们开发应用程序的过程中,所有的组件不可能都在最外层使用,如果我们使用了嵌套组件,我们就无法获取match, location 和 history等参数,所有需要提供一些额外的hook和一些高阶组件,来方便嵌套组件来使用参数
高阶组件withRouter
我们通过高阶组件来进行包裹,提供嵌套组件所需要的api
import React from 'react'
import { RouterContext } from './Context'
function withRouter(Component) {
return (props) => {
return (
<RouterContext.Consumer>
{
context => {
return <Component {...props} {...context} />
}
}
</RouterContext.Consumer>
)
}
}
export default withRouter
一些Hook
useHistoryuseLocationuseRouterMatchuseParams
export function useHistory() {
return useContext(RouterContext).history
}
export function useLocation() {
return useContext(RouterContext).location
}
export function useRouteMatch() {
return useContext(RouterContext).match
}
export function useParams() {
const match = useContext(RouterContext).match
return match ? match.params : {}
}
问题
这样实现了一些Hook方法后,我们发现一个问题,就是嵌套组件获取params, 出现获取不到params的参数值
<Route path="/product/:id" component={Product} />
function Product(props) {
console.log('Product', props.match)
return (
<div>
<h1>Product</h1>
<Detail />
</div>
)
}
function Detail() {
const match = useRouteMatch()
console.log('detail',match)
return (
<div>
<h1>detail</h1>
</div>
)
}
在这个例子中,Detail组件获取不到路由为product/123的params的值,这个是什么原因呢?这个问题要回归到Context的本质,它提供一个全局的存放
数据的环境,我们useRouteMatch的实现,它会一直向上寻找到我们最外层的RouterContext.Provider的对象,
这个对象和Route里面合并后的对象少了一个合并matchPath解析过后的步骤,所以我们需要做一些处理,让hooks的
Context的值来至于Route中合并后的值
// .... 修改return中的值,使得在嵌套组件中获取的history,location 等参数来自于最新的合并后的值
return <RouterContext.Provider value={newProps}>
{
match ? (children ? (typeof children === 'function' ? children(newProps) : children)
: (component ? React.createElement(component, newProps)
: render ? render(newProps) : null))
: ((typeof children === 'function') ? children(newProps) : null)
}
</RouterContext.Provider>
//....
结语
这里我们就基本实现了React-router的基本api了。
- 下一篇介绍实战,通过这些api对于动态路由进行封装,实现类似于Vue中的
keep-alive - 下下篇会讲
React-redux的实现, - 下下下篇会将前端错误监控系统搭建和定位 计划都是美好的,时间就说不定了