react-router 学习笔记
前端路由
路由这个概念最早出现在后端,通过⽤户请求的 url 导航到具体的 html⻚⾯。
现在的前端路由不同 于传统路由,它不需要服务器解析,⽽是可以通过 hash 函数或者 history API 来实现。
在前端开发中,可以使⽤路由设置访问路径,并根据路径与组件的映射关系切换组件的显示
这整个过程都是在同 ⼀个⻚⾯中实现的,不涉及⻚⾯间的跳转,这也就是常说的单⻚应⽤(spa)。
1、查看源码姿势
1.1 代码仓库
https://github.com/ReactTraining/react-router
2.2 包说明
react-router 公共基础包
react-router-dom 在浏览器中使⽤,依赖 react-router
react-router-native 在 react-native 中使用,依赖 react-router
2.3 源码位置
react-router 源码位置
react-router/packages/react-router/modules/react-router-dom 源码位置
react-router/packages/react-router-dom/modules/
2.4 文件结构
Hooks暂不关注<Link>
- <NavLink>
<Prompt>暂不关注<Redirect><Route><Router>
- <HashRouter>
- <BrowserRouter>
- <MemoryRouter> 暂不关注
- <StaticRouter> 暂不关注
<Switch>generatePath
暂不关注history
location
暂不关注match
暂不关注matchPath
暂不关注withRouter
3、React-Router 源码分析
3.1 四种 Router 源码对比
<!--HashRouter-->//便于梳理代码结构,已删除部分代码import React from "react";import { Router } from "react-router";import { createHashHistory as createHistory } from "history";/** * 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;
<!--BrowserRouter-->//便于梳理代码结构,已删除部分代码import React from "react";import { Router } from "react-router";import { createBrowserHistory as createHistory } from "history";/** * 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;
<!--MemoryRouter-->//便于梳理代码结构,已删除部分代码import React from "react";import { createMemoryHistory as createHistory } from "history";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;
<!--StaticRouter-->//便于梳理代码结构,已删除部分代码import React from "react";import { createLocation, createPath } from "history";import Router from "./Router.js";/** * 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 { 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;
都是基于<Router>组件实现,区别就是传递不同的参数。重点关注 <HashRouter> 和 <BrowserRouter>
3.2 <Router> 源码分析
<!--Router-->//便于梳理代码结构,已删除部分代码class Router extends React.Component { render() { return ( <RouterContext.Provider value={{ history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), //只有StaticRouter在使用,暂不考虑 staticContext: this.props.staticContext }} > {/*HistoryContext在hooks中使用,暂不考虑*/} <HistoryContext.Provider children={this.props.children || null} value={this.props.history} /> </RouterContext.Provider> ); }}通过源码可以看出 Router 的核心功能就是提供以下数据
history,父组件传入,由 history 库生成
location,组件内计算生成
match,组件内静态方法计算生成
3.3 <Route> 源码分析
<!--Route-->//便于梳理代码结构,已删除部分代码class Route extends React.Component { render() { return ( <RouterContext.Consumer> {context => { 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> ); }}由于三项表达式嵌套不便于阅读,代码可以转换成
/* * 1、检测是否 match * 1.1 不匹配,children 为函数则返回 children(props),否则返回 null * 1.2 匹配,进行第2步 * * 2、检查 children * 2.1 存在, children 为函数则返回 children(props),否则返回 children * 2.2 不存在,进行第3步 * * 3、检查component * 3.1 存在,返回React.createElement(component, props) * 3.2 不在存,进行第4步 * * 4、检查render * 4.1 存在,返回 render(props) * 4.2 不存在,返回 null * */getComponent(props, children, component, render) { if (props.match) { if (children) { if (typeof children === "function") { return children(props); } else { return children; } } else { if (component) { return React.createElement(component, props); } else { if (render) { return render(props); } else { return null; } } } } else { if (typeof children === "function") { return children(props); } else { return null; } }}通过源码可以看出 Reoute 的核心功能是渲染组件,并有以下特点
三者优先级 children > component > render
children 为函数可以做到匹配与否都显示
children 为组件只能在匹配显示
component 只能为组件
render 只能为函数
3.4 <Redirect> 源码分析
<!--Switch-->//便于梳理代码结构,已删除部分代码function Redirect({ computedMatch, to, push = false }) { const method = push ? history.push : history.replace; return ( <RouterContext.Consumer> {context => { return ( <Lifecycle onMount={() => { method(location); }} onUpdate={(self, prevProps) => { method(location); }} to={to} /> ); }} </RouterContext.Consumer> );}通过源码可以看出 Redirect 的核心功能是跳转到 to 指向的页面。
3.5 <Switch> 源码分析
<!--Switch-->//便于梳理代码结构,已删除部分代码class Switch extends React.Component { render() { return ( <RouterContext.Consumer> {context => { const location = this.props.location || context.location; let element, match; // 匹配第一个Route 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; } }); //匹配则返回React生成的元素,否则返回null return match ? React.cloneElement(element, { location, computedMatch: match }) : null; }} </RouterContext.Consumer> ); }}通过源码可以看出 Switch 的核心功能是渲染匹配到第一个 Route 组件。
3.6 withRouter 源码分析
<!--withRouter-->//便于梳理代码结构,已删除部分代码function withRouter(Component) { const C = props => { const { wrappedComponentRef, ...remainingProps } = props; return ( //通过 Context.Consumer 传递 Router 属性给 Component,达到增强目的 <RouterContext.Consumer> {context => { return ( <Component {...remainingProps} {...context} ref={wrappedComponentRef} /> ); }} </RouterContext.Consumer> ); }; //静态属性拷贝 return hoistStatics(C, Component);}通过源码可以看出 withRouter 的核心功能是 传递 Router 属性给 Component。
3.7 <Link> 源码分析
<!--Link-->//便于梳理代码结构,已删除部分代码const Link = forwardRef( ( { component = LinkAnchor, replace, to, innerRef, // TODO: deprecate ...rest }, forwardedRef ) => { return ( <RouterContext.Consumer> {context => { const { history } = context;
//把 to 属性转换成 href 属性 const location = normalizeToLocation( resolveToLocation(to, context.location), context.location ); const href = location ? history.createHref(location) : ""; //组装 props 数据 const props = { ...rest, href, navigate() { const location = resolveToLocation(to, context.location); const method = replace ? history.replace : history.push;
method(location); } }; // 设置forwardedRef props.ref = forwardedRef //创建并返回组件 return React.createElement(component, props); }} </RouterContext.Consumer> ); });<!--LinkAnchor-->//便于梳理代码结构,已删除部分代码const LinkAnchor = forwardRef( ( { innerRef, // TODO: deprecate navigate, onClick, ...rest }, forwardedRef ) => { const { target } = rest;
//组装 props 数据 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(); } } };
// 设置forwardedRef props.ref = forwardedRef;
/* eslint-disable-next-line jsx-a11y/anchor-has-content */ return <a {...props} />; });通过源码可以看出 Link 的核心功能如下
通过 component 自定义 <Link>,否则默认使用 <LinkAnchor>
<Link> 的主要功能就是把 to 属性转换成 href 属性
<LinkAnchor> 主要功能是屏蔽 <a> 默认点击事件,使用 history 进行路由跳转
3.8 <NavLink> 源码分析
<!--NavLink-->//便于梳理代码结构,已删除部分代码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 => { //根据 isActive 属性判断是否 Active const isActive = !!(isActiveProp ? isActiveProp(match, currentLocation) : match); //根据 isActive 属性设置 className const className = isActive ? joinClassnames(classNameProp, activeClassName) : classNameProp; //根据 isActive 属性设置内联样式 style const style = isActive ? { ...styleProp, ...activeStyle } : styleProp; //组织 props 数据 const props = { "aria-current": (isActive && ariaCurrent) || null, className, style, to: toLocation, ...rest }; // 设置forwardedRef props.ref = forwardedRef; return <Link {...props} />; }} </RouterContext.Consumer> ); });通过源码可以看出 <NavLink> 的核心功能如下
在 <Link> 基础上允许自定义默认和激活状态样式
isActive 优先级高于默认的路由匹配
4、history 源码分析
4.1 createBrowserHistory
<!--createBrowserHistory-->//便于梳理代码结构,已删除部分代码export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory { //获取 history let { window = document.defaultView! } = options; let globalHistory = window.history;
window.addEventListener(PopStateEventType, handlePop);
let action = Action.Pop; let [index, location] = getIndexAndLocation(); let listeners = createEvents<Listener>();
//pathname + search + hash function createHref(to: To) { return typeof to === 'string' ? to : createPath(to); }
//用来处理事件订阅 function applyTx(nextAction: Action) { action = nextAction; [index, location] = getIndexAndLocation(); listeners.call({ action, location }); }
//基于 pushState 方法实现 function push(to: To, state?: State) { let nextAction = Action.Push; let nextLocation = getNextLocation(to, state); function retry() { push(to, state); } if (allowTx(nextAction, nextLocation, retry)) { let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading // try...catch because iOS limits us to 100 pushState calls :/ try { globalHistory.pushState(historyState, '', url); } catch (error) { // They are going to lose state here, but there is no real // way to warn them about it since the page will refresh... window.location.assign(url); } //用来处理事件订阅 applyTx(nextAction); } }
//基于replaceState方法实现 function replace(to: To, state?: State) { let nextAction = Action.Replace; let nextLocation = getNextLocation(to, state); function retry() { replace(to, state); }
if (allowTx(nextAction, nextLocation, retry)) { let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
// TODO: Support forced reloading globalHistory.replaceState(historyState, '', url);
//用来处理事件订阅 applyTx(nextAction); } }
//基于go方法实现 function go(delta: number) { globalHistory.go(delta); }
let history: BrowserHistory = { get action() { return action; }, get location() { return location; }, createHref, push, replace, go, back() { go(-1); }, forward() { go(1); }, listen(listener) { return listeners.push(listener); }, block(blocker) {...} };
return history;}back、forward 都是基于 go 方法实现
push 基于 pushState 方法实现
replace 基于 replaceState 方法实现
浏览器相关(url 输入、前进、后退)事件通过监听 popstate 事件处理,然后通过事件订阅的形式传递给 Route 组件
4.2
createHashHistory 同 createBrowserHistory
createMemoryHistory 同 createBrowserHistory
参考链接
hao-kuai
还未添加个人签名 2018.10.22 加入
还未添加个人简介











评论