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 (
{this.props.children}
)
}
}
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 (
{this.props.children}
)
}
}
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
}
return null;
}
}
render和children
Route
组件还提供了两个参数render
和children
,这两个参数能够让组件通过函数进行渲染,并将props
作为参数提供,这在编写高阶函数中非常有用:
}>
}>
所以我们在Route
组件中也解构出render
和children
,如果有这两个参数的情况下,直接执行返回后的结果并把props
传进去:
//Route.js
...
const { render, children } = this.props;
...
if (result) {
if (render) return render(props);
if (children) return children(props);
return
}
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 (
this.context.history.push(to)}>{this.props.children}
)
}
}
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 (
{children}
)} />
}
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
WithRouter 和 Prompt
WithRouter
WithRouter
是一个高阶函数,经过它的包装可以让组件享有路由的方法:
//WithRouter.js
import React, { Component } from 'react'
import { Route } from '../react-router-dom';
function WithRouter(WrapperComp) {
return () => (
} />
)
}
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的路由插件就大功告成了!