学习react-router
源码笔记。
案例
通过一个简单的案例,引出路由的使用及源代码的实现。
案例结果
-
通过点击不同的链接,实现不同内容的展示。
-
在用户中心页面采用路由守卫功能,如果没有登录,点击用户中心时会跳转登录页面,点击登录之后跳转到用户中心页面。
- '搜索'链采用动态路由匹配,同时路由嵌套显示‘查看详情’链接,点击查看详情显示‘详情页面’。
页面布局代码
首页 --- HomePage.jsx
import React, { Component } from 'react'
export default class HomePage extends Component {
render() {
return (
<div>
<h2>首页</h2>
</div>
)
}
}
用户中心 --- UserPage.jsx
import React, { Component } from 'react'
export default class UserPage extends Component {
render() {
return (
<div>
<h2>用户中心</h2>
</div>
)
}
}
登录 --- LoginPage.jsx
登录页面使用路由守卫判断用户是否登录(isLogin), 如果登录允许用户进入当前页面,如果没有登录,显示登录界面。 点击登录按钮,模拟登录操作,在localStorage中写入{ isLogin: true }
import React, { Component } from 'react'
// import { Redirect } from 'react-router-dom'
import { Redirect } from './react-router-dom'
export default class LoginPage extends Component {
loginSubmit = (loginRedirect) => {
localStorage.setItem('isLogin', true)
window.location.replace(loginRedirect)
}
render() {
const { isLogin, location } = this.props
const { loginRedirect='/' } = location.state || {}
if(isLogin) {
// return <Redirect to={loginRedirect} />
return <Redirect to={{pathname:loginRedirect}} />
}else{
return (
<div>
<button onClick={() => this.loginSubmit(loginRedirect)}>登录</button>
</div>
)
}
}
}
路由守卫 --- Private.jsx
import React, { Component } from 'react'
// import { Redirect, Route } from 'react-router-dom'
import { Route, Redirect } from './react-router-dom'
export default class PrivateRoute extends Component {
render() {
const { isLogin, path, component} = this.props
if(isLogin) {
return <Route path={path} component={component} />
}else {
return <Redirect to={{pathname: '/login', state:{loginRedirect: path}}} />
}
}
}
路由配置 RoutePage.jsx
路由守卫组件判断 isLogin 如果为true允许用户进入当前页面,如果为 false 重定向到登录页面
import React, { Component } from 'react'
// import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom'
import { BrowserRouter as Router, Link, Route, Switch } from './react-router-dom'
import HomePage from './HomePage'
import LoginPage from './LoginPage'
import Private from './Private'
import UserPage from './UserPage'
export default class RouterPages extends Component {
// 获取登录
render() {
return (
<div>
<Router>
<Link to='/'>首页 </Link>
<Link to='/user'> 用户中心 </Link>
<Link to='/login'> 登录 </Link>
<Link to='/children'> children </Link>
<Link to='/render'> render </Link>
<Link to='/search/123'> 搜索 </Link>
{/* <Switch location={{pathname: '/user'}}> */}
<Switch>
<Route path='/' component={HomePage} exact/>
{/* <Route path='/user' component={UserPage} /> */}
<Route path='/children' children={() => (<div>children</div>)} />
{/* <Route path='/children' children={'children'} /> */}
<Route path='/render' render={() => <div>render</div>} />
<Route path='/search/:id' component={Search} />
{/* 从本地存储中获取isLogin 测试路由守卫功能 */}
<Private path='/user' isLogin={localStorage.getItem('isLogin')} component={UserPage} />
<Route path='/login' component={LoginPage} />
<Route render={() => <div>404</div> } />
</Switch>
</Router>
</div>
)
}
}
function Search(props) {
const { id } = props.match.params
return (
<div>
搜索 动态路由--- {id} <br/>
<Link to='/search/123/detail'>查看详情</Link>
<Route path='/search/:id/detail' component={Detail} />
</div>
)
}
function Detail() {
return (
<h2>详情页面</h2>
)
}
react-router源代码实现
创建context
import React from 'react';
export const RouterContext = React.createContext()
BrowserRouter
import React, { Component } from 'react'
import {createBrowserHistory} from 'history'
import { RouterContext } from './RouteContext'
class BrowserRouter extends Component {
// 初始化一个match对象,路由匹配对象,当路由匹配(<Route />)成功之后返回一个match对象,匹配不成功返回null
static computeRootMatch(pathname) {
return {
path: '/',
url: '/',
params: {},
isExact: pathname === '/'
}
}
constructor(props) {
super(props)
// 利用react-router提供的createBrowserHistory创建一个功能兼容性良好的history对象
this.history = createBrowserHistory()
this.state = {
location: this.history.location
}
// 监听location的改变
this.unListen = this.history.listen(location => {
this.setState({location})
})
}
componentWillUnmount() {
// 当组件卸载时,卸载路由监听
console.log('this.unListen', this.unListen)
if(this.unListen) this.unListen()
}
render() {
console.log('当前路由', this.state.location)
// 利用context 实现 history location match对象的共享
return (
<RouterContext.Provider value={{
history: this.history,
location: this.state.location,
match: BrowserRouter.computeRootMatch(this.state.location.pathname)
}}>
{this.props.children}
</RouterContext.Provider>
)
}
}
export {BrowserRouter}
Link
import React, { Component } from 'react'
import { RouterContext } from './RouteContext';
class Link extends Component {
handleClick = (e, history) => {
// 使用a标签实现Link标签,阻止a标签的默认行为 通过 history.push 方法实现跳转
e.preventDefault();
history.push(this.props.to)
}
render() {
const { to, children} = this.props
return (
<RouterContext.Consumer>
{
context => (<a href={to} onClick={(e) => this.handleClick(e, context.history)}>{children}</a>)
}
</RouterContext.Consumer>
)
}
}
export {Link}
Switch
import React, { Component } from 'react'
import matchPath from './matchPath'
import { RouterContext } from './RouteContext'
class Switch extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
console.log('Switch', context, this.props)
/*
Switch
{history: {…}, location: {…}, match: {…}}
history: {length: 50, action: "PUSH", location: {…}, createHref: ƒ, push: ƒ, …}
location: {pathname: "/login", search: "", hash: "", state: undefined, key: "pxbkis"}
match: {path: "/", url: "/", params: {…}, isExact: false}
__proto__: Object
*/
// 找出
let element, match
// const {location} = context
// 优先使用props上的location:<Switch location={{pathname: '/user'}}>
const location = this.props.location || context.location
const { children } = this.props
// React提供的数组遍历的方式
React.Children.forEach(children, child => {
if(match == null && React.isValidElement(child)) {
// <Route path='/' component={HomePage} exact/>
element = child
const path = child.props.path
console.log('child', child, path)
// location.pathname 用户点击的链接的pathname
// path : 遍历children时的每一个child的路径 比如:<Route path='/' component={HomePage} exact/> path: '/'
match = path ? matchPath(location.pathname, {...child.props, path}) : context.match
}
})
// cloneElement(element, otherProps)
// createElement(element.type, props)
return match ? React.cloneElement(element, {location, computedMatch: match}) : null
}
}
</RouterContext.Consumer>
)
}
}
export {Switch}
matchPath
复制的源码中的代码
import pathToRegexp from "path-to-regexp";
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
if (pathCache[path]) return pathCache[path];
const keys = [];
const regexp = pathToRegexp(path, keys, options);
const result = { regexp, keys };
if (cacheCount < cacheLimit) {
pathCache[path] = result;
cacheCount++;
}
return result;
}
/**
* Public API for matching a URL pathname to a path.
*/
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
export default matchPath;
Route
import React, { Component } from 'react'
import { RouterContext } from './RouteContext';
import mathPath from './matchPath'
class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
console.log('Route', context)
const { path, computedMatch, children, component, render } = this.props
// 覆盖组件调用时用户传递的location
const location = this.props.location || context.location
const match = computedMatch ? computedMatch : path ? mathPath(location.pathname, this.props) : context.match
console.log('Route match', match)
/*
match:
{path: "/", url: "/", isExact: true, params: {…}}
isExact: true
params: {}
path: "/"
url: "/"
__proto__: Object}
如果是动态路由:
Route match
{path: "/search/:id/detail", url: "/search/123/detail", isExact: true, params: {…}}
isExact: true
params: {id: "123"}
path: "/search/:id/detail"
url: "/search/123/detail"
__proto__: Object}
*/
// children, component, render 能接收到 history、 location、match
// 所以我们定义props,传下去
const props = {
...context,
location,
match
}
// match 渲染 children component render 或者 null
// match的时候如果children 存在:function 或者 children 本身
// 不match 渲染 children() 或者 null
// children 是和match无关的
return (
// 对于嵌套路由用里层的参数覆盖掉上一层的参数,使内层路由拿到最新的值。
// 在使用context时,拿到的是最近的一层Provider,如果不写,当前<Route />的内层路由就只能获取到<Route />外层Provider的数据
<RouterContext.Provider value={props}>
{
match ?
(children
?
(typeof children === 'function' ? children(props) : children)
:
(component ? (React.createElement(component, props)) : (render ? render(props) : (null)))
)
:
(typeof children === 'function' ? children(props) : null)
}
</RouterContext.Provider>
)
}
}
</RouterContext.Consumer>
)
}
}
export {Route}
Redirect
import React, { Component } from 'react'
import { RouterContext } from './RouteContext'
class Redirect extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
const { history } = context
const { to } = this.props
return <LifeCycle onMount={() => history.push(to)}></LifeCycle>
}
}
</RouterContext.Consumer>
)
}
}
class LifeCycle extends Component {
componentDidMount() {
if(this.props.onMount) {
this.props.onMount()
}
}
render() {
return null
}
}
export {Redirect}
导出react-router方法
export {BrowserRouter} from './BrowserRouter'
export {Link} from './Link'
export {Route} from './Route'
export {Switch} from './Switch'
export {Redirect} from './Redirect'
总结:
- 页面刚加载时,遇到第一个组件 BrowserRoute:
- BrowserRouter 中初始化一个
match
对象
static computeRootMatch(pathname) {
return {
path: '/',
url: '/',
params: {},
isExact: pathname === '/'
}
}
- 通过
createBrowserHistory()
创建一个 history 对象
this.history = createBrowserHistory()
- 在state中创建一个
{ location: this.history.location }
(从history对象中单独拿出location) - 监听 history 的变化,同时修改 state中的location
constructor(props) {
super(props)
this.history = createBrowserHistory()
this.state = {
location: this.history.location
}
this.unListen = this.history.listen(location => {
this.setState({location})
})
}
componentWillUnmount() {
console.log('this.unListen', this.unListen)
if(this.unListen) this.unListen()
}
- 使用 context ,将 history location match 传递下去
render() {
return (
<RouterContext.Provider value={{
history: this.history,
location: this.state.location,
match: BrowserRouter.computeRootMatch(this.state.location.pathname)
}}>
{this.props.children}
</RouterContext.Provider>
)
}
- 第二个组件 Switch 组件:
- 获取到 Switch 组件标签中间所有的 Route 组件
<Switch>
<Route path='/' component={HomePage} exact/>
{/* <Route path='/user' component={UserPage} /> */}
<Route path='/children' children={() => (<div>children</div>)} />
{/* <Route path='/children' children={'children'} /> */}
<Route path='/render' render={() => <div>render</div>} />
<Route path='/search/:id' component={Search} />
<Private path='/user' isLogin={localStorage.getItem('isLogin')} component={UserPage} />
<Route path='/login' component={LoginPage} />
<Route render={() => <div>404</div> } />
</Switch>
-
使用
React.Children.forEach()
遍历每个 Route 组件,得到匹配成功的 Route 赋值 element -
同时利用 matchPath() 方法得到匹配成功的 match 对象。如果没有匹配成功 element 就是
<Route render={() => <div>404</div> } />
, match 取context.match
就是自定义的 match 对象 -
最后利用 React.cloneElement() 返回 匹配成功的 Route 组件 或者
<Route render={() => <div>404</div> } />
context => {
console.log('Switch', context, this.props)
/*
Switch
{history: {…}, location: {…}, match: {…}}
history: {length: 50, action: "PUSH", location: {…}, createHref: ƒ, push: ƒ, …}
location: {pathname: "/login", search: "", hash: "", state: undefined, key: "pxbkis"}
match: {path: "/", url: "/", params: {…}, isExact: false}
__proto__: Object
*/
// 找出
let element, match
// const {location} = context
// 优先使用props上的location:<Switch location={{pathname: '/user'}}>
const location = this.props.location || context.location
const { children } = this.props
React.Children.forEach(children, child => {
if(match == null && React.isValidElement(child)) {
// <Route path='/' component={HomePage} exact/>
element = child
const path = child.props.path
console.log('child', child, path)
// location.pathname 用户点击的链接的pathname
// path : 遍历children时的每一个child的路径 比如:<Route path='/' component={HomePage} exact/> path: '/'
match = path ? matchPath(location.pathname, {...child.props, path}) : context.match
}
})
// cloneElement(element, otherProps)
// createElement(element.type, props)
return match ? React.cloneElement(element, {location, computedMatch: match}) : null
}
- 第三个组件 Route 组件
- 根据点击不同的链接(地址栏中最新的信息),覆盖掉原来的 location match 信息
context => {
console.log('Route', context)
const { path, computedMatch, children, component, render } = this.props
// const match = context.location.pathname === path
// return match ? React.createElement(component, this.props) : null
// 覆盖组件调用时用户传递的location
const location = this.props.location || context.location
const match = computedMatch ? computedMatch : path ? mathPath(location.pathname, this.props) : context.match
console.log('Route match', match)
/*
match:
{path: "/", url: "/", isExact: true, params: {…}}
isExact: true
params: {}
path: "/"
url: "/"
__proto__: Object}
如果是动态路由:
Route match
{path: "/search/:id/detail", url: "/search/123/detail", isExact: true, params: {…}}
isExact: true
params: {id: "123"}
path: "/search/:id/detail"
url: "/search/123/detail"
__proto__: Object}
*/
// children, component, render 能接收到 history、 location、match
// 所以我们定义props,传下去
const props = {
...context,
location,
match
}
- 处理 Route 中使用 children component render 三种不同方式加载组件的情况
- 同时将最新的 location history match 传递下去
<RouterContext.Provider value={props}>
{
match ?
(
children
?
(typeof children === 'function' ? children(props) : children)
:
(component ? (React.createElement(component, props)) : (render ? render(props) : (null)))
)
:
(typeof children === 'function' ? children(props) : null)
}
</RouterContext.Provider>
- Redirect 组件
- 从上下文对象中获取 history 对象,同时获取到 to 属性值
class Redirect extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
const { history } = context
const { to } = this.props
return <LifeCycle onMount={() => history.push(to)}></LifeCycle>
}
}
</RouterContext.Consumer>
)
}
}
- 利用 history.push 方法,在组件componentDidMount生命周期中将路径重定向
class LifeCycle extends Component {
componentDidMount() {
if(this.props.onMount) {
this.props.onMount()
}
}
render() {
return null
}
}