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.js
import React from "react";
export default React.createContext();
HashRouter
接下来编写 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
信息:
//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
BrowserRouter
跟 hashRouter
很像,只是监听的对象不一样了,监听的事件变成了 onpopstate
和onpushstate
,并且页面跳转用 history.pushState
,其它跟hashRouter
一样, 咱们新建BrowserRouter.js
,:
//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
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.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;
}
}
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.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
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>
)} />
}
Switch 和 Redirect
Redirect
组件重定向到指定的组件,不过须要配合 Switch
组件应用
Redirect
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;}
}
Switch
能够看到 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 和 Prompt
WithRouter
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
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 的路由插件就功败垂成了!