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的路由插件就大功告成了!