在 React中学会使用路由
基础知识 Location 对象
location 对象
Nodejs URL 模块 Jest 测试 这是 nodejs 的 url 的学习是用 jest 作为测试的学习方式,对比学习可以更好的理解 location 对象
属性/方法 |
说明 |
|---|---|
| href | :---: |
| protocol | :---: |
| host | :---: |
| hostname | :---: |
| port | :---: |
| pathname | :---: |
| search | :---: |
| hash | :---: |
| usename | :---: |
| password | :---: |
| origin | :---: |
| assign() | :---: |
| reload() | :---: |
| replace() | :---: |
| toString() | :---: |
基础知识 History 对象
history 对象
属性/方法 |
说明 |
|---|---|
| back() | |
| forward() | |
| go() | |
| length | |
| pushState() | h5 |
| replaceState() | h5 |
也就是在 H5 中有 history 的概念。HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。 使用 history.pushState() 可以改变referrer,它在用户发送 XMLHttpRequest 请求时在HTTP头部使用,改变state后创建的 XMLHttpRequest 对象的referrer都会被改变。因为referrer是标识创建 XMLHttpRequest 对象时 this 所代表的window对象中document的URL。
let stateObj = {
foo: "bar",
};
history.pushState(stateObj, "page 2", "bar.html");
history.replaceState(stateObj, "page 3", "bar2.html");
React 路由社区解决方法
- React-Router
- Reach-Route (号称下一代 😸 React-Router)
这里使用 react-router/react-router-dom 来进行说明,静态配置路由虽然是比较好的路由方式,但是在 react-router 中并不成熟。
react-router 对外暴露的 API
- 其次依赖于 react-router 包,它向外暴露的包:
| 序号 | 组件 | 说明 | |
|---|---|---|---|
| 1 | MemoryRouter | :--- | :--- |
| 2 | Prompt | 提示组件 | :--- |
| 3 | <Redirect /> |
路由重定向 | :--- |
| 4 | <Route /> |
:--- | :--- |
| 5 | <Router /> |
:--- | :--- |
| 6 | StaticRouter | :--- | :--- |
| 7 | <Switch /> |
:--- | :--- |
| 8 | __RouterContext | 私有的 Router 上下文 | :--- |
| 9 | generatePath | :--- | :--- |
| 10 | matchPath | :--- | :--- |
装饰器,高阶函数
| 序号 | react-router API | 说明 |
|---|---|---|
| 11 | withRouter | :--- |
| 12 | withHistory | 获取现在location的对象, hook 方便我们使用 |
React 钩子函数
| 序号 | 钩子名 | 说明 |
|---|---|---|
| 13 | useLocation | 使用动态路由的时候,我们要写参数/cards/:id,hook 方便我们使用 |
| 14 | useParams | 获取现在location的对象,hook 方便我们使用 |
| 14 | useRouteMatch | 使用动态路由的时候,我们要写参数/cards/:id,hook 方便我们使用 |
功能
- 嵌套路由
- 重定向
react-router 中的对象
- history, 与浏览器中的 History 对象有所不同
- location 与浏览器中的 location 对象有所不同
- match
// history 对象
history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
// location 对象
location = {
pathname, // 当前路径,即 Link 中的 to 属性
search, // search
hash, // hash
state, // state 对象
action, // location 类型,在点击 Link 时为 PUSH,浏览器前进后退时为 POP,调用 replaceState 方法时为 REPLACE
key, // 用于操作 sessionStorage 存取 state 对象
};
路由按需加载
- 使用 @loadable/component,@loadable/component 支持服务端渲染。
- 不考虑服务端渲染的情况使用使用 React.Suspense 和 React.lazy,即可完成同样的任务。
react-router-dom 对外暴露的 API
针对浏览器平台的路由
| 序号 | 组价 | 说明 |
|---|---|---|
| 1 | <BrowserRouter /> |
浏览器 h5路由 |
| 2 | <HashRouter /> |
Hash 路由 |
| 3 | <Link /> |
导航链接,相当于 <a /> 标签 |
| 4 | <NavLink /> |
路由导航链接 |
以上各个 API 是 react-router-dom 经过修该之后,单独暴露出来,在浏览器中,我们可以使用这些 API。其他组件其实是从 react-router 中直接拿过来的,这样做是为了考虑跨端的抽象能力,不同的平台最后实现的内容都是不一样的。
为了方便使用,通常 BrowserRouter 和 HashRouter 都重新定义为 Router 。在切换不同的路由的时候,更加方便使用。其次 BrowserRouter 和 HashRouter 是包裹性质中的组件,Route 对象都放在 BrowserRouter 和 HashRouter 内部作用 child 进行渲染。
下面是 react-router-dom 中 BrowserRouter 的部分 源码
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
看到 react-router-dom 其实是依赖于 react-router, <BrowserRouter /> 组件中的 history 属性是从 history 中创建的。
- history
- children,所以我们的
Route等等,都是放在Router里面作为children渲染的。
先看创建的 history 中到底包含了什么?
function createBrowserHistory(historyState) {
const globalHistory = window.history;
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
const {
forceRefresh = false,
getUserConfirmation = getConfirmation,
keyLength = 6
} = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
// other functions
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
}
对于 HashHistory 其实和 BrowserHistory 是一样的,在实现的上有些不同罢了, 所暴露出的方法也是一样的,这里就不在重复,可以下载源码对比。
接下俩是 <Link />, 它是通过 forwardRef 实现的, forwardRef 是一个穿透 ref 的 api, forwardRef 是通过 ref 属性在 react 元素上传递的,访问的方式是 ref.current. <Link /> 组件最终渲染内容给很简单,内容如下。
return <a {...props} />;
再来看 <NavLink />, 它其实是对 <Link /> 的一层封装, 可以理解为 <Link /> 一个有激活导航组件自定义样式的特别版本。
再来看 <Switch />, 其实就是根据 match 对象进行切换:
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Switch> outside a <Router>");
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>
);
}
}
match 对象的形式是如下:
{
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;
}, {})
}
matchPath 函数的根据 pathname 作为参数和一些选项作为参数,就可以生成一个匹配对象。最后根绝这个匹配对象 match 来生成组件:
React.cloneElement(element, { location, computedMatch: match })
withRouter 其实是一个一直困扰初学者的问题,我们来看它的源码的实现:
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";
import RouterContext from "./RouterContext.js";
/**
* A public higher-order component to access the imperative API
*/
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
invariant(
context,
`You should not use <${displayName} /> outside a <Router>`
);
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
if (__DEV__) {
C.propTypes = {
wrappedComponentRef: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.object
])
};
}
return hoistStatics(C, Component);
}
export default withRouter;
- hoistStatics 将非 react 特定的静态信息从
子组件复制到父组件。来看源码
import { ForwardRef, Memo, isMemo } from 'react-is';
const REACT_STATICS = {
childContextTypes: true,
contextType: true,
contextTypes: true,
defaultProps: true,
displayName: true,
getDefaultProps: true,
getDerivedStateFromError: true,
getDerivedStateFromProps: true,
mixins: true,
propTypes: true,
type: true
};
const KNOWN_STATICS = {
name: true,
length: true,
prototype: true,
caller: true,
callee: true,
arguments: true,
arity: true
};
const FORWARD_REF_STATICS = {
'?typeof': true,
render: true,
defaultProps: true,
displayName: true,
propTypes: true
};
const MEMO_STATICS = {
'?typeof': true,
compare: true,
defaultProps: true,
displayName: true,
propTypes: true,
type: true,
}
const TYPE_STATICS = {};
TYPE_STATICS[ForwardRef] = FORWARD_REF_STATICS;
TYPE_STATICS[Memo] = MEMO_STATICS;
function getStatics(component) {
// React v16.11 and below
if (isMemo(component)) {
return MEMO_STATICS;
}
// React v16.12 and above
return TYPE_STATICS[component['?typeof']] || REACT_STATICS;
}
const defineProperty = Object.defineProperty;
const getOwnPropertyNames = Object.getOwnPropertyNames;
const getOwnPropertySymbols = Object.getOwnPropertySymbols;
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const getPrototypeOf = Object.getPrototypeOf;
const objectPrototype = Object.prototype;
export default function hoistNonReactStatics(targetComponent, sourceComponent, blacklist) {
if (typeof sourceComponent !== 'string') { // don't hoist over string (html) components
if (objectPrototype) {
const inheritedComponent = getPrototypeOf(sourceComponent);
if (inheritedComponent && inheritedComponent !== objectPrototype) {
hoistNonReactStatics(targetComponent, inheritedComponent, blacklist);
}
}
let keys = getOwnPropertyNames(sourceComponent);
if (getOwnPropertySymbols) {
keys = keys.concat(getOwnPropertySymbols(sourceComponent));
}
const targetStatics = getStatics(targetComponent);
const sourceStatics = getStatics(sourceComponent);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
if (!KNOWN_STATICS[key] &&
!(blacklist && blacklist[key]) &&
!(sourceStatics && sourceStatics[key]) &&
!(targetStatics && targetStatics[key])
) {
const descriptor = getOwnPropertyDescriptor(sourceComponent, key);
try { // Avoid failures from read-only properties
defineProperty(targetComponent, key, descriptor);
} catch (e) {}
}
}
}
return targetComponent;
};
一个后台管理系统的路由设计
在一个后台管理系统中,我们应该如何设计自己路由方案呢?
先思考业务模型:
- 进入网站,登录
/login,后台管理系统一般是直接就是登录路由/login。不考虑角色分配问题。 - 登录请求成功之后,就要决定路由应该如何跳转。登录成功直接进入 app 的仪表盘界路由
/app/dashboard界面。 - 下面有一个概念:精确匹配
exact和模糊匹配。精确匹配是只匹配唯一指定路径。- 例如
/app,有exact属性的路由,使得只能匹配/app路由对应的组件。 - 没有
exact属性的路径,是可以匹配/app/a和/app/b等等模糊的路径。
- 例如
下面是将后台系统简单的抽象:
说明:
当登录成功之后,跳转的应该是 exact /app 路径。进入布局页面 Layout, Layout 中具有二级路由,用于存放 app/*下的路由内容(用 views 视图表示其实更加合适, 因为本质是一个单应用。也用 pages,页面更加的如何 html 的编程习惯)。
登录成功之后,访问 / 或者/app 路径也是,会重新的跳转到 /app/dashboard, 因为 dashboard 仪表盘才是主界面。
import { HashRouter, Route, Switch, Redirect } from "react-router-dom";
<HashRouter>
<Switch>
<Route exact path="/" render={() => <Redirect to="/app/dashboard" />} />
<Route
exact
path="/app"
render={() => <Redirect to="/app/dashboard" />}
/>
<Route path="/app" component={Layout} />
<Route path="/login" component={Login} />
<Route component={Error} />
</Switch>
</HashRouter>
当然可以增加特殊的提示结果页面:
- 404
- 400
- 500
- ...
导航方式
- 组件导航:Link、NavLink 组件进行导航
- 编程式导航
函数组件
- react-router-5.0 使用 react-hooks, 这种方式简单方便
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
history.push('/your/path')
};
class 组件
- 在4.0及更高版本中,将历史记录用作组件的支持, class 组件中,通过 props 拿到 history 对象
class Example extends React.Component {
// use `this.props.history.push('/some/path')` here
}