React-Router的简单使用
手写mini React-Router
Link
React-Router 与 Vue-Router 中的 Link 实质上就是a标签。
import { Component } from "react";
export default class Link extends Component {
render() {
const { to, children, ...restProps } = this.props;
return <a href={to} {...restProps}>{children}</a>;
}
}
BrowserRouter
根据React-Router库中,Router是react-router-dom和react-router-native的公共组件,跟着库的文件目录:BrowserRouter引用Router并传入一个history参数
import { 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() {
const { children } = this.props;
return (
<Router history={this.history}>{ children }</Router>
);
}
}
Context
创建一个全局上下文,方便记录数据,跨层级传递
import React from 'react';
const RouterContext = React.createContext();
export default RouterContext;
Router
Router下的子组件需要根据BrowserRouter传入的history中的参数自动进行渲染,在Router中需要监听location的变化。且传入Provider的参数需要是对象,子组件才会根据其上下文进行重新渲染
import React, { Component } from "react";
import RouterContext from './Context';
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
}
// 监听location变化
this.unListen = props.history.listen(({ location }) => {
this.setState({location});
});
}
componentWillUnmount() {
if (this.unListen) {
this.unListen();
}
}
render() {
const { children, history } = this.props;
return <RouterContext.Provider value={{
history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname)
}}>
{children}
</RouterContext.Provider>;
}
}
Route
import React, { Component } from "react";
import matchPath from "./matchPath";
import RouterContext from "./Context";
// 展示对应的组件,其中children > component > render
export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const { location } = context;
// computedMatch由Switch组件传入,若存在则直接使用即可 单一选择
const { path, children, component, render, computedMatch } =
this.props;
// 根据location判断当前的界面参数
const match = computedMatch
? computedMatch
: path
? matchPath(location.pathname, this.props)
: context.match;
const props = {
...context,
match,
};
return (
// 先判断是否匹配,若匹配则判断是否存在children 再判断是否存在component 最后判断是否存在render
// 若不匹配: 则判断是否存在children(404),若存在则使用children,若不存在则返回null
<div>
{/* 添加Provider的原因: 组件会从组件树离自身最近匹配的Provider中读取当前的context值,hook中需要使用最近的props */}
<RouterContext.Provider value={props}>
{match
? children
? typeof children === "function"
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: children
? typeof children === "function"
? children(props)
: children
: null}
</RouterContext.Provider>
</div>
);
}}
</RouterContext.Consumer>
);
}
}
Switch
import React, { Component } from "react";
import RouterContext from './Context';
import matchPath from "./matchPath";
export default class Switch extends Component {
render() {
const { children } = this.props;
return (
<RouterContext.Consumer>
{
context => {
let match, element;
const { location } = context;
// 只取一个match的组件进行渲染
// 独占路由,若其中一个Route组件已命中(match不再为null),则不再向下匹配 直接渲染命中组件
React.Children.forEach(children, (child) => {
// 精确匹配当前Route组件的path与location.pathname 精确匹配才算匹配
// 若没精确匹配中 则将match设置为null 继续遍历向下匹配
if (match == null && React.isValidElement(child)) {
element = child;
const { path } = child.props;
match = path ? matchPath(location.pathname, path) : context.match;
}
});
return match ? React.cloneElement(element, { computedMatch: match }) : null;
}
}
</RouterContext.Consumer>
);
}
}
Redirect
import React, { Component } from 'react';
import RouterContext from './Context';
export default class Redirect extends Component {
render () {
return (<RouterContext.Consumer>
{context => {
const {history, push = false} = context;
const {to} = this.props;
return (
<LifeCycle
onMount={() => {
push ? history.push(to) : history.replace(to);
}}
/>
);
}}
</RouterContext.Consumer>);
}
}
// 创建一个返回空的组件,执行其中的函数进行跳转
class LifeCycle extends Component() {
componentDidMount() {
if(this.onMount) {
this.onMount();
}
}
render() {
return null;
}
}
withRouter
withRouter本质上就是一个高阶组件,接收一个组件,将 RouteContext 中的context上下文当做 props 一并传入接收的组件,并返回当前接收组件即可
import React from 'react';
import RouterContext from './Context';
const withRouter = Component => props => {
return <RouterContext.Consumer>
{context => {
// 将context上下文作为参数传给Component
return <Component {...props} {...context} />;
}}
</RouterContext.Consumer>
}
export default withRouter;
hooks
import { useContext } from 'react';
import RouterContext from './Context';
export function useHistory() {
return useContext(RouterContext).history;
}
export function useRouteMatch() {
return useContext(RouterContext).match;
}
export function useLocation() {
return useContext(RouterContext).location;
}
export function useParams(params) {
// RouterContext取最近的Provider 需要在Route中包裹一层RouterContext.Provider
const match = useContext(RouterContext).match;
return match ? match.params : null
}
matchPath
直接使用React-Router中的匹配路由文件
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;