实现基本的react-router-dom
react router提供native 和 h5的两套router,这里只涉及h5方面的,所以实现的是react-router-dom。
要求
需要了解react-router-dom的基本使用
常用api
- BrowserRouter
- Router
- Route
- Link
- Switch
...
实现思路:
基本想法: 通过url的变化, 触发相应页面渲染
实现路径:
- 创建一个全局唯一的Router组件,做为顶级组件(这里使用BrowserRouter)。 提供一个context(上下文)包含{ location, match, history }字段。
- 点击路由(Link)跳转时, Router会根据url 去匹配相应的路由(Route), 匹配正确的路由渲染。
- 路由(Route)提供3种props方式渲染组件: children、component和render。
- Switch 保证每次只有一个路由渲染。
案例:
关键点
-
在一个项目中只有一个Router,提供顶层的router context (location, match, history)。 在h5中, 常用的Router的形式有BrowserRouter、HashRouter、MemoryRouter。在这里只实现BrowserRouter形式。
-
Switch组件保证只渲染匹配到的第一个Route组件。
-
Route组件提供三种props(children,component,render)来指定渲染的组件, 优先级是 children > component > render
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
- 在非Switch组件中, 未提供path的Route一直都渲染。
实现步骤
实现Router的上下文对象(Router、BrowserRouter)
Router
基本目的就是提供全局的router context(location, match, history)。
该组件接受history作为props, 根据history来生成context。
import React from "react"
import RouterContext from "./RouterContext"
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" }
}
constructor(props) {
super(props)
this.state = { location: props.history.location }
this.unlisten = props.history.listen((location) => {
this.setState({ location })
})
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten()
}
}
render() {
return (
<RouterContext.Provider
value={{
location: this.state.location,
history: this.props.history,
match: Router.computeRootMatch(this.state.location.pathname),
}}
>
{this.props.children}
</RouterContext.Provider>
)
}
}
export default Router
RouterContext
import React from 'react';
const RouterContext = React.createContext()
export default RouterContext
BrowserRouter
这里引入history包,管理浏览器history对象的一致性。因为是BrowserRouter组件,所以引入的是createBrowserHistory方法来生成history对象,传入到Router组件中。
import React from 'react'
import Router from './Router'
import { createBrowserHistory } from 'history'
class BrowserRouter extends React.Component {
history = createBrowserHistory()
render() {
return <Router history={this.history} >
{this.props.children}
</Router>
}
}
export default BrowserRouter
子组件消费router context来生成对应的组件(Link、 Route、 Switch)
Route
match(路由匹配成功)的条件是,是否存在computedMatch props(Switch组件中存在),有直接使用, 没有使用matchPath函数来匹配path。
匹配成功渲染的条件: 如果匹配则判断是否有children props, 有就根据children 来渲染,(同时判断children是否为函数形式,为函数形式就调用函数, 不是直接渲染), 没有就判断是否存在component props, 有就根据component来渲染, 没有就判断是否存在render props, 有就渲染, 没有就为空。
import React from "react"
import matchPath from "./matchPath"
import RouterContext from "./RouterContext"
function isEmptyChildren(children) {
return React.Children.count(children) === 0
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const location = this.props.location || context.location
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match
const props = { ...context, location, match }
let { children, component, render } = this.props
if (Array.isArray(children) && isEmptyChildren(children)) {
children = null
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
)
}}
</RouterContext.Consumer>
)
}
}
export default Route
matchPath
(源码拷贝), 正则匹配path路径
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;
Switch
作用: 保证只匹配第一个
import React from 'react'
import RouterContext from './RouterContext'
import matchPath from './matchPath'
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location
let element, match
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child
const path = child.props.path || child.props.from
match = path ? matchPath(location.pathname, {...child.props, path }) : context.match
}
})
return match ? React.cloneElement(element, {location, computedMatch: match}) : null
}}
</RouterContext.Consumer>
)
}
}
export default Switch
Link
link 基本的实现就是一个a标签, 先阻止默认的事件, 然后根据replace这个props来判断, 是使用history的repalce还是push方法。 触发事件后,顶层的Router组件会监听repalce和push事件,改变相应的loaction, 从而触发Route重新匹配渲染。
import React from 'react'
import RouterContext from './RouterContext'
export const resolveToLocation = (to, currentLocation) =>
typeof to === "function" ? to(currentLocation) : to;
class Link extends React.Component {
static contextType = RouterContext
handleClick = (e) => {
e.preventDefault()
const { replace = false } = this.props
const location = resolveToLocation(this.props.to, this.context.location)
let method = this.context.history.push
if (replace) {
method = this.context.history.replace
}
method(location)
}
render() {
const { children, to, ...restProps } = this.props
return (
<a href={to} onClick={this.handleClick} {...restProps}>
{children}
</a>
)
}
}
export default Link