react路由通过不同的门路渲染出不同的组件,这篇文章模仿react-router-dom的api,从零开始实现一个react的路由库
两种实现形式
路由的实现形式有两种:hash路由
和Browser路由
HashRouter
HashRouter
即通过hash
实现页面路由的变动,hash
的利用很宽泛,我最开始写代码时接触到hash
,个别用来做页面导航和轮播图定位的,hash
值的变动咱们能够通过hashchange
来监听:
window.addEventListener('hashchange',()=>{ console.log(window.location.hash);});
BrowserRouter
浏览器路由的变动通过h5的pushState
实现,pushState
是全局对象history
的办法, pushState
会往History
写入一个对象,存储在History
包含了length
长度和state
值,其中state
能够退出咱们自定义的数据信息传递給新页面,pushState
办法咱们能够通过浏览器提供的onpopstate
办法监听,不过浏览器没有提供onpushstate
办法,还须要咱们入手去实现它,当然如果只是想替换页面不增加到history
的历史记录中,也能够应用replaceState
办法,更多history
能够查看MDN,这里咱们给浏览器加上onpushstate
事件:
((history)=>{ let pushState = history.pushState; // 先把旧的pushState办法存储起来 // 重写pushState办法 history.pushState=function(state,title,pathname){ if (typeof window.onpushstate === "function"){ window.onpushstate(state,pathname); } return pushState.apply(history,arguments); }})(window.history);
筹备入口文件
首先新建react-router-dom
的入口文件index.js
,这篇文章会实现外面次要的api,所以我把次要的文件和导出内容也先写好:
import HashRouter from "./HashRouter";import BrowserRouter from "./BrowserRouter";import Route from "./Route";import Link from "./Link";import MenuLink from "./MenuLink";import Switch from "./Switch";import Redirect from "./Redirect";import Prompt from "./Prompt";import WithRouter from "./WithRouter";export { HashRouter, BrowserRouter, Route, Link, MenuLink, Switch, Redirect, Prompt, WithRouter}
Context
蕴含在路由外面的组件,能够通过props
拿到路由的api的,所以react-router-dom
应该有一个属于本人的Context
,所以咱们新建一个context
寄存外面的数据:
// context.jsimport React from "react";export default React.createContext();
HashRouter
接下来编写HashRouter
,作为路由最外层的父组件,Router
应该蕴含了提供给子组件所需的api:
//HashRouter.jsimport React, { Component } from 'react'import RouterContext from "./context";export default class HashRouter extends Component { render() { const value={ history:{}, location:{} } return ( <RouterContext.Provider value={value}> {this.props.children} </RouterContext.Provider> ) }}
react
路由最次要的是通过监听路由的变动,渲染出不同的组件,这里咱们能够先在hashRouter
监听路由变动,再传递给子组件路由的变动信息,所有我么须要一个state
来存储变动的location
信息,并且可能监听到它的变动:
//HashRouter.js...export default class HashRouter extends Component { state = { location: { pathname:location.hash.slice(1) } } componentDidMount(){ window.addEventListener("hashchange",(event)=>{ this.setState({ location:{ ...this.state.location, pathname:location.hash.slice(1) } }) }) } render() { const value={ history:{}, location: this.state.location } ... }}
同时给history
增加上push
办法,而且咱们晓得history
是能够携带自定义state
信息的,所有咱们也在组件外面定义locationState
属性,存储路由的state
信息:
//HashRouter.js//renderconst $comp = this;const value = { history: { push(to){ // to 可能是一个对象:{pathname,state} if (typeof to === "object") { location.hash = to.pathname; $comp.locationState = to.state; } else { location.hash = to; $comp.locationState = null; } } }, location: { state: $comp.locationState, //locationState存储路由state信息 pathname: this.state.location.pathname }}
BrowserRouter
BrowserRouter
跟hashRouter
很像,只是监听的对象不一样了,监听的事件变成了onpopstate
和onpushstate
,并且页面跳转用history.pushState
,其它跟hashRouter
一样,咱们新建BrowserRouter.js
,:
//BrowserRouter.jsimport React, { Component } from 'react'import RouterContext from "./context";// 重写 pushState((history) => { let pushState = history.pushState; history.pushState = function (state, title, pathname) { if (typeof window.onpushstate === "function") { // 增加 onpushstate 事件的监听 window.onpushstate(state, pathname); } return pushState.apply(history, arguments); }})(window.history);export default class HashRouter extends Component { state = { location: { pathname: location.hash.slice(1) } }; componentDidMount() { // 监听浏览器后退事件 window.onpopstate = (event) => { this.setState({ location: { ...this.state.location, state: event.state, pathname: event.pathname, } }) } // 监听浏览器后退事件,自定义 window.onpushstate = (state, pathname) => { this.setState({ location: { ...this.state.location, state, pathname } }) } } render() { const value = { history: { push(to) { if (typeof to === "object") { history.pushState(to.state, '', to.pathname); } else { history.pushState('', '', to); } }, }, location: this.state.location } return ( <RouterContext.Provider value={value}> {this.props.children} </RouterContext.Provider> ) }}
Route
Route
组件靠path
和component
两个参数渲染页面组件,逻辑是拿以后url门路跟组件的path
参数进行正则匹配,如果匹配胜利,就返回组件对应的Component
。
path-to-regexp
门路的正则转换用的是path-to-regexp,门路还依据组件参数exact
判断是否全匹配,转换后的正则能够通过regulex*%3F%24)进行测试,首先装置path-to-regexp
:
npm install path-to-regexp --save
path-to-regexp
应用:
const keys = [];const regexp = pathToRegexp("/foo/:bar", keys);// regexp = /^\/foo\/([^\/]+?)\/?$/i// keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }]
Route.js
新建Route.js
:
//Route.jsimport React, { Component } from 'react'import RouterContext from "./context"import { pathToRegexp } from "path-to-regexp"export default class Route extends Component { static contextType = RouterContext; render() { // 获取url门路 pathname 和 组件参数 path 进行正则匹配 const {pathname} = this.context.location; const {path="/",component:Component,exact=false} = this.props; const keys = []; // 正则匹配的 keys 数组汇合 const regexp = pathToRegexp(path,keys,{end:exact}); const result = pathname.match(regexp); // 取得匹配后果 // 将context的值传递给子组件应用 let props = { history:this.context.history, location:this.context.location, } if (result){ // 如果匹配胜利,获取门路参数 const match = {}; // 将result解构进去,第一个是url门路 const [url,...values] = result; // 将门路参数提取进去 const params = keys.map(k=>k.name).reduce((total,key,i)=>(total[key]=values[i]),{}); match = {url,path,params,isExact:url===pathname}; props.match = match; return <Component {...props} /> } return null; }}
render和children
Route
组件还提供了两个参数render
和children
,这两个参数可能让组件通过函数进行渲染,并将props
作为参数提供,这在编写高阶函数中十分有用:
<Route path="/" component={Comp} render={(props) => <Comp {...props}/>}></Route><Route path="/" component={Comp} children={(props) => <Comp {...props}/>}></Route>
所以咱们在Route
组件中也解构出render
和children
,如果有这两个参数的状况下,间接执行返回后的后果并把props
传进去:
//Route.js...const { render, children } = this.props;...if (result) { if (render) return render(props); if (children) return children(props); return <Component {...props} />}if (render) return render(props);if (children) return children(props);return null;
Link 和 MenuLink
Link
和MenuLink
两个组件都提供了路由跳转的性能,其实是包装了一层的a
标签,不便用户在hash
和browser
路由下都能放弃同样的跳转和传参操作,而MenuLink
还给以后路由匹配的组件增加active
类名,不便款式的管制。
Link
// Link.jsimport React, { Component } from 'react'import RouterContext from "./context";export default class Link extends Component { static contextType = RouterContext; render() { let to = this.props.to; return ( <a {...this.props} onClick={()=>this.context.history.push(to)}>{this.props.children}</a> ) }}
MenuLink
MenuLink
间接用函数组件,这里用到了Route
的children
办法去渲染子组件
//MenuLinkimport React from 'react'import {Link,Route} from "../react-router-dom"export default function MenuLink({to,exact,children,...rest}) { let pathname = (typeof to === "object") ? to.pathname : to; return <Route path={pathname} exact={exact} children={(props)=>( <Link {...rest} className={props.match ? 'active' : ''} to={to}>{children}</Link> )} />}
Switch 和 Redirect
Redirect
组件重定向到指定的组件,不过须要配合Switch
组件应用
Redirect
Redirect
的逻辑很简略,就是拿到参数to
间接执行跳转办法
//Redirect.jsimport React, { Component } from 'react'import RouterContext from "./context"export default class Redirect extends Component { static contextType = RouterContext; componentDidMount(){ this.context.history.push(this.props.to); } render() { return null; }}
Switch
能够看到Redirect
组件就是间接重定向到指定门路,如果在组件中间接引入到了这里就间接跳转了,所以咱们要写个Switch
配合它:
//Switch.jsimport React, { Component } from 'react'import RouterContext from "./context";import {pathToRegexp} from "path-to-regexp";export default class Switch extends Component { static contextType = RouterContext; render() { // 取出Switch外面的子组件,遍历查找出跟路由门路雷同的组件返回,如果没有,就会到Redirect组件中去 let {children} = this.props; let {pathname} = this.context.location; for (let i = 0, len = children.length;i<len;i++){ let {path="/",exact=false} = children[i].props; let regexp = pathToRegexp(path,[],{end:exact}); let result = pathname.match(regexp); if (result) return children[i]; } return null }}
WithRouter 和 Prompt
WithRouter
WithRouter
是一个高阶函数,通过它的包装能够让组件享有路由的办法:
//WithRouter.jsimport React, { Component } from 'react'import { Route } from '../react-router-dom';function WithRouter(WrapperComp) { return () => ( <Route render={(routerProps) => <WrapperComp {...routerProps} />} /> )}export default WithRouter
Prompt
Prompt
用的比拟少,组件中如果须要用到它,须要提供when
和message
两个参数,给用户提示信息:
//Prompt.jsimport React, { Component } from 'react'import RouterContext from "./context"export default class Prompt extends Component { static contextType = RouterContext; componentWillUnmount() { this.context.history.unBlock(); } render() { let { message, when } = this.props; let { history, location } = this.context; if (when) { history.block(message(location)) } else { history.unBlock(); } return null; }}
能够看到,history
中新增了block
和unBlock
办法,用来显示提醒的信息,所以要到history
中增加这两个办法,并在路由跳转的时候截取,如果有信息须要提醒,就给予提醒:
//HashRouter.js && BrowserRouter.js...history: { push(){ if ($comp.message) { let confirmResult = confirm($comp.message); if (!confirmResult) return; $comp.message = null; } ... }, block(message){ $comp.message = message }, unBlock(){ $comp.message = null; }}...
ok! 到了这里,一个react的路由插件就功败垂成了!