注:react-router版本为v5.2.0
准备常识
1.前端根底:history、location。
2.react: refs转发、context、useContext(react Hooks)。
3.依赖库:
history(^4.9.0)
path-to-regexp(^1.7.0):次要用到pathToRegexp.compile(path)、pathToRegexp(path, keys, options)两个办法
history库(^4.9.0)
history库是react-router依赖的外围库,它将利用的history做了对立的形象,蕴含一系列对立的属性和办法,反对浏览器的BrowserHistory、HashHistory以及服务端的MemoryHistory。
createBrowserHistory的属性和办法
length: globalHistory.length,action: 'POP',location: initialLocation,createHref,push,replace,go,goBack,goForward,block,listen
createHashHistory的属性和办法
length: globalHistory.length,action: 'POP',location: initialLocation,createHref,push,replace,go,goBack,goForward,block,listen
createMemoryHistory的属性和办法
length: entries.length,action: 'POP',location: entries[index],index,entries,createHref,push,replace,go,goBack,goForward,canGo,block,listen
接下来咱们解说一下这三种history的具体实现。
createTransitionManager
createTransitionManager能够创立一个TransitionManager来帮忙history治理各种行为,它被三种history都应用了,咱们先来介绍它。
这是createTransitionManager的次要性能代码,很容易了解,就是实现了一个公布订阅模式。
let listeners = [];function appendListener(fn) { let isActive = true; function listener(...args) { if (isActive) fn(...args); } listeners.push(listener); return () => { isActive = false; listeners = listeners.filter(item => item !== listener); };}function notifyListeners(...args) { listeners.forEach(listener => listener(...args));}
setPrompt()是显示可提醒用户进行输出的对话框的意思,这个性能次要是为了一些典型场景,比方:用户点击手机的返回键,让用户确认是否返回上一个url。
let prompt = null;function setPrompt(nextPrompt) { warning(prompt == null, 'A history supports only one prompt at a time'); prompt = nextPrompt; return () => { if (prompt === nextPrompt) prompt = null; };}
confirmTransitionTo在history的行为办法中(push、pop、replace)都会被调用,它的作用是拦挡每个行为,让用户或开发者确认是否执行这个行为。
function confirmTransitionTo( location, action, getUserConfirmation, callback) { // TODO: If another transition starts while we're still confirming // the previous one, we may end up in a weird state. Figure out the // best way to handle this. if (prompt != null) { const result = typeof prompt === 'function' ? prompt(location, action) : prompt; if (typeof result === 'string') { if (typeof getUserConfirmation === 'function') { getUserConfirmation(result, callback); } else { warning( false, 'A history needs a getUserConfirmation function in order to use a prompt message' ); callback(true); } } else { // Return false from a transition hook to cancel the transition. callback(result !== false); } } else { callback(true); }}
例如,push办法中confirmTransitionTo是这样应用的,在第四个参数callback中依据返回值是否为true,判断是否真正执行push行为。
function push(path, state) { const action = 'PUSH'; // ... transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (!ok) return; // ... } );}
history.listen
history.listen在浏览器中次要是利用DOM办法进行事件监听的绑定和勾销。
browserHistory中的实现
browserHistory应用的是popstate和hashchange事件。
同时会将监听触发的回调函数增加到后面介绍的transitionManager中,这样监听触发时只须要通过执行transitionManager.notifyListeners()发送告诉,执行这些回调函数就能够了。
const PopStateEvent = 'popstate';const HashChangeEvent = 'hashchange';let 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); }}function listen(listener) { const unlisten = transitionManager.appendListener(listener); checkDOMListeners(1); return () => { checkDOMListeners(-1); unlisten(); };}
在很多浏览器中hash change也会触发popstate事件,所以hashchange事件在browserHistory中也是须要监听的。
const needsHashChangeListener = !supportsPopStateOnHashChange();export function supportsPopStateOnHashChange() { return window.navigator.userAgent.indexOf('Trident') === -1;}
hashHistory中的实现
hashHistory中只须要监听hashchange事件就能够了
const HashChangeEvent = 'hashchange';let listenerCount = 0;function checkDOMListeners(delta) { listenerCount += delta; if (listenerCount === 1 && delta === 1) { window.addEventListener(HashChangeEvent, handleHashChange); } else if (listenerCount === 0) { window.removeEventListener(HashChangeEvent, handleHashChange); }}
memoryHistory中的实现
memoryHistory不须要监听事件,它只须要将监听触发的回调函数增加到transitionManager中就能够了。因为它是服务端被动管制的路由,不须要监听被动的路由扭转,进而执行一些状态更新。
function listen(listener) { return transitionManager.appendListener(listener);}
browserHistory中handlePopState的实现
hashHistory和memoryHistory是没有popState事件的,所以不须要实现它们。
handlePopState次要会执行handlePop办法,handlePop次要会执行setState办法,setState办法次要是合并了history状态,通过transitionManager.notifyListeners告诉了增加的listener函数执行。
getDOMLocation生成的就是咱们常常见到的location参数。
{ pathname, state, hash, search, key, ...}
function handlePopState(event) { // Ignore extraneous popstate events in WebKit. if (isExtraneousPopstateEvent(event)) return; handlePop(getDOMLocation(event.state));}function handlePop(location) { if (forceNextPop) { forceNextPop = false; setState(); } else { const action = 'POP'; transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (ok) { setState({ action, location }); } else { revertPop(location); } } ); }}function setState(nextState) { Object.assign(history, nextState); history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action);}
confirmTransitionTo的回调函数范畴为false的时候,阐明禁止进行这次路由操作。它调用revertPop办法实现,通过计算此次路由操作的delta,调用go(delta)办法将路由复原到原来的状态,go办法就是原生的history.go办法。
function revertPop(fromLocation) { const toLocation = history.location; // TODO: We could probably make this more reliable by // keeping a list of keys we've seen in sessionStorage. // Instead, we just default to 0 for keys we don't know. let toIndex = allKeys.indexOf(toLocation.key); if (toIndex === -1) toIndex = 0; let fromIndex = allKeys.indexOf(fromLocation.key); if (fromIndex === -1) fromIndex = 0; const delta = toIndex - fromIndex; if (delta) { forceNextPop = true; go(delta); }}const globalHistory = window.history;function go(n) { globalHistory.go(n);}
handleHashChange
browserHistory中的实现
它调用的其实次要也是handlePop办法
function getHistoryState() { try { return window.history.state || {}; } catch (e) { // IE 11 sometimes throws when accessing window.history.state // See https://github.com/ReactTraining/history/pull/289 return {}; }}function handleHashChange() { handlePop(getDOMLocation(getHistoryState()));}
hashHistory中的实现
path !== encodedPath这个判断是为了让咱们总是有规范的hash门路,前面的操作判断次要是判断一下前后的location是否雷同、是否是ignorePath,如果都不是,则会执行handlePop办法。
function handleHashChange() { const path = getHashPath(); const encodedPath = encodePath(path); if (path !== encodedPath) { // Ensure we always have a properly-encoded hash. replaceHashPath(encodedPath); } else { const location = getDOMLocation(); const prevLocation = history.location; if (!forceNextPop && locationsAreEqual(prevLocation, location)) return; // A hashchange doesn't always == location change. if (ignorePath === createPath(location)) return; // Ignore this change; we already setState in push/replace. ignorePath = null; handlePop(location); }}function replaceHashPath(path) { const hashIndex = window.location.href.indexOf('#'); window.location.replace( window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path );}
handlePop和后面browserHistory介绍的是相似的,有区别的中央是revertPop应用的allPaths作为history的索引,browserHistory应用的allKeys。
function handlePop(location) { if (forceNextPop) { forceNextPop = false; setState(); } else { const action = 'POP'; transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (ok) { setState({ action, location }); } else { revertPop(location); } } ); }}function setState(nextState) { Object.assign(history, nextState); history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action);}function revertPop(fromLocation) { const toLocation = history.location; // TODO: We could probably make this more reliable by // keeping a list of paths we've seen in sessionStorage. // Instead, we just default to 0 for paths we don't know. let toIndex = allPaths.lastIndexOf(createPath(toLocation)); if (toIndex === -1) toIndex = 0; let fromIndex = allPaths.lastIndexOf(createPath(fromLocation)); if (fromIndex === -1) fromIndex = 0; const delta = toIndex - fromIndex; if (delta) { forceNextPop = true; go(delta); }}const globalHistory = window.history;function go(n) { warning( canGoWithoutReload, 'Hash history go(n) causes a full page reload in this browser' ); globalHistory.go(n);}
allPaths是残缺的门路
export function createPath(location) { const { pathname, search, hash } = location; let path = pathname || '/'; if (search && search !== '?') path += search.charAt(0) === '?' ? search : `?${search}`; if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : `#${hash}`; return path;}
allKeys是随机的key
function createKey() { return Math.random() .toString(36) .substr(2, keyLength); }
history.push
browserHistory中的实现
history.push办法很简略,次要调用了history.pushState办法。因为allKeys保护了所有history state中的key,所以在push办法须要做相应的解决。
const globalHistory = window.history;function push(path, state) { 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' ); 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) { 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 { warning( state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history' ); window.location.href = href; } } );}
hashHistory中的实现
history.push办法很简略,次要调用了window.location.hash办法。因为allPaths保护了所有的path,所以在push办法须要做相应的解决。
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 ? 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;}
memoryHistory中的实现
因为是在内存中保护history的状态,所以次要是history.entries(所有history location列表)的保护。
function push(path, state) { 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' ); const action = 'PUSH'; const location = createLocation(path, state, createKey(), history.location); transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (!ok) return; const prevIndex = history.index; const nextIndex = prevIndex + 1; const nextEntries = history.entries.slice(0); if (nextEntries.length > nextIndex) { nextEntries.splice( nextIndex, nextEntries.length - nextIndex, location ); } else { nextEntries.push(location); } setState({ action, location, index: nextIndex, entries: nextEntries }); } );}function setState(nextState) { Object.assign(history, nextState); history.length = history.entries.length; transitionManager.notifyListeners(history.location, history.action);}
history.replace()
browerHistory中的实现
history.replace办法和push是相似的,次要调用了history.replaceState办法。
function replace(path, state) { 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' ); const action = 'REPLACE'; 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) { globalHistory.replaceState({ key, state }, null, href); if (forceRefresh) { window.location.replace(href); } else { const prevIndex = allKeys.indexOf(history.location.key); if (prevIndex !== -1) allKeys[prevIndex] = location.key; setState({ action, location }); } } else { warning( state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history' ); window.location.replace(href); } } );}
hashHistory中的实现
replace在hashHistory中的实现也很简略,次要调用了window.location.replace办法。
function replace(path, state) { warning( state === undefined, 'Hash history cannot replace state; it is ignored' ); const action = 'REPLACE'; 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 REPLACE, 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; replaceHashPath(encodedPath); } const prevIndex = allPaths.indexOf(createPath(history.location)); if (prevIndex !== -1) allPaths[prevIndex] = path; setState({ action, location }); } );}function replaceHashPath(path) { const hashIndex = window.location.href.indexOf('#'); window.location.replace( window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path );}
memoryHistory中的实现
function replace(path, state) { 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' ); const action = 'REPLACE'; const location = createLocation(path, state, createKey(), history.location); transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (!ok) return; history.entries[history.index] = location; setState({ action, location }); } );}
history.go()、history.goBack()、history.goForward()
browserHistory、hashHistory中的实现
function go(n) { globalHistory.go(n);}function goBack() { go(-1);}function goForward() { go(1);}
memoryHistory中的实现
计算nextIndex(个别为history.index + n),执行POP action即可。
function clamp(n, lowerBound, upperBound) { return Math.min(Math.max(n, lowerBound), upperBound);}function go(n) { const nextIndex = clamp(history.index + n, 0, history.entries.length - 1); const action = 'POP'; const location = history.entries[nextIndex]; transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (ok) { setState({ action, location, index: nextIndex }); } else { // Mimic the behavior of DOM histories by // causing a render after a cancelled POP. setState(); } } );}function goBack() { go(-1);}function goForward() { go(1);}
history.block()
browserHistory、hashHistory中的实现
block提供了setPrompt的调用接口,因为咱们后面介绍过,push、pop、replace action都是在transitionManager.confirmTransitionTo的回调函数中执行的,只有回调函数返回true,能力真正执行这些action。而后面咱们看到回调函数的返回后果其实是由用户传递的prompt办法决定的,这样就能够让用户依据本人的逻辑决定是否阻塞路由跳转了。
let isBlocked = false;function block(prompt = false) { const unblock = transitionManager.setPrompt(prompt); if (!isBlocked) { checkDOMListeners(1); isBlocked = true; } return () => { if (isBlocked) { isBlocked = false; checkDOMListeners(-1); } return unblock(); };}
memoryHistory中的实现
memoryHistory不须要做DOM事件监听的相干解决。
function block(prompt = false) { return transitionManager.setPrompt(prompt);}
react-router
咱们之所以大篇幅介绍history库,是因为history库才是路由治理的底层逻辑,react-router其实只是应用react框架封装了history库的解决(次要应用context跨组件传递history的状态和办法)。介绍到这,你是不是曾经可能大抵勾画出诸如<BrowserRouter>
、<Route>
、<Switch>
、<Link>
、withRouter()
等的简略实现了呢?介绍来让咱们看看react-router中具体是怎么实现的。
createNamedContext()
该办法能够创立有displayName的context。
// TODO: Replace with React.createContext once we can assume React 16+import createContext from "mini-create-react-context";const createNamedContext = name => { const context = createContext(); context.displayName = name; return context;};export default createNamedContext;
generatePath()
生成门路,次要调用的是pathToRegexp.compile()办法,generatePath能够依据门路path和参数params生成残缺的门路。比方('/a/:id', { id: 1 }) -> '/a/1'
。
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;
matchPath()
该办法传入pathname,以及解析pathname的配置,能够失去从pathname中匹配的后果。这是咱们应用react-router常常见到的数据,没错,它就是通过matchPath办法解析的。
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; }, {})};
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;
historyContext
创立historyContext。
import createNamedContext from "./createNameContext";const historyContext = /*#__PURE__*/ createNamedContext("Router-History");export default historyContext;
routerContext
创立routerContext。这里源码的写法有冗余了。
// TODO: Replace with React.createContext once we can assume React 16+import createContext from "mini-create-react-context";const createNamedContext = name => { const context = createContext(); context.displayName = name; return context;};const context = /*#__PURE__*/ createNamedContext("Router");export default context;
Lifecycle
创立一个react组件,它是一个空组件,次要是为了在组件生命周期的各个阶段可能调用用户通过props传入的回调函数。
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;
Router
<Router>
是咱们很罕用的组件,有了后面的常识铺垫,它的实现就非常简单了。
组件外部有一个location的state,如果不是动态路由,通过history.listen办法监听history的变动。这里的history就是咱们后面介绍的history库生成的history,它能够采纳browserHistory、hashHistory、memoryHistory,history库对这三种history做了统一的接口封装。history如果产生扭转,就是调用this.setState({ location })
,组件从新渲染,RouterContext.Provider、HistoryContext.Provider的值更新,它们上面的跨级组件也能感知到,从而取得最新的参数和办法。
import React from "react";import PropTypes from "prop-types";import warning from "tiny-warning";import HistoryContext from "./HistoryContext.js";import RouterContext from "./RouterContext.js";/** * 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._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(); } 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> ); }}export default Router;
Route
应用RouterContext.Consumer能够感知到下层RouterContext.Provider值的变动,从而主动计算match,依据match的后果渲染匹配的业务组件(应用props传入children, component, render办法之一)。
如果有computedMatch属性阐明在<Switch>
组件中曾经计算了match,能够间接应用。Switch组件咱们前面会介绍。
import React from "react";import { isValidElementType } from "react-is";import PropTypes from "prop-types";import invariant from "tiny-invariant";import warning from "tiny-warning";import RouterContext from "./RouterContext.js";import matchPath from "./matchPath.js";function isEmptyChildren(children) { return React.Children.count(children) === 0;}function evalChildrenDev(children, props, path) { const value = children(props); warning( value !== undefined, "You returned `undefined` from the `children` function of " + `<Route${path ? ` path="${path}"` : ""}>, but you ` + "should have returned a React element or `null`" ); return value || null;}/** * 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) && children.length === 0) { 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> ); }}export default Route;
Redirect
重定向组件依据传入的push属性能够决定应用history.push还是history.replace进行重定向,依据传入computedMatch, to能够计算出重定向的location。如果在动态组件中,会间接执行重定向。如果不是,采纳应用空组件Lifecycle,在组件挂载阶段重定向,在onUpdate中判断重定向是否实现。
import React from "react";import PropTypes from "prop-types";import { createLocation, locationsAreEqual } from "history";import invariant from "tiny-invariant";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 => { invariant(context, "You should not use <Redirect> outside a <Router>"); const { history, staticContext } = context; const method = push ? history.push : history.replace; 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;
Switch
被Switch组件包裹的组件只会渲染其中第一个路由匹配胜利的组件。
次要通过React.Children.forEach(this.props.children, child => {}),遍历出第一个匹配的路由及组件,并通过React.cloneElement返回这个组件。
import React from "react";import PropTypes from "prop-types";import invariant from "tiny-invariant";import warning from "tiny-warning";import RouterContext from "./RouterContext.js";import matchPath from "./matchPath.js";/** * The public API for rendering the first <Route> that matches. */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; // 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; 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;
StaticRouter
动态路由组件本人实现了一个简略的history,没有监听history变动的概念,也不须要go、goBack、goForward、listen、block办法。
import React from "react";import PropTypes from "prop-types";import { createLocation, createPath } from "history";import invariant from "tiny-invariant";import warning from "tiny-warning";import Router from "./Router.js";function addLeadingSlash(path) { return path.charAt(0) === "/" ? path : "/" + path;}function addBasename(basename, location) { if (!basename) return location; return { ...location, pathname: addLeadingSlash(basename) + location.pathname };}function stripBasename(basename, location) { if (!basename) return location; const base = addLeadingSlash(basename); if (location.pathname.indexOf(base) !== 0) return location; return { ...location, pathname: location.pathname.substr(base.length) };}function createURL(location) { return typeof location === "string" ? location : createPath(location);}function staticHandler(methodName) { return () => { invariant(false, "You cannot %s with <StaticRouter>", methodName); };}function noop() {}/** * The public top-level API for a "static" <Router>, so-called because it * can't actually change the current location. Instead, it just records * location changes in a context object. Useful mainly in testing and * server-rendering scenarios. */class StaticRouter extends React.Component { navigateTo(location, action) { const { basename = "", context = {} } = this.props; context.action = action; context.location = addBasename(basename, createLocation(location)); context.url = createURL(context.location); } handlePush = location => this.navigateTo(location, "PUSH"); handleReplace = location => this.navigateTo(location, "REPLACE"); handleListen = () => noop; handleBlock = () => noop; render() { const { basename = "", context = {}, location = "/", ...rest } = this.props; const history = { createHref: path => addLeadingSlash(basename + createURL(path)), action: "POP", location: stripBasename(basename, createLocation(location)), push: this.handlePush, replace: this.handleReplace, go: staticHandler("go"), goBack: staticHandler("goBack"), goForward: staticHandler("goForward"), listen: this.handleListen, block: this.handleBlock }; return <Router {...rest} history={history} staticContext={context} />; }}export default StaticRouter;
MemoryRouter
MemoryRouter的history指定应用了createMemoryHistory,外部逻辑就是Router的逻辑。
import React from "react";import PropTypes from "prop-types";import { createMemoryHistory as createHistory } from "history";import warning from "tiny-warning";import Router from "./Router.js";/** * The public API for a <Router> that stores location in memory. */class MemoryRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />; }}export default MemoryRouter;
Prompt
Prompt组件当Router不是staticRouter且when属性为true时才失效。
调用的是后面介绍的history.block()办法。
import React from "react";import PropTypes from "prop-types";import invariant from "tiny-invariant";import Lifecycle from "./Lifecycle.js";import RouterContext from "./RouterContext.js";/** * The public API for prompting the user before navigating away from a screen. */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; const method = context.history.block; return ( <Lifecycle onMount={self => { 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> );}export default Prompt;
withRouter
因为从RouterContext.Consumer的context中能够很不便取到路由参数,所以withRouter就很容易实现了。只须要应用高阶组件的模式,接管被包裹组件作为参数,将context作为参数传入被包裹组件组件,再返回这个组件即可。
component还裸露了wrappedComponentRef属性,能够转发ref。
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; return hoistStatics(C, Component);}export default withRouter;
hooks
react-router还应用了useContext hook应用react hook的形式来提供一些路由参数和history。
import React from "react";import invariant from "tiny-invariant";import Context from "./RouterContext.js";import HistoryContext from "./HistoryContext.js";import matchPath from "./matchPath.js";const useContext = React.useContext;export function useHistory() { return useContext(HistoryContext);}export function useLocation() { return useContext(Context).location;}export function useParams() { const match = useContext(Context).match; return match ? match.params : {};}export function useRouteMatch(path) { const location = useLocation(); const match = useContext(Context).match; return path ? matchPath(location.pathname, path) : match;}
react-router-dom
react-router中还包含react-router-dom库的实现,来提供dom相干的路由操作。
咱们在react工程中个别应用的就是react-router-dom库,它的底层是后面介绍的react-router。
BrowserRouter
咱们在我的项目中应用HTML5 history管制路由,能够间接应用react-router-dom中的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} />; }}export default BrowserRouter;
HashRouter
咱们在我的项目中应用window.location.hash管制路由,能够间接应用react-router-dom中的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} />; }}export default HashRouter;
Link
<Link>
组件是react-router中常见的路由跳转组件。它应用的是html的a标签,为其绑定了点击事件。用户点击时,既能够执行用户自定义的onClick回调函数,也会执行navigate -> method(location),method能够依据用户传入的replace参数决定是应用history.replace还是history.push,同时点击事件也会阻止事件冒泡免得产生副作用。
Link还裸露了forwardedRef属性,能够转发ref。
import React from "react";import { __RouterContext as RouterContext } from "react-router";import PropTypes from "prop-types";import invariant from "tiny-invariant";import { resolveToLocation, normalizeToLocation} from "./utils/locationUtils.js";// React 15 compatconst 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; } 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} />; });if (__DEV__) { LinkAnchor.displayName = "LinkAnchor";}/** * The public API for rendering a history-aware <a>. */const Link = forwardRef( ( { component = LinkAnchor, replace, to, innerRef, // TODO: deprecate ...rest }, forwardedRef ) => { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Link> outside a <Router>"); const { history } = context; const location = normalizeToLocation( resolveToLocation(to, context.location), context.location ); const href = location ? history.createHref(location) : ""; const props = { ...rest, href, navigate() { 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;
NavLink
NavLink是基于Link的,它次要性能是能够自定义设置一些activeStyle、className,从而扭转Link的款式。
import React from "react";import { __RouterContext as RouterContext, matchPath } from "react-router";import PropTypes from "prop-types";import invariant from "tiny-invariant";import Link from "./Link.js";import { resolveToLocation, normalizeToLocation} from "./utils/locationUtils.js";// React 15 compatconst forwardRefShim = C => C;let { forwardRef } = React;if (typeof forwardRef === "undefined") { forwardRef = forwardRefShim;}function joinClassnames(...classnames) { return classnames.filter(i => i).join(" ");}/** * A <Link> wrapper that knows if it's "active" or not. */const NavLink = forwardRef( ( { "aria-current": ariaCurrent = "page", activeClassName = "active", activeStyle, className: classNameProp, exact, isActive: isActiveProp, location: locationProp, sensitive, strict, style: styleProp, to, innerRef, // TODO: deprecate ...rest }, forwardedRef ) => { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <NavLink> outside a <Router>"); const currentLocation = locationProp || context.location; const toLocation = normalizeToLocation( resolveToLocation(to, currentLocation), currentLocation ); const { pathname: path } = toLocation; // Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202 const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); const match = escapedPath ? matchPath(currentLocation.pathname, { path: escapedPath, exact, sensitive, strict }) : null; const isActive = !!(isActiveProp ? isActiveProp(match, currentLocation) : match); const className = isActive ? joinClassnames(classNameProp, activeClassName) : classNameProp; const style = isActive ? { ...styleProp, ...activeStyle } : styleProp; const props = { "aria-current": (isActive && ariaCurrent) || null, className, style, to: toLocation, ...rest }; // React 15 compat if (forwardRefShim !== forwardRef) { props.ref = forwardedRef || innerRef; } else { props.innerRef = innerRef; } return <Link {...props} />; }} </RouterContext.Consumer> ); });export default NavLink;
react-router-config
react-router-config是为了不便咱们应用相似上面的配置来编写react-router
const routes = [ { component: Root, routes: [ { path: "/", exact: true, component: Home }, { path: "/child/:id", component: Child, routes: [ { path: "/child/:id/grand-child", component: GrandChild } ] } ] }];
它只有两个api,matchRoutes和renderRoutes。
matchRoutes
import { matchPath, Router } from "react-router";function matchRoutes(routes, pathname, /*not public API*/ branch = []) { routes.some(route => { const match = route.path ? matchPath(pathname, route) : branch.length ? branch[branch.length - 1].match // use parent match : Router.computeRootMatch(pathname); // use default "root" match if (match) { branch.push({ route, match }); if (route.routes) { matchRoutes(route.routes, pathname, branch); } } return match; }); return branch;}export default matchRoutes;
renderRoutes
renderRoutes在组件中应用,能够依据后面的路由配置渲染相应的组件。
import React from "react";import { Switch, Route } from "react-router";function renderRoutes(routes, extraProps = {}, switchProps = {}) { return routes ? ( <Switch {...switchProps}> {routes.map((route, i) => ( <Route key={route.key || i} path={route.path} exact={route.exact} strict={route.strict} render={props => route.render ? ( route.render({ ...props, ...extraProps, route: route }) ) : ( <route.component {...props} {...extraProps} route={route} /> ) } /> ))} </Switch> ) : null;}export default renderRoutes;
应用示例如下:
import { renderRoutes } from "react-router-config"; const routes = [ { component: Root, routes: [ { path: "/", exact: true, component: Home }, { path: "/child/:id", component: Child, routes: [ { path: "/child/:id/grand-child", component: GrandChild } ] } ] }]; const Root = ({ route }) => ( <div> <h1>Root</h1> {/* child routes won't render without this */} {renderRoutes(route.routes)} </div>); const Home = ({ route }) => ( <div> <h2>Home</h2> </div>); const Child = ({ route }) => ( <div> <h2>Child</h2> {/* child routes won't render without this */} {renderRoutes(route.routes, { someProp: "these extra props are optional" })} </div>); const GrandChild = ({ someProp }) => ( <div> <h3>Grand Child</h3> <div>{someProp}</div> </div>); ReactDOM.render( <BrowserRouter> {/* kick it all off with the root route */} {renderRoutes(routes)} </BrowserRouter>, document.getElementById("root"));
react-router-native
react-router里最初一个包是react-router-native,因为没有做过相干业务,就没有钻研了。