React Router
React Router 概述
React 路由
站点
无论是使用 Vue,还是 React,开发的单页应用程序,可能只是该站点的一部分(某一个功能块)
一个单页应用里,可能会划分为多个页面(几乎完全不同的页面效果)(组件)
如果要在单页应用中完成组件的切换,需要实现下面两个功能:
- 根据不同的页面地址,展示不同的组件(核心)
- 完成无刷新的地址切换 我们把实现了以上两个功能的插件,称之为路由
React Router
react-router
:路由核心库,包含诸多和路由功能相关的核心代码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 组件 根据不同的地址,展示不同的组件
重要属性:
- path:匹配的路径 默认情况下,不区分大小写,可以设置 sensitive 属性为 true,来区分大小写 默认情况下,只匹配初始目录,如果要精确匹配,配置 exact 属性为 true 如果不写 path,则会匹配任意路径
- component:匹配成功后要显示的组件
- 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 对象
- React-Router 中有两种模式:Hash、History,如果直接使用 window.history,只能支持一种模式
//两种模式
import { BrowserRouter as Router } from "react-router-dom"
import { HashRouter as Router } from "react-router-dom"
- 当使用 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
,用于解析地址栏中的数据
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} />
向某个页面传递数据的方式:
- 使用 state:在 push 页面时,加入 state(直接访问 push 的页面是没有数据的)
- 利用 search:把数据填写到地址栏中的?后
- 利用 hash:把数据填写到 hash 后
- 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 中没有路由信息,如果这些组件需要获取到路由信息,可以使用下面两种方式:
- 将路由信息从父组件一层一层传递到子组件
- 使用 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
- POP:出栈
- 参数 1:location 对象,记录当前的地址信息
- 返回结果:函数,可以调用该函数取消监听
- 参数:函数,运行时间点:发生在即将跳转到新页面时
- 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,则表示进入到新页面,否则,不做任何操作
- 参数 1:阻塞消息
- 参数:函数
封装导航守卫函数(代码全)
//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);
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
- 如果 historyState 没有值,则 state 为 undefined
- 如果 historyState 有值
- 如果值的类型不是对象
- 是对象
- 该对象中有 key 属性,将 key 属性作为 location 的 key 属性值,并且将 historyState 对象中的 state 属性作为 state 属性值
- 如果没有 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>
)
}