1.BrowserRouter与HashRouter对比:
-
HashRouter 最简单,不需要服务器端渲染,靠浏览器的 #的来区分 path 就可以, BrowserRouter 需要服务器端对不同的 URL 返回不同的 HTML 。
-
BrowserRouter 使⽤ HTML5 history API ( pushState ,replaceState 和 popstate 事件),让⻚⾯的 UI 于 URL 同步。
- HashRouter不⽀持location.key和location.state,动态路
由跳转需要通过 ? 传递参数。-
Hash history 不需要服务器任何配置就可以运⾏,如果你刚刚⼊⻔,那就使⽤它吧。但是我们不推荐在实际线上环境中⽤到它,因为每⼀个 web 应⽤都应该渴望使⽤browserHistory。
2.BrowserRouter.js
import React, { Component } from "react";
import { createBrowserHistory } from "history";
import { RouterContext } from "./RouterContext";
export default class BrowserRouter extends Component {
static computeRootMatch(pathname) {
return {
path: "/",
url: '/',
params: {},
isExact: pathname = "/"
}
}
constructor(props) {
super(props);
this.history = createBrowserHistory();
this.state = {
location: this.history.location
}
this.unlisten = this.history.listen(location => {
this.setState({ location })
})
}
componentWillUnmount(){
if(this.unlisten) {
this.unlisten()
}
}
render() {
return (
// 需要提供history location match三种属性
<RouterContext.Provider
value ={{
history: this.history,
location: this.state.location,
match: BrowserRouter.computeRootMatch(this.state.location.pathname)
}}
>
{this.props.children}
</RouterContext.Provider>
)
}
}
3.Link.js
import React, {Component} from 'react';
import { RouterContext } from "./RouterContext";
export default class Link extends Component {
// 处理点击事件
handleClick = (event, history) => {
// 禁止默认行为
event.preventDefault();
history.push(this.props.to);
};
render() {
const { to, children } = this.props;
return (
<RouterContext.Consumer>
{
context => (
<a
href={to}
onClick={event => this.handleClick(event, context.history)}>
{children}
</a>
)
}
</RouterContext.Consumer>
)
}
}
4.Route.js
import React, { Component } from "react";
import { RouterContext } from "./RouterContext";
import matchPath from "./matchPath";
// 这里的children不管是否匹配match都可以存在,这里能不能直接返回,就不判断了
// match 匹配 children是function或者是节点
// 不match 不匹配 children是function
export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const { path, computedMatch, children, component, render } = this.props;
// 首先获取props中的location
const location = this.props.location || context.location;
const match = computedMatch
? computedMatch
: path
? matchPath(location.pathname, this.props)
: context.match;
const props = {
...context,
location,
match
};
return (
<RouterContext.Provider value={props}>
{/* 首先匹配children>component>render
1. 匹配上是children>component>render
2. 不匹配是children或者null
3. children 可以是一个函数或者组件
*/}
{match
? children
? typeof children === "function"
? children(props)
: children
: component
? // ? React.cloneElement(element, props)
React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
5.Switch.js
import React, { Component } from "react";
import { RouterContext } from "./RouterContext";
import matchPath from "./matchPath";
export default class Switch extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
// 找出渲染的,第一个符合匹配的元素,存在element
// const {location} = context;
// 优先用props上的location
const location = this.props.location || context.location;
let element,
match = null;
let { children } = this.props;
React.Children.forEach(children, child => {
console.log('child', child)
if (match === null && React.isValidElement(child)) {
element = child;
const path = child.props.path;
match = path
? matchPath(location.pathname, {
...child.props,
path
})
: context.match;
}
});
return match
? React.cloneElement(element, {
location,
computedMatch: match
})
: null;
}}
</RouterContext.Consumer>
);
}
}
6.Redirect.js
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} = this.props;
// history.push(to)
return <LifeCycle onMount={() => history.push(to)} />;
}}
</RouterContext.Consumer>
);
}
}
class LifeCycle extends Component {
// 组件挂载
componentDidMount() {
if (this.props.onMount) {
this.props.onMount();
}
}
render() {
return null;
}
}
7.RouterContext.js
import React from "react";
export const RouterContext = React.createContext();
8.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;