react路由经过不一样的路径渲染出不一样的组件,这篇文章模拟 react-router-dom的api,从零开始实现一个react的路由库
路由的实现方式有两种:hash路由
和Browser路由
react
HashRouter
即经过hash
实现页面路由的变化,hash
的应用很普遍,我最开始写代码时接触到hash
,通常用来作页面导航和轮播图定位的,hash
值的变化咱们能够经过hashchange
来监听:git
window.addEventListener('hashchange',()=>{ console.log(window.location.hash); });
浏览器路由的变化经过h5的pushState
实现,pushState
是全局对象history
的方法, pushState
会往History
写入一个对象,存储在History
包括了length
长度和state
值,其中state
能够加入咱们自定义的数据信息传递給新页面,pushState
方法咱们能够经过浏览器提供的onpopstate
方法监听,不过浏览器没有提供onpushstate
方法,还须要咱们动手去实现它,固然若是只是想替换页面不添加到history
的历史记录中,也能够使用replaceState
方法,更多history
能够查看MDN,这里咱们给浏览器加上onpushstate
事件:github
((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,因此我把主要的文件和导出内容也先写好:npm
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 }
包含在路由里面的组件,能够经过props
拿到路由的api的,因此react-router-dom
应该有一个属于本身的Context
,因此咱们新建一个context
存放里面的数据:api
// context.js import React from "react"; export default React.createContext();
接下来编写HashRouter
,做为路由最外层的父组件,Router
应该包含了提供给子组件所需的api:数组
//HashRouter.js import 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
信息:react-router
//HashRouter.js //render const $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
跟hashRouter
很像,只是监听的对象不同了,监听的事件变成了onpopstate
和onpushstate
,而且页面跳转用history.pushState
,其它跟hashRouter
同样,咱们新建BrowserRouter.js
,:app
//BrowserRouter.js import 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
组件靠path
和component
两个参数渲染页面组件,逻辑是拿当前url路径跟组件的path
参数进行正则匹配,若是匹配成功,就返回组件对应的Component
。dom
路径的正则转换用的是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 import 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; } }
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
两个组件都提供了路由跳转的功能,实际上是包装了一层的a
标签,方便用户在hash
和browser
路由下都能保持一样的跳转和传参操做,而MenuLink
还给当前路由匹配的组件添加active
类名,方便样式的控制。
// Link.js import 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
直接用函数组件,这里用到了Route
的children
方法去渲染子组件
//MenuLink import 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> )} /> }
Redirect
组件重定向到指定的组件,不过须要配合Switch
组件使用
Redirect
的逻辑很简单,就是拿到参数to
直接执行跳转方法
//Redirect.js import 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; } }
能够看到Redirect
组件就是直接重定向到指定路径,若是在组件中直接引入到了这里就直接跳转了,因此咱们要写个Switch
配合它:
//Switch.js import 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
是一个高阶函数,通过它的包装可让组件享有路由的方法:
//WithRouter.js import React, { Component } from 'react' import { Route } from '../react-router-dom'; function WithRouter(WrapperComp) { return () => ( <Route render={(routerProps) => <WrapperComp {...routerProps} />} /> ) } export default WithRouter
Prompt
用的比较少,组件中若是须要用到它,须要提供when
和message
两个参数,给用户提示信息:
//Prompt.js import 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的路由插件就大功告成了!