源码地址:
https://github.com/ReactTraining/react-router
使用教程:
https://reactrouter.com/web/guides/quick-start
基本使用
import React from "react";
import {
BrowserRouter as Router,
Route,
Link,
Switch,
withRouter
} from "./my-react-router-dom/";
import HomePage from "./pages/HomePage";
import UserPage from "./pages/UserPage";
import LoginPage from "./pages/LoginPage";
import ProductPage from './pages/ProductPage';
import _404Page from "./pages/_404Page";
function App() {
return (
<div className="App">
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户中心</Link>
<Link to="/login">登录</Link>
<Link to="/product/123">商品</Link>
<Switch>
<Route
path="/"
exact
component={HomePage}
/>
<Route
path="/product/:id"
render={() => <Product />}
/>
<Route path="/user" component={UserPage} />
<Route path="/login" component={LoginPage} />
<Route component={_404Page} />
</Switch>
</Router>
</div>
);
}
export default App;
接下来本文将根据使用方式从外向内逐一介绍,并简单实现
本文的例子将只考虑 BrowserRouter,HashRouter 可以自己扩展或到源码处阅读,其实道理都差不多
基本概念快速介绍
- Route 是怎么监听路由然后根据路由跳转后的地址实施不同组件的渲染?
react-router-dom 使用了 React 的 ContextAPI 进行值的传递,其中 context 的来源如图所示
- history 对象哪来的?
一般来说可以通过window
对象来获取,但是在React
中,引入了react-router-dom
的时候就会引入一个history依赖
,所以我们只要使用这个依赖就可以了
BrowserRouter
BrowserRouter 完成的事
- 通过
history
的createBrowserRouter
创建history对象
- 通过参数形式传递给通用的
<Router/>
Router
- 挂载时通过传入的
history
的listen()
监听路由 - 在
componentWillUnmount
中执行取消监听方法 - 在返回的
Provider
中的value
传入一个对象,键值对分别为history
、location
和根match
注意:
熟悉 contextAPI 的同学都知道,Provider
所处的组件发生更新时,Provider
会对其 value
进行一次浅比较,当 value
传入的是引用数据类型的时候,更新组件就相当于给 value
赋值了一个新的引用数据,而引用数据类型比较的是地址,所以当这个 Provider
所处的组件发生更新时,Provider
及其后代组件都会发生更新,这样会造成性能浪费。。然而为什么这里传入对象是合适的呢?
回想一下我们平时使用 <Router>
的位置,通常都是放在最外层的组件,所以在这个情况下不用担心这种情况
BrwoserRouter.js
import React, {Component} from "react";
import {createBrowserHistory} from "history";
import Router from "./Router";
export default class BrowserRouter extends Component {
constructor(props) {
super(props);
this.history = createBrowserHistory();
}
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
Router
import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
export default class Router extends 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={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname)
}}>
{this.props.children}
</RouterContext.Provider>
);
}
}
这里将 Router
分离封装出来是因为方便 BrowserRouter
和 HashRouter
的使用
Link
本质是一个 a标签
点击时:
- 阻止默认跳转
- 通过
BrowserRouter
传递的context
的history
跳转
context.history.push(to)
import React, {useCallback} from "react";
import {RouterContext} from "./RouterContext";
export default function Link({to, children, ...restProps}) {
const context = React.useContext(RouterContext);
const handleClick = useCallback(event => {
event.preventDefault();
context.history.push(to);
}, [context.history, to])
return (
<a href={to} {...restProps} onClick={handleClick}>
{children}
</a>
);
}
Switch
Swtich 做的事:
- 得到
根Router
传递下来的context
,去其中的location
- 遍历
children
,选出正确的element(Route)
Switch.js
import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
import matchPath from "./matchPath";
// 独占路由
// 渲染与该地址匹配的第一个子节点 <Route> 或者 <Redirect>。
class Switch extends Component {
render() {
console.log('Switch rendered');
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
let match; //标记匹配
let element; //记录匹配的元素
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
match = child.props.path
? matchPath(location.pathname, child.props)
: context.match;
}
});
return match
? React.cloneElement(element, {computedMatch: match})
: null;
}}
</RouterContext.Consumer>
);
}
}
export default Switch;
注意:
路由更新时只会执行 Consumer
中的内容,不会重新触发 Switch组件
的 render()
Route
Route 渲染方式 优先级从高到低:
- children (func)
- component (component)
- render (func)
Route 渲染逻辑
Route.js
import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
import matchPath from "./matchPath";
export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const {location} = context;
const {path, children, component, render, computedMatch} = this.props;
const match = computedMatch
? computedMatch
: path
? matchPath(location.pathname, this.props)
: context.match;
const props = {
...context,
match
};
//match children, component, render, null
// 不匹配 children(function), null
return (
<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>
);
}
}
matchPath.js
大概知道这个文件的作用就行,第一次看可以不用深究
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;
其他组件
Redirect
import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
export default class Redirect extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const {history} = context;
const {to, push = false} = this.props;
return (
<LifeCycle
onMount={() => {
push ? history.push(to) : history.replace(to);
}}
/>
);
}}
</RouterContext.Consumer>
);
}
}
class LifeCycle extends Component {
componentDidMount() {
if (this.props.onMount) {
this.props.onMount.call(this, this);
}
}
render() {
return null;
}
}
注意:
这里用到了一个工具人函数,借用了一下它的 componentDidMount
去做路由跳转
Prompt
Prompt.js
import React from 'react';
import {RouterContext} from './RouterContext';
import LifeCycleFragment from './LifeCycleFragment';
export default function Prompt({message, when = true}) {
return (
<div>
<RouterContext.Consumer>
{
context => {
if (!when) return null;
let method = context.history.block;
return (
<LifeCycleFragment
onMounted={self => {
self.release = method(message);
}}
onUnmount={self => {
self.release();
}}
/>
)
}
}
</RouterContext.Consumer>
</div>
)
}
LifeCycleFragment.js
import React from 'react';
export default class LifeCycleFragment extends React.Component {
componentDidMount() {
if (this.props.onMounted) {
this.props.onMounted.call(this, this);
}
}
// 在源码中这里还考虑了组件更新的情况
// xxx
componentWillUnmount() {
if (this.props.onUnmount) {
this.props.onUnmount.call(this, this);
}
}
render () {
return null
}
}
钩子函数
import {RouterContext} from "./RouterContext";
import {useContext} from "react";
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 : {};
}
感谢你能看到这里,有任何疑问欢迎评论或私信~
希望更深一步研究请直接研究源码