深入浅出react-router-dom v5源码

78 阅读20分钟

React Router

React Router 概述

React 路由

站点

无论是使用 Vue,还是 React,开发的单页应用程序,可能只是该站点的一部分(某一个功能块)

一个单页应用里,可能会划分为多个页面(几乎完全不同的页面效果)(组件)

如果要在单页应用中完成组件的切换,需要实现下面两个功能:

  1. 根据不同的页面地址,展示不同的组件(核心)
  2. 完成无刷新的地址切换 我们把实现了以上两个功能的插件,称之为路由

React Router

  1. react-router:路由核心库,包含诸多和路由功能相关的核心代码
  2. react-router-dom:利用路由核心库,结合实际的页面,实现跟页面路由密切相关的功能 如果是在页面中实现路由,需要安装 react-router-dom 库

两种模式

路由:根据不同的页面地址,展示不同的组件

url 地址组成

例:www.react.com:443/news/1-2-1.…

协议名(schema):https 主机名(host):www.react.com ip 地址 预设值:localhost 域名 局域网中电脑名称 端口号(port):443 如果协议是 http,端口号是 80,则可以省略端口号 如果协议是 https,端口号是 443,则可以省略端口号 路径(path):/news/1-2-1.html 地址参数(search、query):?a=1&b=2 附带的数据 格式:属性名=属性值&属性名=属性值.... 哈希(hash、锚点) 附带的数据 Hash Router 哈希路由 根据 url 地址中的哈希值来确定显示的组件

原因:hash 的变化,不会导致页面刷新 这种模式的兼容性最好

Borswer History Router 浏览器历史记录路由 HTML5 出现后,新增了 History Api,从此以后,浏览器拥有了改变路径而不刷新页面的方式

History 表示浏览器的历史记录,它使用栈的方式存储。

history.length:获取栈中数据量 history.pushState:向当前历史记录栈中加入一条新的记录 参数 1:附加的数据,自定义的数据,可以是任何类型 参数 2:页面标题,目前大部分浏览器不支持 参数 3:新的地址 history.replaceState:将当前指针指向的历史记录,替换为某个记录 参数 1:附加的数据,自定义的数据,可以是任何类型 参数 2:页面标题,目前大部分浏览器不支持 参数 3:新的地址 根据页面的路径决定渲染哪个组件

路由组件

React-Router 为我们提供了两个重要组件

Router 组件 它本身不做任何展示,仅提供路由模式配置,另外,该组件会产生一个上下文,上下文中会提供一些实用的对象和方法,供其他相关组件使用

HashRouter:该组件,使用 hash 模式匹配 BrowserRouter:该组件,使用 BrowserHistory 模式匹配 通常情况下,Router 组件只有一个,将该组件包裹整个页面

Route 组件 根据不同的地址,展示不同的组件

重要属性:

  1. path:匹配的路径 默认情况下,不区分大小写,可以设置 sensitive 属性为 true,来区分大小写 默认情况下,只匹配初始目录,如果要精确匹配,配置 exact 属性为 true 如果不写 path,则会匹配任意路径
  2. component:匹配成功后要显示的组件
  3. children:

所以是后面的会覆盖前面的

//这种写法无论是否匹配到path,多会运行children函数,渲染<h1 style={{ color: 'red' }}>必定会显示的内容</h1>
<Route
  path="/ab"
  exact
  component={Login}
  children={() => {
    return <h1 style={{ color: 'red' }}>必定会显示的内容</h1>;
  }}
></Route>

// 下面这种写法只有匹配到path,才会渲染 <h1 style={{ color: 'red' }}>必定会显示的内容</h1>
 <Route path="/ab" exact component={Login}>
      <h1 style={{ color: 'red' }}>children不写函数只有匹配到才能显示的内容</h1>
</Route>

// 加了Switch组件包裹之后,只有在匹配的时候才会执行children函数
 <Router>
      <Switch>
        <Route
          path="/a"
          exact
          component={A}
          children={() => (
            <>
              <h1 style={{ color: 'red' }}>children写函数必定会看到的内容</h1>
              <p>adfasdfasdf</p>
            </>
          )}
        ></Route>
        <Route path="/a/b" component={B} />
        <Route component={C} />
      </Switch>
 </Router>
import React from "react"
import { BrowserRouter as Router, Route } from "react-router-dom"
//   /a
function A() {
  return <h1>组件A</h1>
}
//   /a/b
function B() {
  return <h1>组件B</h1>
}
// 任意路径
function C() {
  return (
    <h1>
      找不到页面
      <Route path="/abc" exact component={D} />
    </h1>
  )
}
function D() {
  return <span>D组件</span>
}
export default function App() {
  return (
    <Router>
      <Route path="/a" exact component={A}>
        <h1 style={{ color: "red" }}>匹配会看到的内容</h1>
        <p>adfasdfasdf</p>
      </Route>
      <Route path="/a/b" component={B} />
      <Route component={C} />
    </Router>
  )
}

Switch 组件 写到 Switch 组件中的 Route 组件,当匹配到第一个 Route 后,会立即停止匹配

由于 Switch 组件会循环所有子元素,然后让每个子元素去完成匹配,若匹配到,则渲染对应的组件,然后停止循环。因此,不能在 Switch 的子元素中使用除 Route 外的其他组件。

demo 后台管理模板

import React from "react"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"
import Login from "./pages/Login"
import Admin from "./pages/Admin"
export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/login" exact component={Login} />
        <Route path="/" component={Admin} />
      </Switch>
    </Router>
  )
}

路由信息

Router 组件会创建一个上下文,并且,向上下文中注入一些信息

该上下文对开发者是隐藏的,Route 组件若匹配到了地址,则会将这些上下文中的信息作为属性传入对应的组件

{history: {…}, location: {…}, match: {…},... }

history

它并不是 window.history 对象,我们利用该对象无刷新跳转地址

为什么没有直接使用 history 对象

  1. React-Router 中有两种模式:Hash、History,如果直接使用 window.history,只能支持一种模式
//两种模式
import { BrowserRouter as Router } from "react-router-dom"

import { HashRouter as Router } from "react-router-dom"
  1. 当使用 windows.history.pushState 方法时,没有办法收到任何通知,将导致 React 无法知晓地址发生了变化,结果导致无法重新渲染组件(React 需要监听上下文的变化来重新调用渲染组件) push:将某个新的地址入栈(历史记录栈) 参数 1:新的地址 参数 2:可选,附带的状态数据 replace:将某个新的地址替换掉当前栈中的地址 go: 与 window.history 一致 forward: 与 window.history 一致 back: 与 window.history 一致
import React from "react"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"

function A(props) {
  return (
    <div>
      <p>组件A</p>
      <button
        onClick={() => {
          props.history.replace("/b", "状态数据")
        }}>
        跳转到/b
      </button>
    </div>
  )
}

function B(props) {
  return (
    <div>
      <p>组件B</p>
      <p>获取状态数据:{props.history.location.state}</p>
      <button
        onClick={() => {
          props.history.replace("/a")
        }}>
        跳转到/a
      </button>
    </div>
  )
}

function NotFound() {
  return <h1>找不到页面</h1>
}
export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/a" component={A} />
        <Route path="/b" component={B} />
        <Route component={NotFound} />
      </Switch>
    </Router>
  )
}

location

与 history.location 完全一致,是同一个对象,但是,与 window.location 不同

location 对象中记录了当前地址的相关信息

http://localhost:3000/a?s=123&q=234#zhangju console.log(props.location) {pathname: "/a", search: "?s=123&q=234", hash: "#zhangju", state: undefined}

我们通常使用第三方库 query-string,用于解析地址栏中的数据

www.npmjs.com/package/que…

import React from "react"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"
import qs from "query-string"

function A(props) {
  console.log(props.location)
  var query = qs.parse(props.location.search)
  var hash = qs.parse(props.location.hash)
  return (
    <div>
      <p>组件A</p>
      <p>访问地址:{props.location.pathname}</p>
      <p>
        地址参数:a:{query.a}, b:{query.b}, c:{query.c}
      </p>
      <p>
        hash: d:{hash.d}, f:{hash.f}
      </p>
    </div>
  )
}

function NotFound() {
  return <h1>找不到页面</h1>
}
export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/a" exact component={A} />
        <Route component={NotFound} />
      </Switch>
    </Router>
  )
}

match

该对象中保存了,路由匹配的相关信息

isExact:事实上,当前的路径和路由配置的路径是否是精确匹配的 params:获取路径规则中对应的数据 实际上,在书写 Route 组件的 path 属性时,可以书写一个 string pattern(字符串正则)


<Router>
      <Switch>
        <Route path="/a/:year/:month/:day" exact component={A} />
        <Route component={NotFound} />
      </Switch>
</Router>
// 地址栏访问
//http://localhost:3000/a/2019/8/19

//console.log(matchs);
{path: "/a/:year/:month/:day", url: "/a/2019/8/19", isExact: true, params: {year: "2019", month: "8", day: "19"}}

// path表示匹配上的规则路径
// url表示匹配上的路径
// params表示接受的参数
// isExact表示是否是精确匹配(假设我们在浏览器上输入的url是/a 此时Route组件上path也是/a 那么此时isExact是true,否则是false)

react-router 使用了第三方库:Path-to-RegExp,该库的作用是,将一个字符串正则转换成一个真正的正则表达式(react-router-dom 包中依赖这个库)。

加?表示改参数可传可不传

import React from "react"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"

function News(props) {
  console.log(props.match) //353行
  return (
    <div>
      <p>
        显示{props.match.params.year}年{props.match.params.month}月
        {props.match.params.day}日的新闻
      </p>
    </div>
  )
}

function NotFound() {
  return <h1>找不到页面</h1>
}
export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/news/:year?/:month?/:day?" component={News} />
        <Route component={NotFound} />
      </Switch>
    </Router>
  )
}

(\d+)表示后面是数字

import React from "react"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"

function News(props) {
  console.log(props.match)
  return (
    <div>
      <p>
        显示{props.match.params.year}年{props.match.params.month}月
        {props.match.params.day}日的新闻
      </p>
    </div>
  )
}

function NotFound() {
  return <h1>找不到页面</h1>
}
export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/news/:year(\d+)/:month(\d+)/:day(\d+)" component={News} />
        <Route component={NotFound} />
      </Switch>
    </Router>
  )
}

*表示后面还有东西

import React from "react"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"

function News(props) {
  console.log(props.match)
  return (
    <div>
      <p>
        显示{props.match.params.year}年{props.match.params.month}月
        {props.match.params.day}日的新闻
      </p>
    </div>
  )
}

function NotFound() {
  return <h1>找不到页面</h1>
}
export default function App() {
  return (
    <Router>
      <Switch>
        <Route
          //exact要求 2021/7/24/xxx  xxx是必须要的
          path="/news/:year(\d+)/:month(\d+)/:day(\d+)/*"
          exact
          component={News}
        />
        <Route component={NotFound} />
      </Switch>
    </Router>
  )
}

path 是数组可以匹配多个路径

<Route path={["/news", "/news/:year/:month/:day", "/n"]} exact component={A} />

向某个页面传递数据的方式:

  1. 使用 state:在 push 页面时,加入 state(直接访问 push 的页面是没有数据的)
  2. 利用 search:把数据填写到地址栏中的?后
  3. 利用 hash:把数据填写到 hash 后
  4. params:把数据填写到路径中

总结 history(action,block,createHref,go,goBack,goForward,length,listen,location,push,replace) location(hash,key,pathname,search,state) match(isExact,params,path,url)

非路由组件获取路由信息

某些组件,并没有直接放到 Route 中,而是嵌套在其他普通组件中,因此,它的 props 中没有路由信息,如果这些组件需要获取到路由信息,可以使用下面两种方式:

  1. 将路由信息从父组件一层一层传递到子组件
  2. 使用 react-router 提供的高阶组件 withRouter,包装要使用的组件,该高阶组件会返回一个新组件,新组件将向提供的组件注入路由信息。(我们拿不到 Router 的上下文信息,只能用它提供的 API 来获取上下文)
import React from "react"
import {
  BrowserRouter as Router,
  Route,
  Switch,
  withRouter,
} from "react-router-dom"

const AWrapper = withRouter(A)

// function withRouter(Comp) {
//     return function routerWrapper(props) {
//         //获取上下文中的信息
//         return <Comp {...props} history={上下文中的history} />
//     }
// }

function News(props) {
  return (
    <div>
      <h1>新闻列表</h1>
      <AWrapper />
    </div>
  )
}

function A(props) {
  console.log(props)
  return (
    <button
      onClick={() => {
        props.history.push("/")
      }}>
      点击返回
    </button>
  )
}

function Index() {
  return <h1>首页</h1>
}

function NotFound() {
  return <h1>找不到页面</h1>
}
export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/news" component={News} />
        <Route path="/" exact component={Index} />
        <Route component={NotFound} />
      </Switch>
    </Router>
  )
}

其他组件

已学习:

Router:BrowswerRouter、HashRouter Route Switch 高阶函数:withRouter

Link

//Link.js
/* eslint {"jsx-a11y/anchor-is-valid":"off", "no-script-url":"off"} */
import React from "react"
import { withRouter } from "react-router-dom"
function Link(props) {
  return (
    <a
      href={props.to}
      onClick={(e) => {
        // 这里使用React的事件对象也能阻止默认事件
        e.preventDefault() //阻止a标签的默认跳转刷新功能
        // e.nativeEvent拿到的原生事件对象,用原生事件对象阻止默认事件
        //  e.nativeEvent.preventDefault(); //阻止a标签的默认跳转刷新功能
        props.history.push(props.to)
      }}>
      {props.children}
    </a>
  )
}
export default withRouter(Link)

生成一个无刷新跳转的 a 元素

  • to
    • 字符串:跳转的目标地址
    • 对象:
      • pathname:url 路径
      • search
      • hash
      • state:附加的状态信息
  • replace:bool,表示是否是替换当前地址,默认是 false
  • innerRef:可以将内部的 a 元素的 ref 附着在传递的对象或函数参数上
    • 函数
    • ref 对象
import React from "react"
import { BrowserRouter as Router, Route, Link } from "react-router-dom"
// import Link from "./Link"
function PageA() {
  return <h1>A页</h1>
}
function PageB() {
  return <h1>B页</h1>
}
function NavBar() {
  return (
    <div>
      <Link
        innerRef={(node) => {
          console.log(node) //<a href="/a" style="margin-right: 20px;">去a页</a>
        }}
        to="/a"
        style={{ marginRight: 20 }}>
        去a页
      </Link>
      <Link
        replace
        to={{
          pathname: "/b",
          hash: "#abc",
          search: "?a=1&b=2",
        }}>
        去b页
      </Link>
    </div>
  )
}
export default function App() {
  return (
    <Router>
      <NavBar />
      <Route path="/a" component={PageA} />
      <Route path="/b" component={PageB} />
    </Router>
  )
}

NavLink

是一种特殊的 Link,Link 组件具备的功能,它都有

它具备的额外功能是:根据当前地址和链接地址,来决定该链接的样式

  • activeClassName: 匹配时使用的类名(默认匹配添加 class='active')
  • activeStyle: 匹配时使用的内联样式
  • exact: 是否精确匹配
  • sensitive:匹配时是否区分大小写
  • strict:是否严格匹配最后一个斜杠
import React from "react"
import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"
import "./App.css"
// import Link from "./Link"
function PageA() {
  return <h1>A页</h1>
}
function PageB() {
  return <h1>B页</h1>
}
function NavBar() {
  return (
    <div>
      <NavLink
        activeClassName="selected"
        exact
        strict
        activeStyle={{
          background: "#ccc",
        }}
        innerRef={(node) => {
          console.log(node)
        }}
        to="/a"
        style={{ marginRight: 20 }}>
        去a页
      </NavLink>
      <NavLink
        activeClassName="selected"
        activeStyle={{
          background: "#ccc",
        }}
        replace
        to={{
          pathname: "/b",
          hash: "#abc",
          search: "?a=1&b=2",
        }}>
        去b页
      </NavLink>
    </div>
  )
}
export default function App() {
  return (
    <Router>
      <NavBar />
      <Route path="/a" component={PageA} />
      <Route path="/b" component={PageB} />
    </Router>
  )
}

Redirect

重定向组件,当加载到该组件时,会自动跳转(无刷新)到另外一个地址

  • to:跳转的地址 字符串 对象
  • push: 默认为 false,表示跳转使用替换的方式,设置为 true 后,则使用 push 的方式跳转
  • from:当匹配到 from 地址规则时才进行跳转(就相当于是 Route 的 path 属性)
  • exact: 是否精确匹配 from
  • sensitive:from 匹配时是否区分大小写
  • strict:from 是否严格匹配最后一个斜杠

只要在地址栏上匹配上/abc 才会进行跳转到/a <Redirect from="/abc" to="/a" /> 当在地址栏上输入地址为/abc/123 才会跳转到/a/123 <Redirect from="/abc/:id" to="/a/:id" />

import React from "react"
import {
  BrowserRouter as Router,
  Route,
  NavLink,
  Switch,
  Redirect,
} from "react-router-dom"
import "./App.css"
// import Link from "./Link"

function PageA() {
  return <h1>A页</h1>
}

function PageB() {
  return <h1>B页</h1>
}

function NavBar() {
  return (
    <div>
      <NavLink
        activeClassName="selected"
        exact
        strict
        activeStyle={{
          background: "#ccc",
        }}
        innerRef={(node) => {
          console.log(node)
        }}
        to="/a"
        style={{ marginRight: 20 }}>
        去a页
      </NavLink>
      <NavLink
        activeClassName="selected"
        activeStyle={{
          background: "#ccc",
        }}
        replace
        to={{
          pathname: "/b",
          hash: "#abc",
          search: "?a=1&b=2",
        }}>
        去b页
      </NavLink>

      <NavLink to="/abc" style={{ marginLeft: 20 }}>
        其他页
      </NavLink>
    </div>
  )
}

export default function App() {
  return (
    <Router>
      <NavBar />
      <Switch>
        <Route path="/a" component={PageA} />
        <Route path="/b" component={PageB} />
        <Redirect from="/abc/:id" to="/a/:id" />
      </Switch>
    </Router>
  )
}

嵌套的路由

第一种使用 match

import React from "react"
import { BrowserRouter as Router, Route, Link } from "react-router-dom"

function User({ match }) {
  //match可以获取到Route上下文中匹配的url信息
  return (
    <div>
      <h1>User组件固定的区域</h1>
      <p>
        <Link to={`${match.url}/update`}>用户信息</Link>
        <Link to={`${match.url}/pay`}>充值</Link>
      </p>
      <div
        style={{
          width: 500,
          height: 500,
          background: "lightblue",
          border: "2px solid",
          margin: "0 auto",
        }}>
        {/* User组件变化的区域:根据地址的不同发生变化 */}
        <Route path={`${match.url}/update`} component={UserUpdate} />
        <Route path={`${match.url}/pay`} component={UserPay} />
      </div>
    </div>
  )
}

function UserUpdate() {
  return <h1>修改用户信息</h1>
}

function UserPay() {
  return <h1>用户充值</h1>
}

export default function App() {
  return (
    <Router>
      <Route path="/u" component={User} />
      {/* 其他组件 */}
    </Router>
  )
}

第二种抽离路由

const config = {
  user: {
    root: "/user",
    update: "/update",
    pay: {
      root: "/pay",
      afterPay: "/after",
      before: "/before",
    },
  },
}

function setConfig() {
  _setConfig(config, "")
}

/**
 * 将该对象的每一个字符串属性,前面添加baseStr
 * 如果属性名为root,则直接添加baseStr
 * 如果属性名不是root,则添加baseStr/root属性值
 * 如果属性不是字符串,递归调用该方法
 * @param {*} obj
 * @param {*} baseStr
 */
function _setConfig(obj, baseStr) {
  const newBaseUrl = baseStr + (obj.root === undefined ? "" : obj.root)
  for (let prop in obj) {
    const value = obj[prop]
    if (typeof value === "string") {
      if (prop === "root") {
        obj[prop] = baseStr + value
      } else {
        obj[prop] = newBaseUrl + value
      }
    } else {
      _setConfig(obj[prop], newBaseUrl)
    }
  }
}

setConfig()

export default config
import React from "react"
import { BrowserRouter as Router, Route, Link } from "react-router-dom"
import routeConfig from "./RouteConfig"

function User({ match }) {
  return (
    <div>
      <h1>User组件固定的区域</h1>
      <p>
        <Link to={routeConfig.user.update}>用户信息</Link>
        <Link to={routeConfig.user.pay.root}>充值</Link>
      </p>
      <div
        style={{
          width: 500,
          height: 500,
          background: "lightblue",
          border: "2px solid",
          margin: "0 auto",
        }}>
        {/* User组件变化的区域:根据地址的不同发生变化 */}
        <Route path={routeConfig.user.update} component={UserUpdate} />
        <Route path={routeConfig.user.pay.root} component={UserPay} />
      </div>
    </div>
  )
}

function UserUpdate() {
  return <h1>修改用户信息</h1>
}

function UserPay() {
  return <h1>用户充值</h1>
}

export default function App() {
  return (
    <Router>
      <Route path={routeConfig.user.root} component={User} />
      {/* 其他组件 */}
    </Router>
  )
}

受保护的页面

// loginInfo.js  用一个对象存登录信息
export default {
  isLogin: false,
  login() {
    this.isLogin = true
  },
  loginOut() {
    this.isLogin = false
  },
}
// App.js
import React from "react"
import { HashRouter as Router, Route, Switch, Link } from "react-router-dom"
import Home from "./Home"
import Personal from "./Personal"
import Login from "./Login"
import ProtectedRoute from "./ProtectedRoute"
export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">首页</Link>
          </li>
          <li>
            <Link to="/login">登录页</Link>
          </li>
          <li>
            <Link to="/personal">个人中心</Link>
          </li>
        </ul>
        <div>
          <Switch>
            <Route path="/login" component={Login} />
            <ProtectedRoute path="/personal" component={Personal} />
            {/* render和children的区别:render是匹配后才会运行,children无论是否匹配都会运行 */}
            {/* <Route path="/personal" render={ values => {
                            console.log(values)
                            return <h1>asdfdasdfa</h1>
                        }} /> */}
            <Route path="/" component={Home} />
          </Switch>
        </div>
      </div>
    </Router>
  )
}
//封装ProtectedRoute.js
import React from "react"
import { Route, Redirect } from "react-router-dom"
import loginInfo from "./loginInfo"
export default function ProtectedRoute({
  component: Component,
  children,
  render,
  ...rest
}) {
  return (
    <Route
      {...rest}
      render={(values) => {
        if (loginInfo.isLogin) {
          //可以正常展示页面
          return <Component />
        } else {
          // return <Redirect to={{
          //     pathname: "/login",
          //     search: "?returnurl=" + values.location.pathname
          // }} />
          return (
            <Redirect
              to={{
                pathname: "/login",
                state: values.location.pathname,
              }}
            />
          )
        }
      }}
    />
  )
}

代码全

import React from "react"
import {
  HashRouter as Router,
  Route,
  Switch,
  Link,
  Redirect,
} from "react-router-dom"
let loginInfo = {
  isLogin: false,
  login() {
    this.isLogin = true
  },
  loginOut() {
    this.isLogin = false
  },
}
function Home() {
  return (
    <div>
      <h1>首页</h1>
    </div>
  )
}

function Personal() {
  return (
    <div>
      <h1>个人中心</h1>
      <p>如果你看到该页面,说明你已经完成了登录</p>
    </div>
  )
}
function Login(props) {
  return (
    <div>
      <h1>登录授权页</h1>
      <p>该页面仅作测试,点击下方按钮即登录成功</p>
      <button
        onClick={() => {
          loginInfo.login()
          if (props.location.state) {
            props.history.push(props.location.state)
          } else {
            props.history.push("/")
          }
          // const query = qs.parse(props.location.search);
          // if (query.returnurl) {
          //     props.history.push(query.returnurl);
          // }
          // else {
          //     props.history.push("/");
          // }
        }}>
        登录
      </button>
    </div>
  )
}

function ProtectedRoute({ component: Component, children, render, ...rest }) {
  return (
    <Route
      {...rest}
      render={(values) => {
        // console.log(values);//{history: {…}, location: {…}, match: {…}, staticContext: undefined}
        if (loginInfo.isLogin) {
          //可以正常展示页面
          return <Component />
        } else {
          // return <Redirect to={{
          //     pathname: "/login",
          //     search: "?returnurl=" + values.location.pathname
          // }} />

          return (
            <Redirect
              to={{
                pathname: "/login",
                state: values.location.pathname,
              }}
            />
          )
        }
      }}
    />
  )
}
export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">首页</Link>
          </li>
          <li>
            <Link to="/login">登录页</Link>
          </li>
          <li>
            <Link to="/personal">个人中心</Link>
          </li>
        </ul>
        <div>
          <Switch>
            <Route path="/login" component={Login} />
            <ProtectedRoute path="/personal" component={Personal} />
            {/* render和children的区别:render函数是匹配后才会运行,children函数无论是否匹配都会运行(但是在switch里面必须精确匹配才能运行) */}
            {/* <Route path="/personal" render={ values => {
                            console.log(values)
                            return <h1>asdfdasdfa</h1>
                        }} /> */}
            {/* <Route path="/personal" children={() => 123}></Route> */}
            {/* 如果是下面这种情况,元素在Route标签内,也是需要匹配后才能渲染元素内容的(这里和children函数还要有点区别的,因为children函数在没有switch包裹的情况下也是会执行的)  */}
            {/* <Route path="/personal">123123</Route> */}
            <Route path="/" component={Home} />
          </Switch>
        </div>
      </div>
    </Router>
  )
}

非常重要的一波总结!!! Route 组件内 > children > component > render

<Route
  path="/personal"
  render={(values) => {
    console.log(values);
    return <h1>asdfdasdfa</h1>;
  }}
  component={Personal}
  children={() => 'children'}
>
  123
</Route>

// 当我在浏览器上输入http://localhost:3000/#/personal时
// 显示的是123,然后我把123删除,这时候无论点击哪个页面,都会渲染出 children
// 当我把children={() => 'children'} 去掉会渲染component={Personal}
// 只有Route组件中没有上述的情况并且匹配到/personal时才会执行render函数

//当我在浏览器上输入http://localhost:3000/时
// 然后我把123删除,这时候无论点击哪个页面,不管路由是否匹配,都会渲染出 children

//总结: component render children 同时存在会渲染children
//      component render 同时存在会渲染component

//  如果children是函数,不管是否匹配,都会渲染123
//  <Route path="/a" component={PageA} children={() => 123} />
//  如果children不是函数,只有在匹配的情况才会渲染123
//  <Route path="/a" component={PageA} children={123} />

// 最后就是render是函数的情况传递的默认参数是{history: {…}, location: {…}, match: {…}, staticContext: undefined}
<Route
key={i}
{...rest}
path={newPath}
render={(values) => {
  // console.log('values', values);
  return (
    <Component {...values}>{getRoutes(children, newPath)}</Component>
  );
}}
/>

下面这个例子有个问题:为什么我点击首页的时候会执行三次 ProtectedRoute

import React from "react"
import {
  HashRouter as Router,
  Route,
  Switch,
  Link,
  Redirect,
} from "react-router-dom"
let loginInfo = {
  isLogin: false,
  login() {
    this.isLogin = true
  },
  loginOut() {
    this.isLogin = false
  },
}
function Home() {
  return (
    <div>
      <h1>首页</h1>
    </div>
  )
}

function Personal() {
  return (
    <div>
      <h1>个人中心</h1>
      <p>如果你看到该页面,说明你已经完成了登录</p>
    </div>
  )
}
function Login(props) {
  console.log("login", props)
  return (
    <div>
      <h1>登录授权页</h1>
      <p>该页面仅作测试,点击下方按钮即登录成功</p>
      <button
        onClick={() => {
          loginInfo.login()
          if (props.location.state) {
            props.history.push(props.location.state)
          } else {
            props.history.push("/")
          }
          // const query = qs.parse(props.location.search);
          // if (query.returnurl) {
          //     props.history.push(query.returnurl);
          // }
          // else {
          //     props.history.push("/");
          // }
        }}>
        登录
      </button>
    </div>
  )
}

function ProtectedRoute(props) {
  console.log(props, loginInfo.isLogin)
  return (
    <Route
      render={(values) => {
        console.log(values, "valuse") //{history: {…}, location: {…}, match: {…}, staticContext: undefined}
        if (loginInfo.isLogin) {
          //可以正常展示页面
          return <props.Component />
        } else {
          // return <Redirect to={{
          //     pathname: "/login",
          //     search: "?returnurl=" + values.location.pathname
          // }} />
          console.log(1111)
          return (
            <Redirect
              to={{
                pathname: "/login",
                state: values.location.pathname,
              }}
            />
          )
        }
      }}
    />
  )
}
export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">首页</Link>
          </li>
          <li>
            <Link to="/login">登录页</Link>
          </li>
          <li>
            <Link to="/personal">个人中心</Link>
          </li>
        </ul>
        <div>
          {/* <Switch> */}
          <Route path="/login" component={Login} />
          <ProtectedRoute path="/personal" component={Personal} />
          {/* render和children的区别:render是匹配后才会运行,children(函数)无论是否匹配都会运行 */}
          {/* <Route
              path="/personal"
              render={(values) => {
                console.log(values);
                return <h1>asdfdasdfa</h1>;
              }}
            /> */}
          {/* <Route
            path="/personal"
            render={(values) => {
              console.log(values);
              return <h1>asdfdasdfa</h1>;
            }}
            component={Personal}
          ></Route> */}
          <Route path="/" component={Home} />
          {/* </Switch> */}
        </div>
      </div>
    </Router>
  )
}

实现 Vue 路由模式

核心代码

//routeConfig.js  配置文件
import Home from "./Home"
import News from "./News"
import NewsHome from "./NewsHome"
import NewsDetail from "./NewsDetail"
import NewsSearch from "./NewsSearch"

export default [
  {
    path: "/news",
    component: News,
    name: "news",
    children: [
      { path: "/", name: "newsHome", exact: true, component: NewsHome },
      { path: "/dl", name: "newsDetail", exact: true, component: NewsDetail },
      { path: "/ser", name: "newsSearch", exact: true, component: NewsSearch },
    ],
  },
  { path: "/", name: "home", component: Home },
]
//BetterLink.js  封装Link组件,通过name属性访问Route
import React from "react"
import { Link } from "react-router-dom"
import routeConfig from "./routeConfig"

export default function BetterLink({ to, ...rest }) {
  if (to.name && typeof to !== "string") {
    to.pathname = getPathFromName(to.name, "/", routeConfig)
    if (to.pathname === undefined) {
      throw new Error(`name属性值${to.name}无效`)
    }
  }

  return <Link {...rest} to={to} />
}

/**
 * 根据name的值,查找对应的path,没有考虑有params的情况
 * 如果有,会比较复杂,需要用到第三方库path-to-regexp
 * @param {*} name
 */
function getPathFromName(name, baseUrl, routesArr) {
  for (const item of routesArr) {
    let newPath = baseUrl + item.path
    newPath = newPath.replace(/\/\//g, "/")
    if (item.name === name) {
      return newPath
    } else {
      if (Array.isArray(item.children)) {
        const path = getPathFromName(name, newPath, item.children)
        if (path !== undefined) {
          return path
        }
      }
    }
  }
}
// RootRouter.js

import React from "react"
import { Route, Switch } from "react-router-dom"
import routeConfig from "./routeConfig"

/**
 * 根据一个路由配置数组,遍历该数组,得到一组Route组件
 * @param {*} routes
 */
function getRoutes(routes, basePath) {
  if (!Array.isArray(routes)) {
    return null
  }
  var rs = routes.map((rt, i) => {
    const { children, name, path, component: Component, ...rest } = rt
    let newPath = `${basePath}${path}`
    newPath = newPath.replace(/\/\//g, "/")
    return (
      <Route
        key={i}
        {...rest}
        path={newPath}
        render={(values) => {
          return (
            <Component {...values}>{getRoutes(rt.children, newPath)}</Component>
          )
        }}
      />
    )
  })
  return <Switch>{rs}</Switch>
}

/**
 * 使用Route组件,根据不同的路径,渲染顶级页面
 */
export default function RootRouter() {
  return (
    <>
      {/* 返回一组Route */}
      {getRoutes(routeConfig, "/")}
    </>
  )
}
import React from "react"
import { BrowserRouter as Router } from "react-router-dom"
import Link from "./BetterLink"
import RootRouter from "./RootRouter"
import "./App.css"

export default function App() {
  return (
    <Router>
      <nav>
        <Link to={{ name: "home" }}>首页</Link>
        <Link to={{ name: "news" }}>新闻页</Link>
      </nav>
      <div>
        {/* 匹配网站的顶级页面 */}
        <RootRouter />
      </div>
    </Router>
  )
}

实现导航守卫

导航守卫:当离开一个页面,进入另一个页面时,触发的事件

history 对象

action: "POP"
block: ƒ block(prompt)
createHref: ƒ createHref(location)
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
length: 50
listen: ƒ listen(listener)
location: {pathname: "/page1", search: "", hash: "", state: undefined, key: "y8i2bk"}
push: ƒ push(path, state)
replace: ƒ replace(path, state)
  • listen: 添加一个监听器,监听地址的变化,当地址发生变化时,会调用传递的函数
    • 参数:函数,运行时间点:发生在即将跳转到新页面时
      • 参数 1:location 对象,记录当前的地址信息
        location: hash: ""
        key: "p08cu0"
        pathname: "/page2"
        search: ""
        state: undefined
        
      • 参数 2:action,一个字符串,表示进入该地址的方式
        • POP:出栈
          • 通过点击浏览器后退、前进
          • 调用 history.go
          • 调用 history.goBack
          • 调用 history.goForward
        • PUSH:入栈
          • history.push
        • REPLACE:替换
          • history.replace
    • 返回结果:函数,可以调用该函数取消监听
  • block:设置一个阻塞,并同时设置阻塞消息,当页面发生跳转时,会进入阻塞,并将阻塞消息传递到路由根组件的 getUserConfirmation 方法。 设置阻塞 this.props.history.block('要阻塞页面吗?');
    • 返回一个回调函数,用于取消阻塞器callback(true)
// App.js
import React from "react"
import { Route, Link, BrowserRouter as Router } from "react-router-dom"
import RouteGuard from "./RouteGuard"

function Page1() {
  return <h1>Page1</h1>
}

function Page2() {
  return <h1>Page2</h1>
}

export default function App() {
  return (
    <Router
      getUserConfirmation={(msg, callback) => {
        console.log("阻塞的参数:", msg)
        // callback(true);
        // callback(window.confirm()) //默认值
      }}>
      <ul>
        <li>
          <Link to="/page1">页面一</Link>
        </li>
        <li>
          <Link to="/page2">页面二</Link>
        </li>
      </ul>
      <RouteGuard
        onChange={(prev, location, action) => {
          console.log(prev, location, action)
        }}>
        <Route path="/page1" component={Page1}></Route>
        <Route path="/page2" component={Page2}></Route>
      </RouteGuard>
    </Router>
  )
}
// RouteGuard.js
import React, { Component } from "react"
import { BrowserRouter as Router, withRouter } from "react-router-dom"

class RouteGuard extends Component {
  componentDidMount() {
    // 添加监听器
    this.unListener = this.props.history.listen((location, action) => {
      //this.props.localtion存的是当前的路由信息
      //location存的是将要去to的路由信息
      if (this.props.onChange) {
        this.props.onChange(
          this.props.location,
          location,
          action,
          this.unListener
        )
      }
    })

    //设置阻塞
    this.props.history.block("要阻塞页面吗?")
  }

  componentWillUnmount() {
    this.unListener()
  }

  render() {
    return this.props.children
  }
}

export default withRouter(RouteGuard)

路由(Router)根组件

<Router
getUserConfirmation={(msg, callback) => {
  console.log('阻塞的参数:', msg);
  // callback(true);
  callback(window.confirm(msg)); //默认值
}}
>
  • getUserConfirmation
    • 参数:函数
      • 参数 1:阻塞消息
        • 字符串消息 this.props.history.block('阻塞消息');
        • 函数,block 函数的返回结果是一个字符串,用于表示阻塞消息(每次切换页面都会运行)
          • 参数 1:location 对象
          • 参数 2:action 值
      • 参数 2:回调函数,调用该函数并传递 true,则表示进入到新页面,否则,不做任何操作

封装导航守卫函数(代码全)

//App.js
import React from "react"
import { Route, Link } from "react-router-dom"
import RouteGuard from "./RouteGuard"

function Page1() {
  return <h1>Page1</h1>
}

function Page2() {
  return <h1>Page2</h1>
}

export default function App() {
  return (
    <RouteGuard
      onBeforeChange={(prev, cur, action, commit, unBlock) => {
        console.log(
          `页面想要从${prev.pathname}跳转到${cur.pathname},跳转方式是${action},允许跳转`
        )
        commit(true)
        unBlock() //取消阻塞,仅阻塞了一次
      }}
      onChange={(prevLocation, location, action, unListen) => {
        console.log(
          `日志:从${prevLocation.pathname}进入页面${location.pathname},进入方式${action}`
        )
        unListen() //取消监听,仅监听了一次
      }}>
      <ul>
        <li>
          <Link to="/page1">页面1</Link>
        </li>
        <li>
          <Link to="/page2">页面2</Link>
        </li>
      </ul>

      <Route path="/page1" component={Page1} />
      <Route path="/page2" component={Page2} />
    </RouteGuard>
  )
}
// RouteGuard.js
import React, { Component } from "react"
import { BrowserRouter as Router, withRouter } from "react-router-dom"

let prevLoaction, location, action, unBlock

// 辅助函数,获取上下文的信息
class _GuardHelper extends Component {
  componentDidMount() {
    //添加阻塞
    // this.props.history.block('阻塞消息');
    // this.props.histroy.block的返回值和this.props.history.listen的返回值一样,也是一个函数,用来取消该block阻塞事件
    // block的参数为函数时,接受两个参数location和action,也是和listen参数函数的一样的
    unBlock = this.props.history.block((newLocation, ac) => {
      prevLoaction = this.props.location //先调用阻塞函数,阻塞函数全局唯一,所以将location放在全局
      location = newLocation
      action = ac
      return "阻塞消息" //block参数为函数时,该函数的返回值为getUserConfirmation的参数
    })
    // 添加一个监听器,改监听器调用的时间是push一个新的地址的时候
    // 因此此时的第一个参数location对象就是新添加的location对象
    // 而this.props.location是跳转之前的location对象
    this.unListen = this.props.history.listen((location, action) => {
      if (this.props.onChange) {
        //拿到之前的location对象
        const prevLoaction = this.props.location
        this.props.onChange(prevLoaction, location, action, this.unListen)
      }
    })
  }
  componentWillUnmount() {
    unBlock() //取消阻塞
    //卸载监听器
    this.unListen()
  }
  render() {
    return null
  }
}

const GuardHelper = withRouter(_GuardHelper)

class RouteGuard extends Component {
  handleConfirm = (msg, commit) => {
    if (this.props.onBeforeChange) {
      this.props.onBeforeChange(prevLoaction, location, action, commit, unBlock)
    } else {
      commit(true)
    }
  }

  render() {
    return (
      <Router getUserConfirmation={this.handleConfirm}>
        <GuardHelper onChange={this.props.onChange} />
        {this.props.children}
      </Router>
    )
  }
}

export default RouteGuard

路由切换动画

由于 Route 是匹配到路径后才加载路由,而动画效果是需要先加载所有的路由 所以需要利用 Route 的 children 属性来做路由的切换功能,通过 match 来确定是否是匹配到当前的路由 代码全

//App.js
import React from 'react';
import * as Pages from './Demo/TransitionRoute/pages/pages';
import { BrowserRouter as Router } from 'react-router-dom';
import './App.css';
import TransitionRoute from './Demo/TransitionRoute/TransitionRoute';

export default function App() {
  return (
    <div className="main">
      <Router>
        <Pages.NavBar></Pages.NavBar>
        <div className="page-container">
          <TransitionRoute path="/" exact component={Pages.Home} />
          <TransitionRoute path="/news" exact component={Pages.News} />
          <TransitionRoute path="/personal" exact component={Pages.Personal} />
        </div>
      </Router>
    </div>
  );
}
//App.css
.main {
  width: 800px;
  margin: 0 auto;
}

.page-container {
  position: relative;
}
//pages.js
import "./pages.css"
import React from "react"
import { NavLink } from "react-router-dom"

export function NavBar() {
  return (
    <div className="header">
      <NavLink to="/" exact>
        首页
      </NavLink>
      <NavLink to="/news" exact>
        新闻页
      </NavLink>
      <NavLink to="/personal" exact>
        个人中心
      </NavLink>
    </div>
  )
}

export function Home() {
  return (
    <div className="page home">
      <h1>首页</h1>
    </div>
  )
}

export function News() {
  return (
    <div className="page news">
      <h1>新闻页</h1>
    </div>
  )
}

export function Personal() {
  return (
    <div className="page personal">
      <h1>个人中心</h1>
    </div>
  )
}
/* page.css */
.header {
  background: #008c8c;
  line-height: 50px;
  text-align: center;
}

.header a {
  color: #fff;
  text-decoration: none;
  margin: 0 10px;
  font-size: 1.3em;
}

.header a.active {
  color: rgb(234, 236, 112);
}

.page {
  width: 100%;
  height: 300px;
  /* 这里加定位是覆盖前一个页面 */
  position: absolute;
  left: 0;
  top: 0;
}

.page h1 {
  margin: 0;
  line-height: 3;
  text-align: center;
}

.home {
  background: lightblue;
}

.news {
  background: lightgreen;
}

.personal {
  background: lightpink;
}
import React from "react"
import { Route } from "react-router-dom"
import { CSSTransition } from "react-transition-group"
import "animate.css"
export default function TransitionRoute(props) {
  console.log(props)
  const { component: Component, ...rest } = props
  return (
    <Route {...rest}>
      {({ match }) => {
        return (
          <CSSTransition
            in={match ? true : false}
            timeout={500}
            classNames={{
              enter: "animate__animated animate__fast animate__bounceIn",
              exit: "animate__animated animate__fast animate__bounceOut",
            }}
            mountOnEnter={true} //这里加上mountOnEnter&&unmountOnExit表示每次切换当前页面的时候挂载,上一个页面移除
            unmountOnExit={true}>
            <Component />
          </CSSTransition>
        )
      }}
    </Route>
  )
}

滚动条复位

从页面一跳转到页面二,页面有跳转,但是滚动条不复位的问题

lorem2000 快捷键 生成两千个字符

高阶组件

// 核心代码
import React from "react"
import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"
import "./App.css"
import reset from "./resetScroll"
function withScroll(Comp) {
  return class ScrollWrapper extends React.Component {
    componentDidMount() {
      reset() //window.scrollTo(0,0)
    }
    render() {
      return <Comp {...this.props} />
    }
  }
}
const Page1WithScroll = withScroll(Page1)
const Page2WithScroll = withScroll(Page2)
function Page1(props) {
  return <div className="page page1">lorm2000</div>
}

function Page2(props) {
  return <div className="page page1">lorm2000</div>
}

export default function App() {
  return (
    <Router>
      <Route path="/page1" component={Page1WithScroll} />
      <Route path="/page2" component={Page2WithScroll} />
      <div className="nav">
        <NavLink to="/page1">页面1</NavLink>
        <NavLink to="/page2">页面2</NavLink>
      </div>
    </Router>
  )
}

使用 useEffect

// 核心代码
import React from 'react';
import { BrowserRouter as Router, Route, NavLink } from 'react-router-dom';
import './App.css';
import { useEffect } from 'react';
import reset from './resetScroll';

export default function useScroll(pathname) {
  useEffect(reset, [pathname]); //reset的主要作用是window.scrollTo(0,0)
}

function Page1(props) {
  useScroll(props.location.pathname); //url地址发生变化调用hook
  return <div className="page page1">lorm2000</div>;
}

function Page2(props) {
  useScroll(props.location.pathname);
  return <div className="page page1">lorm2000</div>;
}

export default function App() {
  return (
    <Router>
      <Route path="/page1" component={Page1} />
      <Route path="/page2" component={Page2} />
      <div className="nav">
        <NavLink to="/page1">页面1</NavLink>
        <NavLink to="/page2">页面2</NavLink>
      </div>
    </Router>
  );
}

使用自定义导航守卫

// 核心代码
import React from "react"
import { Route, NavLink } from "react-router-dom"
import RouteGuard from "./RouteGuard"
import "./App.css"
import reset from "./resetScroll"
export default function App() {
  // 利用之前封装的导航守卫  监听path的变化触发onChange
  return (
    <RouteGuard
      onChange={(prevLocation, location) => {
        if (prevLocation.pathname !== location.pathname) {
          reset() //reset的主要作用是window.scrollTo(0,0)
        }
      }}>
      <Route path="/page1" component={Page1} />
      <Route path="/page2" component={Page2} />
      <div className="nav">
        <NavLink to="/page1">页面1</NavLink>
        <NavLink to="/page2">页面2</NavLink>
      </div>
    </RouteGuard>
  )
}

阻止跳转

React 提供 Prompt 用来做跳转提示

import React from "react"
import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"
import "./App.css"
import Page1 from "./Page1"
import Page2 from "./Page2"
export default function App() {
  return (
    <Router
      getUserConfirmation={(msg, callback) => {
        callback(window.confirm(msg))
      }}>
      <div className="nav">
        <NavLink to="/page1">页面1</NavLink>
        <NavLink to="/page2">页面2</NavLink>
      </div>
      <div className="container">
        <Route path="/page1" component={Page1} />
        <Route path="/page2" component={Page2} />
      </div>
    </Router>
  )
}
//Page1.js
import React from 'react';
export default function Page1() {
  return <h1>Page1</h1>;
}
//Page2.js
import React, { Component } from 'react';
import { Prompt } from 'react-router-dom';
export default class Page2 extends Component {
  state = {
    val: '',
  };
  render() {
    return (
      <div>
        <Prompt
          when={this.state.val !== ''}
          message="别乱来,会导致数据丢失,真的要跳转吗?"
        />
        <textarea
          value={this.state.val}
          onChange={(e) => {
            this.setState({
              val: e.target.value,
            });
          }}
        ></textarea>
      </div>
    );
  }
}

手写 Prompt

import { Component } from "react"
import { withRouter } from "react-router-dom"

class Prompt extends Component {
  static defaultProps = {
    when: false, //当when为true的时候,添加阻塞
    message: "", //当阻塞时,跳转页面的提示消息
  }

  componentDidMount() {
    this.handleBlock()
  }

  componentDidUpdate(prevProps, prevState) {
    this.handleBlock()
  }

  handleBlock() {
    if (this.props.when) {
      //添加阻塞
      this.unBlock = this.props.history.block(this.props.message)
    } else {
      if (this.unBlock) {
        this.unBlock()
      }
    }
  }

  componentWillUnmount() {
    if (this.unBlock) {
      this.unBlock()
    }
  }

  render() {
    return null
  }
}

export default withRouter(Prompt)

React-Router 源码

path-to-reg

第三方库:path-to-regexp,用于将一个字符串正则(路径正则,path regexp) yarn add path-to-regexp

(?:pattern)
非获取匹配,匹配pattern但不获取匹配结果,不进行存储供以后使用。这在使用或字符"(|)"来组合一个模式的各个部分是很有用。例如"industr(?:y|ies)"就是一个比"industry|industries"更简略的表达式。
(?=pattern)
非获取匹配,正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如,"Windows(?=95|98|NT|2000)"能匹配"Windows2000"中的"Windows",但不能匹配"Windows3.1"中的"Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)
非获取匹配,正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如"Windows(?!95|98|NT|2000)"能匹配"Windows3.1"中的"Windows",但不能匹配"Windows2000"中的"Windows"。
(?<=pattern)
非获取匹配,反向肯定预查,与正向肯定预查类似,只是方向相反。例如,"(?<=95|98|NT|2000)Windows"能匹配"2000Windows"中的"Windows",但不能匹配"3.1Windows"中的"Windows"。
(?<!pattern)
非获取匹配,反向否定预查,与正向否定预查类似,只是方向相反。例如"(?<!95|98|NT|2000)Windows"能匹配"3.1Windows"中的"Windows",但不能匹配"2000Windows"中的"Windows"。这个地方不正确,有问题
import { pathToRegexp } from "path-to-regexp"

const result = pathToRegexp("/news/:id/:page")

console.log(result) //   /^\/news(?:\/([^\/#\?]+?))(?:\/([^\/#\?]+?))[\/#\?]?$/i

console.log(result.exec("/news/123/345")) //  ['/news/123/345', '123', '345', index: 0, input: '/news/123/345', groups: undefined]

var reg = /^\/news(?:\/([^\/#\?]+?))(?:\/([^\/#\?]+?))[\/#\?]?$/i
reg.test("/news/123/234") // true
//eg:
import { pathToRegexp } from "path-to-regexp"
let keys = []
//pathToRegexp将字符串转化为正则
const result = pathToRegexp("/news/:id/:page", keys)

//用正则去匹配字符串
console.log(result.exec("/news/123/5"))

console.log(keys) //找到/news/:id/:page里面的关键字id和page放到keys数组里
// 0: {name: "id", prefix: "/", suffix: "", pattern: "[^\/#\?]+?", modifier: ""}
// 1: {name: "page", prefix: "/", suffix: "", pattern: "[^\/#\?]+?", modifier: ""}

export default function pathMath(path, pathname, options) {
  let keys = [] // 两项 [{name:'id'...},{name:'page'...}]
  const reg = pathToRegexp(path, keys, options)
  const res = reg.exec(pathname) //匹配成功返回类似这样的数组['/news/123/345', '123', '345', index: 0, input: '/news/123/345', groups: undefined]
  //匹配失败返回的res为null
  if (!res) {
    return null //没有匹配
  }
  //匹配了
  let groups = Array.from(res)
  groups = groups.slice(1) //得到匹配的分组结果  ['/news/123/345', '123', '345'].slice(1)  => ['123', '345']
  const params = getParams(groups, keys) //得到{id:'123',page:'345'}
  return params
}
function getParams(groups, keys) {
  const obj = {}
  for (let i = 0; i < groups.length; i++) {
    const value = groups[i]
    const name = keys[i].name
    obj[name] = value
  }
  return obj
}

console.log(
  pathMath("/news/", "/news/123", {
    end: false,
  })
)
import { pathToRegexp } from "path-to-regexp"

/**
 * 得到匹配结果(match对象),如果没有匹配,返回null
 * @param {*} path 路径规则
 * @param {*} pathname 真实地址
 * @param {*} options 相关配置,该配置是一个对象,该对象中,可以出现:exact、sensitive、strict
 */
export default function matchPath(path, pathname, options) {
  // const pathname = window.location.pathname; //这种地址location没有 http://localhost:3000/#/personal
  const keys = [] //保存路径规则中的关键字
  const regExp = pathToRegexp(path, keys, getOptions(options))
  const result = regExp.exec(pathname) //匹配url地址
  if (!result) {
    return null //没有匹配
  }
  //匹配了
  let groups = Array.from(result)
  groups = groups.slice(1) //得到匹配的分组结果
  const params = getParams(groups, keys)
  return {
    isExact: pathname === result[0], //matchPath('/news/:pittle', '/news/asjf')此时isExact就是true   matchPath('/news/:pittle', '/news/asjf/123')此时isExact就是false
    params,
    path,
    url: result[0], //url是正则表达式匹配到的第一项
  }
}

/**
 * 将传入的react-router配置,转换为path-to-regexp的配置
 * @param {*} options
 */
function getOptions(options = {}) {
  const defaultOptions = {
    exact: false, //是否是精确匹配
    sensitive: false, //是否大小写敏感
    strict: false, //是否严格匹配末尾的/
  }
  const opts = { ...defaultOptions, ...options }
  return {
    sensitive: opts.sensitive,
    strict: opts.strict,
    end: opts.exact,
  }
}

/**
 * 根据匹配的分组结果,得到一个params对象
 * @param {*} groups
 * @param {*} keys
 */
function getParams(groups, keys) {
  const obj = {}
  for (let i = 0; i < groups.length; i++) {
    const value = groups[i]
    const name = keys[i].name
    obj[name] = value
  }
  return obj
}

history

复习 window.history

//pushState
const state = { page_id: 1, user_id: 5 };
const title = '';
const url = 'hello-world.html';
history.pushState(state, title, url);
window.history.go(-1);
window.history.forward();
window.history.back();
history.replaceState(stateObj, title[, url]);
window.addEventListener('popstate', (event) => {
  console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
});
// 仅调用history.pushState()orhistory.replaceState()不会触发popstate事件。该popstate事件将通过执行浏览器操作触发,例如单击后退或前进按钮(或调用history.back()或history.forward()在 JavaScript 中)。
window.addEventListener('hashchange', function() {
  console.log('The hash has changed!')
}, false);

react-guide.github.io/react-route…

www.jianshu.com/p/bdab3f6a2… [笔记]

https://www.npmjs.com/package/history 该对象提供了一些方法,用于控制或监听地址的变化。

该对象不是window.history,而是一个抽离的对象,它提供统一的 API 接口,封装了具体的实现

  • createBrowserHistory 产生的控制浏览器真实地址的 history 对象
  • createHashHistory 产生的控制浏览器 hash 的 history 对象
  • createMemoryHistory 产生的控制内存中地址数组的 history 对象

history 对象共同的特点:维护了一个地址栈

第三方库:history

以下三个函数,虽然名称和参数不同,但返回的对象结构(history)完全一致

history 对象

浏览器下面的

> http://localhost:3000/students?a=123&b=234#123

window.location

// Location {ancestorOrigins: DOMStringList, href: 'http://localhost:3000/students?a=123&b=234#123', origin: 'http://localhost:3000', protocol: 'http:', host: 'localhost:3000', …}
// ancestorOrigins: DOMStringList {length: 0}
// assign: ƒ assign()
// hash: "#123"
// host: "localhost:3000"
// hostname: "localhost"
// href: "http://localhost:3000/students?a=123&b=234#123"
// origin: "http://localhost:3000"
// pathname: "/students"
// port: "3000"
// protocol: "http:"
// reload: ƒ reload()
// replace: ƒ replace()
// search: "?a=123&b=234"
// toString: ƒ toString()
// valueOf: ƒ valueOf()
// Symbol(Symbol.toPrimitive): undefined
// [[Prototype]]: Location
  • action:当前地址栈,最后一次操作的类型
    • 如果是通过 createXXXHistory 函数新创建的 history 对象,action 固定为 POP
    • 如果调用了 history 的 push 方法,action 变为 PUSH
    • 如果调用了 history 的 replace 方法,action 变为 REPLACE
  • push:向当前地址栈指针位置,入栈一个地址
  • replace:替换指针指向的地址
  • go:控制当前地址栈指针偏移,如果是 0,地址不变;如果是负数,则后退指定的步数;如果是正数,则前进指定的步数;
  • length:当前栈中的地址数量
  • goBack:相当于 go(-1)
  • goForward:相当于 go(1)
  • location:表达当前地址中的信息
  • listen:函数,用于监听地址栈指针的变化
    • 该函数接收一个函数作为参数,该参数表示地址变化后要做的事情
      • 参数函数接收两个参数
      • location:记录了新的地址
      • action:进入新地址的方式
        • POP:指针移动,调用 go、goBack、goForward、用户点击浏览器后退按钮
        • PUSH:调用 history.push
        • REPLACE:调用 history.replace
    • 该函数有一个返回值,返回的是一个函数,用于取消监听
  • block:用于设置一个阻塞,当页面发生跳转时,会将指定的消息传递到 getUserConfirmation,并调用 getUserConfirmation 函数
    • 该函数接收一个字符串作为参数,表示消息内容,也可以接收一个函数作为参数,函数的返回值是消息内容
    • 该函数返回一个取消函数,调用取消函数可以解除阻塞
  • createHref:basename+url
history.push(path, [state])
history.replace(path, [state])
history.go(n)
history.goBack()
history.goForward()
history.canGo(n)(only in createMemoryHistory)
// Push a new entry onto the history stack.
history.push("/home")

// Push a new entry onto the history stack with a query string
// and some state. Location state does not appear in the URL.
history.push("/home?the=query", { some: "state" })

// If you prefer, use a single location-like object to specify both
// the URL and state. This is equivalent to the example above.
history.push({
  pathname: "/home",
  search: "?the=query",
  state: { some: "state" },
})

// push之后会将state的数据放到window.history.state里面的state属性中
// pushState的值会放到window.history.state中
// window.history.pushState({a:1,b:2,state:123,key:'asfdas'},null,'/213')
// window.history.state //{a: 1, b: 2, state: 123, key: "asfdas"}
//所以push调用的是window.history.pushState
// window.history.state
// {
//     "key": "4epm2y",
//     "state": {
//         "some": "state"
//     }
// }

// Go back to the previous history entry. The following
// two lines are synonymous.
history.go(-1)
history.goBack()

重点

window.h = createBrowserHistory()
// 浏览器上测试
window.h //{length: 5, action: "POP", location: {…}, createHref: ƒ, push: ƒ, …}
var a = window.h
window.h.go(-1)
var b = window.h //{length: 5, action: "POP", location: {…}, createHref: ƒ, push: ƒ, …}
a === b //true  说明地址变化会复用history对象
window.h.push("jy")
var c = window.h //{length: 5, action: "PUSH", location: {…}, createHref: ƒ, push: ƒ, …}
a === c //true

// c.location   //{pathname: "/jy", search: "", hash: "", state: undefined, key: "n3q4zt"}
// 这里的6位key值可能是用来区分变化的不同的location地址

createBrowserHistory

创建一个使用浏览器 History Api 的 history 对象

配置对象:

  • basename:设置根路径
const history = createHistory({
  basename: "/the/base",
})
history.listen((location) => {
  console.log(location.pathname) // /home
})
history.push("/home") // URL is now /the/base/home
  • forceRefresh:地址改变时是否强制刷新页面
const history = createBrowserHistory({
  forceRefresh: true,
})
  • keyLength:location 对象使用的 key 值长度
    • 地址栈中记录的并非字符串,而是一个 location 对象
  • getUserConfirmation:一个函数,该函数当调用 history 对象 block 函数后,发生页面跳转时运行
import { createBrowserHistory } from "history"
window.createBrowserHistory = createBrowserHistory
window.h = createBrowserHistory({
  basename: "/news",
  forceRefresh: false,
  keyLength: 4,
  getUserConfirmation: (msg, callback) => {
    callback(window.confirm(msg))
  },
})

window.unblock = window.h.block((location, action) => {
  return `你真的要进入${location.pathname}吗?${action}`
})

// window.unListen = window.h.listen((location, action) => {
//     console.log(location)
//     window.h.action = action;
// })

createHashHistory

创建一个使用浏览器 hash 的 history 对象

配置对象:

  • hashType:#号后给定的路径格式
    • hashbang:被 chrome 弃用,#!路径
    • noslash:#a/b/c
    • slash:#/a/b/c
const history = createHashHistory({
  hashType: "slash", // the default
})

history.push("/home") // window.location.hash is #/home

const history = createHashHistory({
  hashType: "noslash", // Omit the leading slash
})

history.push("/home") // window.location.hash is #home

const history = createHashHistory({
  hashType: "hashbang", // Google's legacy AJAX URL format
})

history.push("/home") // window.location.hash is #!/home
import { createHashHistory } from "history"
window.createHashHistory = createHashHistory
window.h = createHashHistory({
  hashType: "slash",
  getUserConfirmation: (msg, callback) => {
    callback(window.confirm(msg))
  },
})

window.unblock = window.h.block((location, action) => {
  return `你真的要进入${location.pathname}吗?${action}`
})

// window.unListen = window.h.listen((location, action) => {
//     console.log(location)
//     window.h.action = action;
// })

createMemoryHistory

创建一个使用内存中的地址栈的 history 对象,一般用于没有地址栏的环境

配置对象:详见 memoryHistory.js

import { createMemoryHistory } from "history"
window.createMemoryHistory = createMemoryHistory
window.h = createMemoryHistory({
  initialEntries: ["/", "/abc"], // 表示初始数组内容
  initialIndex: 0, // 默认指针指向的数组下标
})

// window.unblock = window.h.block((location, action) => {
//     return `你真的要进入${location.pathname}吗?${action}`;
// });

手写 createBrowserHistory

创建 location

state 处理:

var historyState = window.history.state
  1. 如果 historyState 没有值,则 state 为 undefined
  2. 如果 historyState 有值
    1. 如果值的类型不是对象
    2. 是对象
      1. 该对象中有 key 属性,将 key 属性作为 location 的 key 属性值,并且将 historyState 对象中的 state 属性作为 state 属性值
      2. 如果没有 key 属性,则直接将 historyState 赋值给 state
export default function createBrowserHistory(options = {}) {
  const {
    basename = "",
    forceRefresh = false,
    keyLength = 6,
    getUserConfirmation = (message, callback) =>
      callback(window.confirm(message)),
  } = options
  function go(step) {
    window.history.go(step)
  }
  function goBack() {
    window.history.back()
  }
  function goForward() {
    window.history.forward()
  }

  /**
   * 向地址栈中加入一个新的地址
   * @param {*} path 新的地址,可以是字符串,也可以是对象
   * @param {*} state 附加的状态数据,如果第一个参数是对象,该参数无效
   */
  function push(path, state) {
    // 两种情况
    //h.push('/abc',{a:123}); //第一种
    // history.push({         //第二种
    //   pathname: '/home',
    //   search: '?the=query',
    //   state: { some: 'state' },
    // });
    history.action = "PUSH"
    const pathInfo = handlePathAndState(path, state, basename)
    console.log(pathInfo)
    window.history.pushState(
      {
        key: createKey(keyLength),
        state: pathInfo.state,
      },
      null,
      pathInfo.path
    )
    if (forceRefresh) {
      //强制刷新
      window.location.href = pathInfo.path
    }
  }

  const history = {
    action: "POP",
    length: window.history.length,
    location: createLocation(basename),
    go,
    goBack,
    goForward,
    push,
  }

  return history
}

//createLocation需要完成的属性
//{pathname: "/home1", search: "?the=1234", state: {…}, hash: "", key: "1v6odk"}
function createLocation(basename = "") {
  //window.location
  let pathname = window.location.pathname
  // 处理basename的情况
  const reg = new RegExp(`^${basename}`)
  pathname = pathname.replace(reg, "")
  let location = {
    hash: window.location.hash,
    pathname: pathname,
    search: window.location.search,
  }
  //处理state
  //总而言之就是:如果是push添加state数据,将这个state数据添加到location中
  // 其他情况直接将window.history.state返回

  // pushState的值会放到window.history.state中
  // window.history.pushState({a:1,b:2,state:123,key:'asfdas'},null,'/213')
  // window.history.state //{a: 1, b: 2, state: 123, key: "asfdas"}
  let state,
    historyState = window.history.state
  if (historyState == null) {
    state = undefined
  } else if (typeof historyState != "object") {
    state = historyState //window.history.pushState('1234',null,'/abc');
  } else {
    if ("key" in historyState) {
      //window.history.pushState({a:1,b:2,key:"afdasr"},null,'/1234');
      // 或者直接用push添加数据,会自动生成一个key值,这里的history是用createBrowserHistory创建的对象
      // history.push({
      //   pathname: '/home',
      //   search: '?the=query',
      //   state: { some: 'state' },
      // });
      location.key = historyState.key
      state = historyState.state
    } else {
      state = historyState //window.history.pushState({a:1,b:2},null,'/abc');
    }
  }
  location.state = state
  return location
}

/**
 * 根据path和state,得到一个统一的对象格式
 * @param {*} path
 * @param {*} state
 */
function handlePathAndState(path, state, basename) {
  if (typeof path == "string") {
    return {
      path: basename + path,
      state,
    }
  } else if (typeof path == "object") {
    let pathResult = basename + path.pathname
    let { search = "", hash = "" } = path
    if (search.charAt(0) !== "?" && search !== "") {
      search = "?" + search
    }
    if (hash.charAt(0) !== "#" && hash !== "") {
      hash = "#" + hash
    }
    pathResult += search
    pathResult += hash
    return {
      path: pathResult,
      state: path.state,
    }
  } else {
    throw new TypeError("path must be string or object")
  }
}

/**
 * 产生一个指定长度的随机字符串,随机字符串中可以包含数字和字母
 * @param {*} keyLength
 */
function createKey(keyLength) {
  return Math.random().toString(36).substr(2, keyLength)
}

window.myh = createBrowserHistory({
  basename: "/news",
  // forceRefresh: true,
})

改变 location replace

export default class BlockManager {
  prompt = null //该属性是否有值,决定了是否有阻塞

  constructor(getUserConfirmation) {
    this.getUserConfirmation = getUserConfirmation
  }

  /**
   * 设置一个阻塞,传递一个提示消息
   * @param {*} prompt 可以是字符串,也可以一个函数,函数返回一个消息字符串
   */
  block(prompt) {
    if (typeof prompt !== "string" && typeof prompt !== "function") {
      throw new TypeError("block must be string or function")
    }
    this.prompt = prompt
    return () => {
      this.prompt = null
    }
  }

  /**
   * 触发阻塞
   * @param {function} callback 当阻塞完成之后要做的事情(一般是跳转页面)
   */
  triggerBlock(location, action, callback) {
    if (!this.prompt) {
      callback()
      return
    }
    let message //阻塞消息
    if (typeof this.prompt === "string") {
      message = this.prompt
    } else if (typeof this.prompt === "function") {
      message = this.prompt(location, action)
    }
    //调用getUserConfirmation
    this.getUserConfirmation(message, (result) => {
      if (result === true) {
        //可以跳转了
        callback()
      }
    })
  }
}
export default class ListenerManager {
  //存放监听器的数组
  listeners = []

  /**
   * 添加一个监听器,返回一个用于取消监听的函数
   */
  addListener(listener) {
    this.listeners.push(listener)
    const unListen = () => {
      const index = this.listeners.indexOf(listener)
      this.listeners.splice(index, 1)
    }
    return unListen
  }

  /**
   * 触发所有的监听器
   * @param {*} location
   * @param {*} action
   */
  triggerListener(location, action) {
    for (const listener of this.listeners) {
      listener(location, action)
    }
  }
}
import ListenerManager from "./ListenerManager"
import BlockManager from "./BlockManager"
export default function createBrowserHistory(options = {}) {
  const {
    basename = "",
    forceRefresh = false,
    keyLength = 6,
    getUserConfirmation = (message, callback) =>
      callback(window.confirm(message)),
  } = options

  const listenerManager = new ListenerManager()
  const blockManager = new BlockManager(getUserConfirmation)

  function go(step) {
    window.history.go(step)
  }
  function goBack() {
    window.history.back()
  }
  function goForward() {
    window.history.forward()
  }

  /**
   * 向地址栈中加入一个新的地址
   * @param {*} path 新的地址,可以是字符串,也可以是对象
   * @param {*} state 附加的状态数据,如果第一个参数是对象,该参数无效
   */
  function push(path, state) {
    changePage(path, state, true)
  }

  function replace(path, state) {
    changePage(path, state, false)
  }

  /**
   * 抽离的,可用于实现push或replace功能的方法
   * @param {*} path
   * @param {*} state
   * @param {*} isPush
   */
  function changePage(path, state, isPush) {
    let action = "PUSH"
    if (!isPush) {
      action = "REPLACE"
    }
    const pathInfo = handlePathAndState(path, state, basename)
    const location = createLoactionFromPath(pathInfo)
    // 这里的location不能用下面这种写法:
    // 原因是:blockManager需要拿到的location要是将要跳转到的location,
    // 而createLocation只有在pushState跳转后才能拿到新的跳转地址
    // 所以这里需要重新封装一个函数createLoactionFromPath(这个函数拿到我们push的path和state,其他的location属性还是拿的window上的)
    // const location = createLocation(basename);
    blockManager.triggerBlock(location, action, () => {
      if (isPush) {
        window.history.pushState(
          {
            key: createKey(keyLength),
            state: pathInfo.state,
          },
          null,
          pathInfo.path
        )
      } else {
        window.history.replaceState(
          {
            key: createKey(keyLength),
            state: pathInfo.state,
          },
          null,
          pathInfo.path
        )
      }
      listenerManager.triggerListener(location, action)
      //改变action
      history.action = action
      //改变location
      history.location = location
      if (forceRefresh) {
        //强制刷新
        window.location.href = pathInfo.path
      }
    })
  }

  /**
   * 添加对地址变化的监听
   */
  function addDomListener() {
    //popstate事件,仅能监听前进、后退、用户对地址hash的改变
    //无法监听到pushState、replaceState
    window.addEventListener("popstate", () => {
      const location = createLocation(basename)
      const action = "POP"
      blockManager.triggerBlock(location, action, () => {
        console.log("zhangjuyu")
        listenerManager.triggerListener(location, "POP")
        history.location = location
      })
    })
  }

  addDomListener()

  /**
   * 添加一个监听器,并且返回一个可用于取消监听的函数
   * @param {*} listener
   */

  //var unListen = myh.listen((location,action) => {console.log(location,action);});
  function listen(listener) {
    return listenerManager.addListener(listener)
  }

  function block(prompt) {
    return blockManager.block(prompt)
  }

  function createHref(location) {
    return basename + location.pathname + location.search + location.hash
  }

  const history = {
    action: "POP",
    createHref,
    block,
    length: window.history.length,
    go,
    goBack,
    goForward,
    push,
    replace,
    listen,
    location: createLocation(basename),
  }

  return history
}

//createLocation需要完成的属性
//{pathname: "/home1", search: "?the=1234", state: {…}, hash: "", key: "1v6odk"}
function createLocation(basename = "") {
  //window.location
  let pathname = window.location.pathname
  // 处理basename的情况
  const reg = new RegExp(`^${basename}`)
  pathname = pathname.replace(reg, "")
  let location = {
    hash: window.location.hash,
    pathname: pathname,
    search: window.location.search,
  }
  //处理state
  //总而言之就是:如果是push添加state数据,将这个state数据添加到location中
  // 其他情况直接将window.history.state返回

  // pushState的值会放到window.history.state中
  // window.history.pushState({a:1,b:2,state:123,key:'asfdas'},null,'/213')
  // window.history.state //{a: 1, b: 2, state: 123, key: "asfdas"}
  let state,
    historyState = window.history.state
  if (historyState == null) {
    state = undefined
  } else if (typeof historyState != "object") {
    state = historyState //window.history.pushState('1234',null,'/abc');
  } else {
    if ("key" in historyState) {
      //window.history.pushState({a:1,b:2,key:"afdasr"},null,'/1234');
      // 或者直接用push添加数据,会自动生成一个key值,这里的history是用createBrowserHistory创建的对象
      // history.push({
      //   pathname: '/home',
      //   search: '?the=query',
      //   state: { some: 'state' },
      // });
      location.key = historyState.key
      state = historyState.state
    } else {
      state = historyState //window.history.pushState({a:1,b:2},null,'/abc');
    }
  }
  location.state = state
  return location
}

/**
 * 根据path和state,得到一个统一的对象格式
 * @param {*} path
 * @param {*} state
 */
function handlePathAndState(path, state, basename) {
  if (typeof path == "string") {
    return {
      path: basename + path,
      state,
    }
  } else if (typeof path == "object") {
    let pathResult = basename + path.pathname
    let { search = "", hash = "" } = path
    if (search.charAt(0) !== "?" && search !== "") {
      search = "?" + search
    }
    if (hash.charAt(0) !== "#" && hash !== "") {
      hash = "#" + hash
    }
    pathResult += search
    pathResult += hash
    return {
      path: pathResult,
      state: path.state,
    }
  } else {
    throw new TypeError("path must be string or object")
  }
}

/**
 * 产生一个指定长度的随机字符串,随机字符串中可以包含数字和字母
 * @param {*} keyLength
 */
function createKey(keyLength) {
  return Math.random().toString(36).substr(2, keyLength)
}

/**
 * 根据pathInfo得到一个location对象
 * @param {*} pathInfo  {path:"/news/asdf#aaaaaa?a=2&b=3", state:状态}
 * @param {*} basename
 */
function createLoactionFromPath(pathInfo, basename) {
  //取出pathname
  let pathname = pathInfo.path.replace(/[#?].*$/, "")
  //处理basename的情况
  let reg = new RegExp(`^${basename}`)
  pathname = pathname.replace(reg, "")
  //search
  var questionIndex = pathInfo.path.indexOf("?")
  var sharpIndex = pathInfo.path.indexOf("#")
  let search
  if (questionIndex === -1 || questionIndex > sharpIndex) {
    search = ""
  } else {
    search = pathInfo.path.substring(questionIndex, sharpIndex)
  }
  //hash
  let hash
  if (sharpIndex === -1) {
    hash = ""
  } else {
    hash = pathInfo.path.substr(sharpIndex)
  }
  return {
    hash,
    pathname,
    search,
    state: pathInfo.state,
  }
}

window.myh = createBrowserHistory({
  basename: "/news",
  // forceRefresh: true,
})

手写 Router

import React, { Component } from "react"
import PropTypes from "prop-types"
// 创建的上下文对象
import ctx from "./RouterContext"
import matchPath from "./pathMatch"

export default class Router extends Component {
  static propTypes = {
    history: PropTypes.object.isRequired,
    children: PropTypes.node,
  }
  state = {
    location: this.props.history.location,
  }
  componentDidMount() {
    this.unListen = this.props.history.listen((location, action) => {
      this.props.history.action = action
      this.setState({
        location,
      })
    })
  }
  componentWillUnmount() {
    this.unListen() //取消监听
  }

  // ctxValue = {}; //上下文中的对象

  render() {
    // 这样写上下文对象地址不会变化
    // this.ctxValue.history = this.props.history;
    // this.ctxValue.location = this.state.location;
    // this.ctxValue.match = matchPath('/', this.state.location.pathname);
    const ctxValue = {
      history: this.props.history,
      location: this.state.location,
      match: matchPath("/", this.state.location.pathname),
    }
    return (
      <ctx.Provider value={ctxValue}>
        {/* <h1>{this.ctxValue.location.pathname}</h1>
        <button
          onClick={() => {
            console.log(this.ctxValue.history);
            this.ctxValue.history.push('/abc');
          }}
        >
          跳转
        </button> */}
        {this.props.children}
      </ctx.Provider>
    )
  }
}

手写 Route

  • 1200h 行有关于 children 和 render 的总结
//用于匹配路由,并将匹配的结果放入到上下文中
import React, { Component } from "react"
import ctx from "./RouterContext"
import matchPath from "./pathMatch"

export default class Route extends Component {
  /*
    path:路径规则,可以是字符串,可以是字符串数组
    children:无论是否匹配,都应该渲染的子元素
    render:匹配成功后,渲染函数
    component:匹配成功后,渲染的组件
    以下是调用matchPath方法时的配置
    exact
    strict
    sensitive
    */

  // /**
  //  * 在上下文提供者内部渲染的内容(旧)
  //  * @param {*} ctx
  //  */
  // renderChildren(ctx) {
  //   //children有值
  //   if (this.props.children !== undefined && this.props.children !== null) {
  //     //无论是否匹配都要渲染
  //     if (typeof this.props.children === 'function') {
  //       return this.props.children(ctx);
  //     } else {
  //       return this.props.children;
  //     }
  //   }
  //   //children没有值
  //   if (!ctx.match) {
  //     //没有匹配
  //     return null;
  //   }
  //   //匹配了
  //   //render有值
  //   if (typeof this.props.render === 'function') {
  //     return this.props.render(ctx);
  //   }
  //   //只有component有值
  //   if (this.props.component) {
  //     const Component = this.props.component;
  //     return <Component {...ctx} />;
  //   }
  //   return null;
  // }

  /**
   * 在上下文提供者内部渲染的内容(新)
   * @param {*} ctx
   */
  renderChildren(ctx) {
    if (typeof this.props.children === "function") {
      return this.props.children(ctx)
    }
    //match没有值的情况
    if (!ctx.match) {
      //没有匹配
      return null
    } else {
      // match有值的情况
      //匹配了
      if (this.props.children && typeof this.props.children !== "function") {
        return this.props.children
      } else {
        //render有值
        if (typeof this.props.render === "function") {
          return this.props.render(ctx)
        }
        //只有component有值
        if (this.props.component) {
          const Component = this.props.component

          return <Component {...ctx} />
        }
      }
    }
    return null
  }

  /**
   * 根据指定的location对象,返回match对象
   */
  matchRoute(location) {
    const { exact = false, strict = false, sensitive = false } = this.props
    return matchPath(this.props.path || "/", location.pathname, {
      exact,
      strict,
      sensitive,
    })
  }
  /**
   * 上下文中消费者函数
   */
  consumerFunc = (value) => {
    const ctxValue = {
      history: value.history,
      location: value.location,
      match: this.matchRoute(value.location), //value.location  {hash: "", pathname: "/page2", search: "", key: "eys59h", state: undefined}
    }
    return (
      <ctx.Provider value={ctxValue}>
        {this.renderChildren(ctxValue)}
      </ctx.Provider>
    )
  }

  render() {
    return <ctx.Consumer>{this.consumerFunc}</ctx.Consumer>
  }
}

Switch

Switch:匹配 Route 子元素,渲染第一个匹配到的 Route

实现 Switch:循环 Switch 组件的 children,依次匹配每一个 Route 组件,当匹配到时,直接渲染,停止循环

import React, { Component } from "react"
import matchPath from "./pathMatch"
import ctx from "./RouterContext"
import Route from "./Route"

export default class Switch extends Component {
  /**
   * 循环children,得到第一个匹配的Route组件,若没有匹配,则返回null
   */
  getMatchChild = ({ location }) => {
    let children = []
    if (Array.isArray(this.props.children)) {
      children = this.props.children
    } else if (typeof this.props.children === "object") {
      children = [this.props.children]
    }
    for (const child of children) {
      if (child.type !== Route) {
        //子元素不是Route组件
        throw new TypeError(
          "the children of Switch component must be type of Route"
        )
      }
      //判断该子元素是否能够匹配
      const {
        path = "/",
        exact = false,
        strict = false,
        sensitive = false,
      } = child.props
      const result = matchPath(path, location.pathname, {
        exact,
        strict,
        sensitive,
      })
      // console.log('result', result, child);
      if (result) {
        //该Route组件匹配了
        return child
      }
    }
    return null
  }

  render() {
    return <ctx.Consumer>{this.getMatchChild}</ctx.Consumer>
  }
}

测试代码

import React from "react"
import { BrowserRouter, Route, Switch } from "./react-router-dom"

function Page1() {
  return <h1>Page1</h1>
}

function Page2() {
  return <h1>Page2</h1>
}

function Change({ history }) {
  return (
    <div>
      <button
        onClick={() => {
          history.push("/page1")
        }}>
        去page1
      </button>
      <button
        onClick={() => {
          history.push("/page2")
        }}>
        去page2
      </button>
    </div>
  )
}

export default function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/page1" component={Page1} />
        <Route path="/page1" component={Page1} />
        <Route path="/page2" component={Page2} />
      </Switch>
    </BrowserRouter>
  )
}

withRouter

HOC 高阶组件,用于将路由上下文中的数据,作为属性注入到组件中

import { createContext } from "react"

const context = createContext()

context.displayName = "Router" //在调试工具中显示的名字

export default context
import React from "react"
import ctx from "./RouterContext"
export default function withRouter(Comp) {
  function RouterWrapper(props) {
    return (
      <ctx.Consumer>{(value) => <Comp {...value} {...props} />}</ctx.Consumer>
    )
  }
  //设置组件在调试工具中显示的名字
  RouterWrapper.displayName = `withRouter(${Comp.displayName || Comp.name})`
  return RouterWrapper
}

const withRouter = (Component: any) => (props: any) => {
  const location = useLocation()
  const navigate = useNavigate()
  const params = useParams()

  return (
    <Component
      {...props}
      location={location}
      navigate={navigate}
      params={params}
    />
  )
}

Link

import React from "react"
import ctx from "../react-router/RouterContext"
import { parsePath } from "history"
//parsePath的作用,是根据一个路径字符串,返回一个location对象

export default function Link(props) {
  const { to, ...rest } = props
  return (
    <ctx.Consumer>
      {(value) => {
        let loc
        if (typeof props.to === "object") {
          loc = props.to
        } else {
          //将props.to转换为location对象
          loc = parsePath(props.to)
        }
        const href = value.history.createHref(loc)
        return (
          <a
            {...rest}
            href={href}
            onClick={(e) => {
              e.preventDefault() //阻止默认行为
              value.history.push(loc)
            }}>
            {props.children}
          </a>
        )
      }}
    </ctx.Consumer>
  )
}

NavLink

import React from "react"
import Link from "./Link"
import ctx from "../react-router/RouterContext"
import matchPath from "../react-router/matchPath"
import { parsePath } from "history"

export default function NavLink(props) {
  const {
    activeClass = "active",
    exact = false,
    strict = false,
    sensitive = false,
    ...rest
  } = props
  return (
    <ctx.Consumer>
      {({ location }) => {
        let loc //保存to中的locaiton对象
        if (typeof props.to === "string") {
          loc = parsePath(props.to)
        }
        const result = matchPath(loc.pathname, location.pathname, {
          exact,
          strict,
          sensitive,
        })
        if (result) {
          return <Link {...rest} className={activeClass} />
        } else {
          return <Link {...rest} />
        }
      }}
    </ctx.Consumer>
  )
}

测试代码

import React from "react"
import { BrowserRouter, Route, Link, NavLink } from "./react-router-dom"

function Page1() {
  return (
    <div>
      <h1>Page1</h1>
    </div>
  )
}

function Page2() {
  return <h1>Page2</h1>
}

export default function App() {
  return (
    <BrowserRouter>
      <Route path="/page1" component={Page1} />
      <Route path="/page2" component={Page2} />
      <ul>
        <li>
          <Link
            to={{
              pathname: "/page1",
              search: "?a=1&b=2",
            }}>
            跳转到页面1
          </Link>
        </li>
        <li>
          <NavLink to="/page2">跳转到页面2</NavLink>
        </li>
      </ul>
    </BrowserRouter>
  )
}