1. React-Router 的简单介绍
React Router 是一个基于 React 的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
它拥有简单的 API 与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理。
2. 为什么要用 React-Router?
首先来看一下,在不使用 react-router 的情况下,我们切换页面,切换显示的组件可以通过什么方式实现?
2.1 使用 State 来判断渲染的组件
function Login() {
return <div>Register</div>;
}
function Register() {
return <div>Login</div>;
}
function App() {
let [route, setRoute] = useState('Login');
let onClickLogin = () => {
setRoute('Login')
}
let onClickRegister = () => {
setRoute('Register')
}
let Child
switch(route) {
case 'Login':
Child = <Login/>;
break;
case 'Register':
Child = <Register/>;
break;
}
return (
<div className="App">
<button onClick={onClickLogin}>Login</button>
<button onClick={onClickRegister}>Register</button>
<Child />
</div>
);
}
这段代码实现来通过点击按钮,切换不通的状态,渲染不同的组件
2.2 使用 hash 来判断要渲染的组件
使用 hash 来判断的好处是,可以通过不同的 url 渲染不同的页面(组件)
function App() {
const [route, setRoute] = useState(window.location.hash.substr(1))
useEffect(() => {
window.addEventListener('hashchange', () => {
setRoute(window.location.hash.substr(1))
})
}, [])
let Child
switch(route) {
case 'Login':
Child = <Login/>;
break;
case 'Register':
Child = <Register/>;
break;
}
return (
<div className="App">
<ul>
<li><a href="#/about">About</a></li>
<li><a href="#/inbox">Inbox</a></li>
</ul>
<Child />
</div>
);
}
同理,也可以使用 pathname 来判断渲染的页面。
上面这种实现,如果页面多了,组件多了,就需要写很多的判断逻辑。
而 React-Router 就是将这些判断逻辑封装起来,并扩展了其他功能。使我们在开发 react 应用中可以快速解决路由方面的问题。
3. React-Router 的使用
在做 web 开发时,一般我们使用 react-router-dom 这个库,这个库包含了 react-router 的所有组件并且扩展了 BrowerRouter、HashRouter、Link 与 NavLink 这几个组件。
3.1 API 介绍
-
<BrowserRouter />
对 Router 的扩展,基于 html 5 history api 实现监测 url 变化同步更新组件显示 -
<HashRouter />
对 Router 的扩展,监测 hash 变化,同步更新组件 -
<Router />
提供给其他 xxxRouter 使用的低层级公用组件 -
<Route />
计算是否匹配当前的url,并展示相应的组件 -
<Switch />
渲染第一个匹配到的 Route -
<Link />
声明式路由导航 -
<NavLink />
一个特殊的 ,当匹配到当前 url 时,会添加可以自定义样式到属性(className,style) -
<Redirect />
重定向 -
<Prompt />
用于在离开页面之前提示用户,例如编辑表单时离开页面 -
<MemoryRouter />
使用内存保存路由状态而不是根据 url,一般用于非浏览器场景,例如 react-native -
<StaticRouter />
使用在 location 不会发生改变的场景,一般用于服务端渲染 -
generatePath
-
history
-
location
-
match
-
matchPath
-
withRouter
-
Hooks
-
useHistory
-
useLocation
-
useParams
-
useRouteMatch
-
3.2 react-router 使用
将上面的例子改成使用 react-router
// 引入 相关组件
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
function Login() {
return <div>Register</div>;
}
function Register() {
return <div>Login</div>;
}
function App() {
return (
<Router>
<div className="App">
{/* 使用 Link 替换 a 标签 */}
<Link to="/login">Login</Link>
<Link to="/register">Register</Link>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</div>
</Router>
);
}
由 react-router 去帮我们匹配想要渲染的组件,而不用我们手动去匹配。
可以看到重构后,少了很多代码,这种情况在页面多的时候,更为明显。
4. React-Router 的实现
接下来通过实现一个精简的 react-router 来了解其原理。
以下内容基于 react-router 5.2 版本,5.x 使用的 history 版本为 4.x
4.1 <BrowserRouter />
的实现
BrowserRouter 是基于 HTML5 history 对 Router 组件进行包装,一般是浏览器端使用
import React from "react";
import { createBrowserHistory as createHistory } from "history";
import Router from "./Router";
class BrowserRouter extends React.Component {
// createHistory 返回一个基于 html5 history api 的自定义 history 对象
history = createHistory(this.props);
render() {
// 将 history 提供给 Router
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
4.2 <Router />
的实现
import React from "react";
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";
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;
// staticContext 是 StaticRouter 传入的,使用 BrowserRouter 时是没有的
if (!props.staticContext) {
// 开始监听 url 变化,变化时会执行传入的回调函数
this.unlisten = props.history.listen(location => {
// 这里做了一层兼容操作,有可能这里执行的时候,
// 组件还没加载完成,就会把 location 暂存起来,
// 等组件加载完成后再去更新 location
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
this._isMounted = false;
this._pendingLocation = null;
}
}
render() {
return (
/*
* RouterContext 为子组件提供 history,location,match,staticContext 数据
* 这里 history 已经包含了 location,为什么这里还要将 location 单独传递呢?
* 因为 location 通过 state 保存,用于当 url 发生变化时,更新 state 实现同步渲染页面
*/
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
// HistoryContext 主要为 子组件 提供 children 和 history 数据
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
export default Router;
4.3 <Route />
的实现
<Route />
匹配单一的 path 并渲染相应的组件
import React from "react";
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
// 这里判断是否匹配当前 url,
// computedMatch 是使用 Switch 时,Switch 提前计算好是否匹配
// context.math 是 Router 传下来的
// 如果有用 Switch 则优先使用Switch 传递的 computedMatch 判断匹配
// 否则判断是否有 path,有 path 就计算是否匹配当前 url,
// 如果没有 path,则使用 Router 传递的 context.match
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}>
{/* 渲染的优先级 children > component > render */}
{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>
);
}
}
export default Route;
4.4 <Link />
的实现
为什么使用 Link 而不直接使用 a 标签?
<Link />
是对 a 标签进行包装,屏蔽掉默认的行为,使用 history api 进行跳转,实现 url 改变的时候,不会重新加载页面。
import React from "react";
import RouterContext from "./RouterContext";
import {
resolveToLocation,
normalizeToLocation
} from "./utils/locationUtils.js";
// React 15 compat
const forwardRefShim = C => C;
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
forwardRef = forwardRefShim;
}
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
const LinkAnchor = forwardRef(
(
{
innerRef, // TODO: deprecate
navigate,
onClick,
...rest
},
forwardedRef
) => {
const { target } = rest;
let props = {
...rest,
onClick: event => {
try {
if (onClick) onClick(event);
} catch (ex) {
event.preventDefault();
throw ex;
}
/*
* event.defaultPrevented 判断 onClick 中是否执行了 event.preventDefault()
* event.button 判断点击的是否是左键
* target 判断链接是不是在当前窗口打开
* isModifiedEvent 判断是否是 组合键 事件(例如:按住alt+左键点击)
*/
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
(!target || target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
navigate();
}
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.ref = innerRef;
}
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
return <a {...props} />;
}
);
/**
* The public API for rendering a history-aware <a>.
*/
const Link = forwardRef(
(
{
// 可以传自定义的 component,如果没传 默认使用 LinkAnchor 组件
component = LinkAnchor,
// 默认是 false,使用 history.push,如果传 true,会使用 history.replace
replace,
// 跳转的目标,可以是 string、Object、Function
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
const { history } = context;
/**
* 根据传入 to 参数的不同形式,创建一个 location 对象
* resolveToLocation:如果 to 是function 执行 to,
* 传入 context.location 创建一个新的location,否则返回 to 本身
*
* normalizeToLocation:如果 to 是 string,使用 createLocation 创建一个 location,
* 否则返回 to 本身
*/
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);
// 通过 location({ pathname, search, hash }) 与 basename 组成 href
// basename 是使用 BrowserRouter 时传入的 props
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href,
navigate() {
/**
* navigate 使用 history 进行跳转
*
* replace 是传入的 props 参数,
* 决定使用 history.replace 还是 history.push 进行跳转
*
* history.replace 会替换当前history 栈中当前指针指向的那条记录 (history length 不变)
* history.push 会向栈中 push 多一条记录(history length + 1)
*/
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
method(location);
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
}
);
export default Link;
4.5 <Switch />
的实现
<Switch />
会渲染第一个匹配到的它的 Route 子组件 ,使用 Switch 可以确保只渲染一个 Route 子组件。
import React from 'react';
import RouterContext from './RouterContext.js';
import matchPath from './matchPath.js';
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const location = this.props.location || context.location;
let element, match;
// We use React.Children.forEach instead of React.Children.toArray().find()
// here because toArray adds keys to all child elements and we do not want
// to trigger an unmount/remount for two <Route>s that render the same
// component at different URLs.
React.Children.forEach(this.props.children, (child) => {
if (match == null && React.isValidElement(child)) {
element = child;
// from 是来自 <Redirect />
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>
);
}
}
export default Switch;
4.6 <Redirect />
的实现
import React from "react";
import { createLocation, locationsAreEqual } from "history";
import Lifecycle from "./Lifecycle.js";
import RouterContext from "./RouterContext.js";
import generatePath from "./generatePath.js";
/**
* The public API for navigating programmatically with a component.
*/
/**
*
*/
function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{context => {
const { history, staticContext } = context;
// 根据 push 参数判断使用的方法
const method = push ? history.push : history.replace;
/**
* 创建一个 location 对象
* computedMatch 是使用 <Switch /> 传递过来的
* to 可以是 string 或 object
* generatePath 根据 to 和 params 生成 path
* 最后 createLocation 生成 location 对象
*/
const location = createLocation(
computedMatch
? typeof to === "string"
? generatePath(to, computedMatch.params)
: {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
}
: to
);
// When rendering in a static context,
// set the new location immediately.
if (staticContext) {
method(location);
return null;
}
return (
<Lifecycle
onMount={()=> {
method(location);
}}
onUpdate={(self, prevProps)=> {
const prevLocation= createLocation(prevProps.to);
if (
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
})
) {
method(location);
}
}}
to={to}
/>
);
}}
</RouterContext.Consumer>
);
}
export default Redirect;
./Lifecycle.js
import React from "react";
class Lifecycle extends React.Component {
componentDidMount() {
if (this.props.onMount) this.props.onMount.call(this, this);
}
componentDidUpdate(prevProps) {
if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
}
componentWillUnmount() {
if (this.props.onUnmount) this.props.onUnmount.call(this, this);
}
render() {
return null;
}
}
export default Lifecycle;
./generatePath.js
import pathToRegexp from "path-to-regexp";
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path) {
if (cache[path]) return cache[path];
const generator = pathToRegexp.compile(path);
if (cacheCount < cacheLimit) {
cache[path] = generator;
cacheCount++;
}
return generator;
}
/**
* Public API for generating a URL pathname from a path and parameters.
*/
function generatePath(path = "/", params = {}) {
return path === "/" ? path : compilePath(path)(params, { pretty: true });
}
export default generatePath;
5. history
version 4.7.0
5.1 createBrowserHistory
首先看一下 createBrowserHistory 这个 API 返回哪些方法,再逐个看每个方法的实现。
const createBrowserHistory = (props = {}) => {
/* 此处省略具体功能函数实现代码 */
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
}
return history
}
5.2 history.listen
const listen = (listener) => {
/*
* transitionManager 是一个 location 转换过程的管理器,使用 发布-订阅 的模式
* appendListener 会将 listener 保存在 transitionManager 内的 listeners 数组中
* appendListener 返回一个函数,这个函数执行后会将 相应的 listener 从 listeners 数组中过滤掉
* checkDOMListeners
*/
const unlisten = transitionManager.appendListener(listener)
checkDOMListeners(1)
return () => {
checkDOMListeners(-1)
unlisten()
}
}
appendListener 的实现,位于 ./createTransitionManager.js
const appendListener = (fn) => {
// isActive 用于保存当前监听函数的状态,默认是 true,取消监听后会变成 false
// 主要用于处理,listeners 数组中函数已经在遍历执行,同时取消监听,
// 这时可能这个函数还在执行的数组队列中,isActive 变成 false 可以阻止该函数继续执行
let isActive = true
const listener = (...args) => {
if (isActive)
fn(...args)
}
listeners.push(listener)
// 返回一个取消监听的函数
return () => {
isActive = false
listeners = listeners.filter(item => item !== listener) // 过滤掉 listener
}
}
checkDOMListeners 的实现
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
// 对 popstate 事件进行监听,handlePopState 会
addEventListener(window, PopStateEvent, handlePopState)
if (needsHashChangeListener)
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState)
if (needsHashChangeListener)
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}
const handlePopState = (event) => {
// 忽略无关的 popstate 事件
if (isExtraneousPopstateEvent(event))
return
handlePop(getDOMLocation(event.state))
}
// 返回一个 location 对象
const getDOMLocation = (historyState) => {
const { key, state } = (historyState || {})
const { pathname, search, hash } = window.location
let path = pathname + search + hash
if (basename)
path = stripBasename(path, basename)
return createLocation(path, state, key)
}
let forceNextPop = false
const handlePop = (location) => {
// forceNextPop 默认为 false,所以默认会走 else
if (forceNextPop) {
forceNextPop = false
setState()
} else {
const action = 'POP'
// confirmTransitionTo 正常情况下,会调用最后一个回调函数,
// 然后传入 ok 为 true,执行 setState
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
}
// setState 会把新的 state 合并到 history 对象,然后调用 notifyListeners
// notifyListeners 会执行 listen 时候保存的监听函数
const setState = (nextState) => {
Object.assign(history, nextState)
history.length = globalHistory.length
transitionManager.notifyListeners(
history.location,
history.action
)
}
notifyListeners 的实现,位于 ./createTransitionManager.js
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args))
}
confirmTransitionTo 的实现,位于 ./createTransitionManager.js
const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
// 使用 Prompt 才会进入这个,否则进入 else
if (prompt != null) {
const result = typeof prompt === 'function' ? prompt(location, action) : prompt
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback)
} else {
callback(true)
}
} else {
// Return false from a transition hook to cancel the transition.
callback(result !== false)
}
} else {
// 没有 prompt 就只执行这个
callback(true)
}
}
5.3 history.push
const push = (path, state) => {
const action = 'PUSH'
const location = createLocation(path, state, createKey(), history.location)
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (!ok)
return
const href = createHref(location)
const { key, state } = location
if (canUseHistory) {
// 使用原生 history 的 pushState 方法,改变 url
globalHistory.pushState({ key, state }, null, href)
if (forceRefresh) {
window.location.href = href
} else {
const prevIndex = allKeys.indexOf(history.location.key)
const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1)
nextKeys.push(location.key)
allKeys = nextKeys
setState({ action, location })
}
} else {
window.location.href = href
}
})
}
参考资料
React-Router 官网:
Github 源码:
扫下方二维码或微信搜索,关注公众号「天才前端计划」,获取一手资料。
谢谢您的支持^_^