【React Router】在非组件(或工具方法)中如何优雅的跳转路由 - 理解 HashHistory 和 BrowserHistory
Router
由于项目为 Hash 路由,连夜百度了下,确实有相关属性供我们使用
createHashHistory
由之前的
import { HashRouter } from 'react-router-dom';
function App() {
return <HashRouter>xxx</HashRouter>;
}
改为
import { Router } from 'react-router-dom';
// history 为 react-router-dom 依赖,直接引入即可
import { createHashHistory, createBrowserHistory } from 'history';
// 若你是 browser 路由则使用 createBrowserHistory
const history = createHashHistory();
function App() {
return <Router history={history}>xxx</Router>;
}
const history = createHashHistory();
history 可在 utils 定义再导出 App 内在引用赋值,方便其它地方引用,当然这因人而异。- 另外 HashRouter 是不接 history 属性虽说是基于 Router 具体可继续往下看
- history 直接使用不赋值给 HashRouter 也可以对路由产生作用(测试仅限 Hash,应该内部监听了 Hash change),也由于未给 Router 赋值,所以会出现 history 表现不一致等未知情况,例如history的action等
createHashHistory 与 createBrowserHistory
看 V5 中 HashRouter
与 BrowserRouter
的源码,虽然是基于 Router
但是并没有透传多余参数给他,所以上面我们直接使用 Router
即可,内部参数相同
HashRouter
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
/**
* The public API for a <Router> that uses window.location.hash.
*/
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
if (__DEV__) {
HashRouter.propTypes = {
basename: PropTypes.string,
children: PropTypes.node,
getUserConfirmation: PropTypes.func,
hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"])
};
HashRouter.prototype.componentDidMount = function() {
warning(
!this.props.history,
"<HashRouter> ignores the history prop. To use a custom history, " +
"use `import { Router }` instead of `import { HashRouter as Router }`."
);
};
}
export default HashRouter;
BrowserRouter
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
/**
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
if (__DEV__) {
BrowserRouter.propTypes = {
basename: PropTypes.string,
children: PropTypes.node,
forceRefresh: PropTypes.bool,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number
};
BrowserRouter.prototype.componentDidMount = function() {
warning(
!this.props.history,
"<BrowserRouter> ignores the history prop. To use a custom history, " +
"use `import { Router }` instead of `import { BrowserRouter as Router }`."
);
};
}
export default BrowserRouter;
Route 和 Router(V5)
插句嘴~ 区分下 Route 和 Router 一字之差到底有啥区别
Router
如他首行注释所描述就是 Router 做的事情,
RouterContext
串联起 Route 为其提供了 路由相关方法(props history)
通过 history 提供的 listener 去修改 state.location 来触发 render
/**
* The public API for putting history on context.
*/
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 is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
this.unlisten = props.history.listen(location => {
this._pendingLocation = location;
});
}
}
componentDidMount() {
this._isMounted = true;
if (this.unlisten) {
// Any pre-mount location changes have been captured at
// this point, so unregister the listener.
this.unlisten();
}
if (!this.props.staticContext) {
this.unlisten = this.props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
}
});
}
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
this._isMounted = false;
this._pendingLocation = null;
}
}
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
Route
Route 主要是根据传入的 path 和当前的 pathname 是否 match,来决定是否 rendering
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
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;
// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && isEmptyChildren(children)) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
有兴趣的可以看下 matchPath 的实现,里面使用
path-to-regexp
的库可以帮你处理 pathname 与 route path 的关系,当你在 layout 组件内时match
是无法正确返回当前组件的,可以通过此库来实现 match
history - 5.x
ok,现在看下 history
内部是如何处理 Hash 和 Browser 的(有点跳啊,本身项目是 5.x 的 router-dom 对应的是 4.x 的 history,但github上是新版的 history 就代入了。。。这里直接就 5.x 吧)
Hash 和 Browser 的区别是什么?
Hash | Browser | |
---|---|---|
Url | url 中存留 hash 标识# ,通过 hash 值来 match route, |
URL 即为资源地址,浏览器请求资源不会携带 Hash 值,所以对 Hash 来说始终为 index.html,对 Browser 而言 /home 就需要 home.html 存在不然会 404,或服务端将请求指向 index.html |
location.state | 对于 5.x 的话此处无区别,而 4.x 的 Hash 路由则是通过模仿 history 来实现,不会在浏览器持久化保存 | 使用 web api history 来实现路由跳转,5.x 中 hash 与 Browser 无区别 |
createHashHistory
4.x 跳转(push)
path 为 history.push 的参数,传的 state 在 path 内,因为跳转使用的是 window.location 相关 api ,而 state 也只是透传 (
setState({ action, location });
)所以在 4.x 中你刷新就会丢失 state 状态
// /history/modules/createHashHistory.js
function push(path, state) {
warning(
state === undefined,
'Hash history cannot push state; it is ignored'
);
const action = 'PUSH';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const path = createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
// We cannot tell if a hashchange was caused by a PUSH, so we'd
// rather setState here and ignore the hashchange. The caveat here
// is that other hash histories in the page will consider it a POP.
ignorePath = path;
pushHashPath(encodedPath);
const prevIndex = allPaths.lastIndexOf(createPath(history.location));
const nextPaths = allPaths.slice(0, prevIndex + 1);
nextPaths.push(path);
allPaths = nextPaths;
setState({ action, location });
} else {
warning(
false,
'Hash history cannot PUSH the same path; a new entry will not be added to the history stack'
);
setState();
}
}
);
}
function pushHashPath(path) {
window.location.hash = path;
}
5.x
globalHistory.pushState(historyState, "", url);
这里就很清楚的看到 5.x 的 hash 是使用 history api 来操作的,所以 state 将和 Browser 一致会保存,也可 window.history log 出来
function push(to: To, state?: any) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
function retry() {
push(to, state);
}
warning(
nextLocation.pathname.charAt(0) === "/",
`Relative pathnames are not supported in hash history.push(${JSON.stringify(
to
)})`
);
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
applyTx(nextAction);
}
}