什么是前端路由
首先,路由的概念开始是后端提出来的,用来跟服务器进行数据/资源获取的一种方式,通过不同的路径,来获取不同的资源
前端随着ajax(不刷新页面情况下请求数据)的流行,推动着异步交互体验提升,随后spa<单页面应用程序>在前端领域大放异彩
spa: 单页面应用程序不仅在页面内交互是无刷新的,连页面之间的跳转也没有刷新。
spa核心思想
监听url变化
改变context的值
匹配相对应的组件
实现前端路由应该包括哪些功能
前端可以自己维护和控制浏览器的history<历史记录>。我们称之为history栈, 保证浏览器在url改变的时候不会刷新页面。并且通过history栈控制浏览器页面的前进和后退
目前 Router有两种实现方式 hash 和 History
hash表示页面中的一个位置,当浏览器页面完全加载好了,页面会滚动到hash位置指定的地。
hash 只作用在浏览器,不会在请求中发送给服务器
hash 发生变化时,浏览器并不会重新给后端发送请求加载页面。
hash 发生变化时会触发 hashchange 事件,在该事件中可以通过 window.location.hash 获取到当前 hash值
History
在 html5 中新增了 history.pushState() 和 history.replaceState(),相比hash路由 url上不美观的hash值 ,取而代之使用 history.pushState 来完成对 window.location 的操作。
History和Hash对比
hash后面使用#来模拟完整的路径,不太美观
用户手动刷新页面, 对于hashRouter来说,后端收到的是同一个地址, history因为直接修改浏览器url,对于后端而言接受不同的地址,需要后端对资源做统一跳转处理
webpack本地开发模式下,用webpack-dev-server插件开启本地服务器,解决请求资源:
historyapifallback
React-Router
React作为前端视图层的框架,本身是不具有除了view层面以外的功能。需要引入React-Router, 对 react的来说管理路由需要管理组件的生命周期,对不同的路由渲染不同的组件。
React-Router 库包含三个包:react-router、react-router-dom 和 react-router-native 。
路由操作的核心包 react-router
,而其他两个是特定环境下使用的。如果我们开发web应用,使用react-router-dom,如果开发RN相关react-router-native。
React-Router实现单页面应用程序路由跳转,分为HashRouter, BrowserRouter
browserHistory 是使用 React-Router 的应用推荐的 history方案
学前小知识
html5 history.pushstate && window.popstate
React.Context
发布订阅设计模式
history.pushstate
history.pushState方法向当前浏览器会话的历史堆栈中添加一个状态
history.pushState(state, title[, url])
state: state状态是一个JavaScript对象, 每当用户导航到新状态时,都会触发popstate事件,并且该事件的状态属性指向了创建历史条目的创建的state属性
title: 大多数浏览器都会忽略这个属性
url: 新历史记录条目的URL由此参数指定, "新的url必须和当前网址同源"
window.popstate
当活动的历史记录更改的时候,将会触发popstate事件,如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用。popstate事件的state属性包含历史条目的状态对象的副本。
histry.pushstate、history.replacestate方法调用不会触发window.popstate事件,只要作为浏览器行为才会触发、例如操作哦了tab页上面前进和后退,或者调用了history.back()、history.forword();
react-router-dom
react-router
history
react-router-dom
react-router-dom在react-router核心基础上扩展了可操作DOM的api, 添加了用于跳转的Link组件、history模式下的BrowserRouter组件和hash模式下的HashRouter组件。
BrowserRouter和HashRouter,调用了history库中createBrowserHistory和createHashHistory方法
react-router
react路由核心包。 提供了路由的核心组件。如Router、Route、Switch等,但没有提供有关dom操作进行路由跳转的api;
history
可以理解为react-router的核心,也是整个路由原理的核心,里面集成了底层路由原理的实现。
BrowserRouter源码分析
BrowserRouter
内部创建了一个全局的history对象<用于监听整个路由的变化>, 并且把history作为props传递的react-router的Router组件
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
Router
构造函数中把history.location作为自己的state,并且监听了location的变化。
render中利用了React的Context提供了RouterContext,HistoryContext两个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._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
this.unlisten = props.history.listen(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();
* @description
* Router中的2个context由HistoryContext和RouterContext组件。 考虑到兼容性,并没有使用 React.createContext 方式 来创建
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
// 解析得到 包含path url params isExact 四个属性的属性。 默认指向了根地址
match: Router.computeRootMatch(this.state.location.pathname),
// 只有StaticRouter会传staticContext用于服务端渲染。 HashRouter 和 BrowserRouter 都是 null
staticContext: this.props.staticContext
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
</RouterContext.Provider>
Route
Route 组件根据自身的传参,对上层 RouterContext 中的部分属性(location 和 match)进行了更新,并且如果当前路径和配置的 path 路径 match,则渲染该组件,渲染的方式有 children,component,render 三种方式,我们最常用的就是 component 方式,注意每种方式的区别
Route的component,render,children三个属性是互斥的
优先级children>component>render
children在无论路由匹配与否,都会渲染
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 };
// 提供3种渲染组件的方式
let { children, component, render } = this.props;
if (Array.isArray(children) && children.length === 0) {
children = null;
// 渲染逻辑
// 当props匹配了路由时,先判断是否匹配,如果不匹配就将props向下传递。
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>
组件渲染逻辑如下:
Switch
switch 用来嵌套在Route外面,当Switch中的第一个Route匹配后就不会渲染其他的Route了
案例见switch.tsx
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 对子元素做遍历
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;
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;
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,
url: path === "/" && url === "" ? "/" : url,
isExact,
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
}, null);
通过审查元素发现,link最终后悔创建一个a标签来包裹要跳转的元素的元素,但是如果只是一个普通的带 href 的 a 标签,那么就会直接跳转到一个新的页面而不是 SPA 了。所以a标签中的默认跳转事件会被禁止调。所以这里的 href 并没有实际的作用,但仍然可以标示出要跳转到的页面的 URL 并且有更好的 html 语义
对没有被 “preventDefault调用 && 鼠标左键点击的 && 非 _blank 跳转 的&& 没有按住其他功能键的“ 单击进行 preventDefault,然后 push 进 history 中,这也是前面讲过的 —— 路由的变化 与 页面的跳转 是不互相关联的,react-router中 Link 中通过 history 库的 push 调用了 H5 history 的 pushState,但是这仅仅会让路由变化,其他什么都没有改变。 之前在Router创建 listen,它会监听路由的变化,然后通过 context 更新 props 和 nextContext 让下层的 Route 去重新匹配,完成需要渲染部分的更新
点击时候进行如下判断。当下面4个条件都满足调用navigate方法。否则新窗口打开
event.defaultPrevented: 返回一个boolean,表明当前事件是否调用了event.preventDefault()
event.button === 0 鼠标左键
target === "_self" 非_blank跳转
!isModifiedEvent: 点击事件发生时候,没有同时按住metaKey, altKey, ctrlKey, shiftKey
Refs转发
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
/ 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 &&
event.button === 0 &&
(!target || target === "_self") &&
!isModifiedEvent(event)
event.preventDefault();
navigate();
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.ref = innerRef;
return <a {...props} />;
* The public API for rendering a history-aware <a>.
const Link = forwardRef(
component = LinkAnchor,
replace,
innerRef, // TODO: deprecate
...rest
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const { history } = context;
// 生成location对象
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
// 拼接完整路径 basename+path
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
// 执行history.push 或者 history.replace 默认 push
method(location);
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
return React.createElement(component, props);
</RouterContext.Consumer>
withRouter
withRouter是一个高阶组件, 可以让普通非包裹在Route的组件也能获取路由信息。把react-router 的 history、location、match 三个对象传入 props上
高阶组件: 高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧、自己不是React Api的一部分, 是基础React组合特性行为的设计模式
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>`
{/* 把context注入到Component中 */}
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
* @description
* 当给组件添加至高阶组件中后,原来的组件会被一组容器组件包含。这样意味着。容器组件不会有原来组件的任何的静态方法
* 为了解决这个问题, 在返回容器组件之前。务必复制wrappedComponent的静态方法到容器组件上
return hoistStatics(C, Component);
有时候React组件定义的静态很有有用,务必复制静态方法
Redirect
该组件在componentDidMount生命周期内,通过history Api跳转到path指定位置, 默认情况下,新位置将覆盖历史堆栈中的当前位置。
function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Redirect> outside a <Router>");
const { history, staticContext } = context;
const method = push ? history.push : history.replace;
// to 重定向地址,可以是一个string, 也可以是对象
// computedMatch 从switch上面拿到match
const location = createLocation(
computedMatch
? typeof to === "string"
? generatePath(to, computedMatch.params)
...to,
pathname: generatePath(to.pathname, computedMatch.params)
// 服务端渲染直接执行方法一次
if (staticContext) {
method(location);
return null;
// lifeCycle不会渲染任何页面, 只有一些生命周期函数componentDidMount、componentDidUpdate、componentWillUnmount
return (
<Lifecycle
onMount={() => {
method(location);
// componentDidUpdate 时候判断当前 location 和上一个 location 是否发生变化
// 一般来说在componentDidMount就调走了。不会走到ComponentDidUpdate
onUpdate={(self, prevProps) => {
const prevLocation = createLocation(prevProps.to);
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
method(location);
to={to}
</RouterContext.Consumer>
Prompt
用于路由切换提示,在某些场景下比较有用,比较用户咋某个页面修改数据。离开页面的时候,提示用户是保存。 详细在history中block明确解释
message:用于显示提示的文本信息。
when:默认是 true,设置成 false 时,失效
Prompt 的本质是在 when 为 true 的时候,调用 context.history.block 方法,为全局注册路由监听。见prompt.tsx
function Prompt({ message, when = true }) {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Prompt> outside a <Router>");
if (!when || context.staticContext) return null;
// 调用history.block注册全局路由监听器
// message可以是字符串也可以是一个函数, 如果是字符串默认调用window.confirm方法
// 如果是一个函数,需要返回一个boolean判断是否需要拦截
const method = context.history.block;
return (
<Lifecycle
onMount={self => {
// 调用了 history.block 方法
self.release = method(message);
onUpdate={(self, prevProps) => {
if (prevProps.message !== message) {
self.release();
self.release = method(message);
onUnmount={self => {
self.release();
message={message}
</RouterContext.Consumer>
hooks
hooks是react16.8引入的特性,允许我们在不写class的情况下,操作state和react其他特新。为了在hooks即函数式组件能够操作路由。react-router提供了hooks方法, 底层都是使用React.useContext api <可以获取指定context的值>
useHistory: 返回一个history对象
useLocation 返回context下的location对象
useParams 返回当前匹配路径的params
useRouteMatch 可以有一个参数 path,如果什么都不传,会返回当前 context 上的 match 的值。 如果传了 path,会比较这个 path 和当前 location 是否 match
const useContext = React.useContext;
export function useHistory() {
if (__DEV__) {
invariant(
typeof useContext === "function",
"You must use React >= 16.8 in order to use useHistory()"
return useContext(HistoryContext);
export function useLocation() {
if (__DEV__) {
invariant(
typeof useContext === "function",
"You must use React >= 16.8 in order to use useLocation()"
return useContext(Context).location;
export function useParams() {
if (__DEV__) {
invariant(
typeof useContext === "function",
"You must use React >= 16.8 in order to use useParams()"
const match = useContext(Context).match;
return match ? match.params : {};
export function useRouteMatch(path) {
if (__DEV__) {
invariant(
typeof useContext === "function",
"You must use React >= 16.8 in order to use useRouteMatch()"
const location = useLocation();
const match = useContext(Context).match;
return path ? matchPath(location.pathname, path) : match;
Router初始化,创建监听函数, 底层逻辑全部由history库管理
当点击Link标签的时候,实际上点击是页面渲染出来的a标签。通过preventDefault来组织a标签的页面跳转
Link中拿到context传递的history, 进行路由跳转
路由发生变化,触发了监听函数,Router会重新setState, 每次路由变化 -> 触发顶层 Router 的监听事件 -> Router 触发 setState -> 向下传递新的 nextContext
下层Route组件拿到最新nextContext后判断当前path和location是否匹配。内置component,render,children三个属性的渲染机制,并且通过switch组件匹配唯一的路由
history库源码分析
原理就是封装了原生的html5 的history api, 如pushState, replaceState。当这些事件被触发的时候,执行响应回调函数,用于操控和观察地址栏的变更
history库创建了一个虚拟的history对象, 操纵浏览器地址变更,或者操作hash变更、管理内存中的虚拟历史堆栈
utils
pathUtils
function addLeadingSlash(path) {
return path.charAt(0) === '/' ? path : '/' + path;
function stripLeadingSlash(path) {
return path.charAt(0) === '/' ? path.substr(1) : path;
function hasBasename(path, prefix) {
return path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && '/?#'.indexOf(path.charAt(prefix.length)) !== -1
;
function stripBasename(path, prefix) {
return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
function stripTrailingSlash(path) {
return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
function parsePath(path) {
var pathname = path || '/';
var search = '';
var hash = '';
var hashIndex = pathname.indexOf('#');
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex);
pathname = pathname.substr(0, hashIndex);
var searchIndex = pathname.indexOf('?');
if (searchIndex !== -1) {
search = pathname.substr(searchIndex);
pathname = pathname.substr(0, searchIndex);
return {
pathname: pathname,
search: search === '?' ? '' : search,
hash: hash === '#' ? '' : hash
function createPath(location) {
var pathname = location.pathname,
search = location.search,
hash = location.hash;
var path = pathname || '/';
if (search && search !== '?') path += search.charAt(0) === '?' ? search : "?" + search;
if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : "#" + hash;
return path;
domUtils
var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
function getConfirmation(message, callback) {
callback(window.confirm(message));
* Returns true if the HTML5 history API is supported. Taken from Modernizr.
* https://github.com/Modernizr/Modernizr/blob/master/LICENSE
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
* changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586
function supportsHistory() {
var ua = window.navigator.userAgent;
if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1) return false;
return window.history && 'pushState' in window.history;
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
* Returns false if using go(n) with hash history causes a full page reload.
function supportsGoWithoutReloadUsingHash() {
return window.navigator.userAgent.indexOf('Firefox') === -1;
* Returns true if a given popstate event is an extraneous WebKit event.
* Accounts for the fact that Chrome on iOS fires real popstate events
* containing undefined state when pressing the back button.
function isExtraneousPopstateEvent(event) {
return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
var PopStateEvent = 'popstate';
var HashChangeEvent = 'hashchange';
function getHistoryState() {
try {
return window.history.state || {};
} catch (e) {
return {};
createLocation
生成 {pathname, search, hash, state, key} 对象
function createLocation(path, state, key, currentLocation) {
var location;
if (typeof path === 'string') {
location = parsePath(path);
location.state = state;
} else {
location = _extends({}, path);
if (location.pathname === undefined) location.pathname = '';
if (location.search) {
if (location.search.charAt(0) !== '?') location.search = '?' + location.search;
} else {
location.search = '';
if (location.hash) {
if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
} else {
location.hash = '';
if (state !== undefined && location.state === undefined) location.state = state;
try {
location.pathname = decodeURI(location.pathname);
} catch (e) {
if (e instanceof URIError) {
throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.');
} else {
throw e;
if (key) location.key = key;
if (currentLocation) {
if (!location.pathname) {
location.pathname = currentLocation.pathname;
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
} else {
if (!location.pathname) {
location.pathname = '/';
return location;
createTransitionManager
创建任务管理器=> 控制路由跳转以及添加路由监听函数
function createTransitionManager() {
var prompt = null;
function setPrompt(nextPrompt) {
process.env.NODE_ENV !== "production" ? warning(prompt == null, 'A history supports only one prompt at a time') : void 0;
prompt = nextPrompt;
return function () {
if (prompt === nextPrompt) prompt = null;
function confirmTransitionTo(location, action, getUserConfirmation, callback) {
if (prompt != null) {
var result = typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
} else {
process.env.NODE_ENV !== "production" ? warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message') : void 0;
callback(true);
} else {
callback(result !== false);
} else {
callback(true);
var listeners = [];
function appendListener(fn) {
var isActive = true;
function listener() {
if (isActive) fn.apply(void 0, arguments);
listeners.push(listener);
return function () {
isActive = false;
listeners = listeners.filter(function (item) {
return item !== listener;
function notifyListeners() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
listeners.forEach(function (listener) {
return listener.apply(void 0, args);
return {
setPrompt: setPrompt,
confirmTransitionTo: confirmTransitionTo,
appendListener: appendListener,
notifyListeners: notifyListeners
createBrowserHistory
if (props === void 0) {
props = {};
!canUseDOM ? process.env.NODE_ENV !== "production" ? invariant(false, 'Browser history needs a DOM') : invariant(false) : void 0;
var globalHistory = window.history;
var canUseHistory = supportsHistory();
var needsHashChangeListener = !supportsPopStateOnHashChange();
var _props = props,
_props$forceRefresh = _props.forceRefresh,
forceRefresh = _props$forceRefresh === void 0 ? false : _props$forceRefresh,
_props$getUserConfirm = _props.getUserConfirmation,
getUserConfirmation = _props$getUserConfirm === void 0 ? getConfirmation : _props$getUserConfirm,
_props$keyLength = _props.keyLength,
keyLength = _props$keyLength === void 0 ? 6 : _props$keyLength;
var basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
function getDOMLocation(historyState) {
var _ref = historyState || {},
key = _ref.key,
state = _ref.state;
var _window$location = window.location,
pathname = _window$location.pathname,
search = _window$location.search,
hash = _window$location.hash;
var path = pathname + search + hash;
process.env.NODE_ENV !== "production" ? warning(!basename || hasBasename(path, basename), 'You are attempting to use a basename on a page whose URL path does not begin ' + 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".') : void 0;
if (basename) path = stripBasename(path, basename);
return createLocation(path, state, key);
function createKey() {
return Math.random().toString(36).substr(2, keyLength);
var transitionManager = createTransitionManager();
function setState(nextState) {
_extends(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
function handlePopState(event) {
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
var forceNextPop = false;
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
var action = 'POP';
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (ok) {
setState({
action: action,
location: location
} else {
revertPop(location);
function revertPop(fromLocation) {
var toLocation = history.location;
var toIndex = allKeys.indexOf(toLocation.key);
if (toIndex === -1) toIndex = 0;
var fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) fromIndex = 0;
var delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
var initialLocation = getDOMLocation(getHistoryState());
var allKeys = [initialLocation.key];
function createHref(location) {
return basename + createPath(location);
function push(path, state) {
process.env.NODE_ENV !== "production" ? warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' + 'argument is a location-like object that already has state; it is ignored') : void 0;
var action = 'PUSH';
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
globalHistory.pushState({
key: key,
state: state
}, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
var prevIndex = allKeys.indexOf(history.location.key
);
var nextKeys = allKeys.slice(0, prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({
action: action,
location: location
} else {
process.env.NODE_ENV !== "production" ? warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history') : void 0;
window.location.href = href;
function replace(path, state) {
process.env.NODE_ENV !== "production" ? warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' + 'argument is a location-like object that already has state; it is ignored') : void 0;
var action = 'REPLACE';
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
globalHistory.replaceState({
key: key,
state: state
}, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
var prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({
action: action,
location: location
} else {
process.env.NODE_ENV !== "production" ? warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history') : void 0;
window.location.replace(href);
function go(n) {
globalHistory.go(n);
function goBack() {
go(-1);
function goForward() {
go(1);
var listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) window.removeEventListener(HashChangeEvent, handleHashChange);
var isBlocked = false;
function block(prompt) {
if (prompt === void 0) {
prompt = false;
var unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
return function () {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
return unblock();
function listen(listener) {
var unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return function () {
checkDOMListeners(-1);
unlisten();
var history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref: createHref,
push: push,
replace: replace,
go: go,
goBack: goBack,
goForward: goForward,
block: block,
listen: listen
return history;
首先BrowserRouter通过history库创建了history对象,并且把此对象通过props形势传递Router组件
Router组件使用hisroty中listen,注册了资深的setState方法,当路由发生发生改变的时候<出发popstate,手动push>组件就会执行setState方法,完成整个组件数的render
history是一个对象 包含了各种操作页面的方法。 同时会用Router的props里面forceRefresh、basename、getUserComfirmation、keyLength 来生成一个初始化的location对象
拿到从初始化的location对象,history开始封装push,replace,go,goback等方法。对于任何地址栏上的更新,都会执行confirmTransitionTo验证,这个方法是为了支持prompt拦截器功能, 正常在拦截器关闭的情况下,每次调用push或者replace都会随机生成一个key,代表这个路径的唯一hash值,并将用户传递的state和key作为state,注意这部分state会被保存到 浏览器 中是一个长效的缓存,将拼接好的path作为传递给history的第三个参数
地址栏地址得到更新后,页面在不使用foreceRefrsh的情况下是不会自动更新的, 需要执行setState, 同时执行调度中心里面监听函数
history有block方法,这个方法初衷是实现对路由跳转的拦截,我们知道浏览器的回退和前进操作按钮是无法进行拦截的, 只能做hack。为了history抽离了一个路由控制器createTransitionManager,里面维护了一个prompt开关, 每当prompt存在的时候,默认会被window.confirm拦截,如果确认拦截,则页面仍然会停留在当前页面中,但是地址栏已经更新了地址,这就产生了矛盾。需要把地址栏路径重置为之前的路径。为了实现这功能。都会在allkeys找到当前key对应的下表,以及之前页面key对应的下标。以两者下标差做一个回滚
正是因为有了Prompt才会促使history添加key
手动实现mini-router
utils
export interface Path {
hash: string
search: string
pathname: string
export function parsePath(path: string) {
const partialPath = {} as Path
const pathname = path
if (path) {
const hashIndex = path.indexOf('#')
if (hashIndex >= 0) {
partialPath.hash = path.substr(hashIndex)
pathname = path.substr(0, hashIndex)
const searchIndex = path.indexOf('?')
if (searchIndex >= 0) {
partialPath.search = path.substr(searchIndex)
pathname = path.substr(0, searchIndex)
if (path) {
partialPath.pathname = pathname
return partialPath
history
import { parsePath } from './utils'
export interface History {
push(): void
export type State = object | null
export type Listener = (location: Location) => void
export interface Location {
state: State
hash: string
search: string
pathname: string
const getLocation = (): Location => {
const { pathname, search, hash } = window.location
return {
pathname,
search,
hash,
state: null,
let location = getLocation()
const getNextLocation = (to: string, state: State = null) => ({
...parsePath(to),
state,
const listeners: Listener[] = []
const push = (to: string, state?: State) => {
location = getNextLocation(to, state)
window.history.pushState(state, '', to)
listeners.forEach((fn) => fn(location))
const listen = (fn: Listener) => {
listeners.push(fn)
return function unlisten() {
listeners = listeners.filter((listener) => listener !== fn)
window.addEventListener('popstate', () => {
location = getLocation()
listeners.forEach((fn) => fn(location))
export const history = {
get location() {
return location
push,
listen,
Router
import { history, Location } from './history'
import React, { useState, useEffect } from
'react'
interface RouterContextProps {
location: Location
history: typeof history
export const RouterContext = React.createContext<RouterContextProps | null>(null)
export const Router: React.FC = ({ children }) => {
const [location, setLocation] = useState(history.location)
useEffect(() => {
const unlisten = history.listen((newLocation) => {
setLocation(newLocation)
return unlisten
}, [])
return <RouterContext.Provider value={{ history, location }}>{children}</RouterContext.Provider>
Route
import { ReactNode } from 'react'
import { useLocation } from './hooks'
interface RouteProps {
path: string
children: ReactNode
export const Route = ({ path, children }: RouteProps) => {
const { pathname } = useLocation()
const matched = path === pathname
if (matched) {
return children
return null
hooks
import React from 'react'
import { RouterContext } from './Router'
export const useHistory = () => React.useContext(RouterContext)!.history
export const useLocation = () => React.useContext(RouterContext)!.location
import React from 'react'
import { Card, Button, Divider } from 'antd'
import { Router, Route, useHistory } from './customizeRouter/index'
const List = () => <h1>列表</h1>
const Detail = () => <h1>详情</h1>
const About = () => <h1>关于我</h1>
const RenderRoute = () => {
const history = useHistory()
const go = (path: string) => {
const state = { name: Math.random().toString(36).substr(2, 8) }
history.push(path, state)
return (
<Button type="link" size="small" onClick={() => go('/list')}>
</Button>
<Button type="link" size="small" onClick={() => go('/detail')}>
</Button>
<Button type="link" size="small" onClick={() => go('/about')}>
</Button>
</div>
export default () => (
<Card title="自定义mini-router">
<Router>
<RenderRoute />
<Divider />
<Route path="/list">
<List />
</Route>
<Route path="/detail">
<Detail />
</Route>
<Route path="/about">
<About />
</Route>
</Router>
</Card>