这一辑我决定摹写一个基于Hash实现的react路由库,但是鉴于browserRouter很简单也就顺便一起摹写了。
- 1、我先将要实现的文件归类导出一下,原谅我默默地装一下大(年龄大)佬(不用装就是老)。
- react-router-dom/index.js
import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
Import Switch from './Switch';
import Redirect from './Redirect';
import NavLink from './NavLink';
import WithRouter from './WithRouter';
import Prompt from './Prompt';
import BrowserRouter from './BrowserRouter';
export {
HashRouter,
Route,
Link,
Swicth,
Redirect,
NavLink,
WithRouter,
Prompt,
BrowserRouter
}
- 第一步我们先来实现一下
HashRouter
事实上HashRouter只是个容器,并不具有DOM结构,他的职责就是渲染他的子组件,和想下层组件传递数据
exp: location
。
- react-router-dom/HashRouter.js
<!--创建并导出一个HashRouter文件-->
import React from 'react';
import RouterContext from './RouterContext';
export default class HashRouter extends React.Component {
state = {
location : {
// 去掉#号,hash是带有#
pathname: window.location.hash.slice(1)
}
}
componentDidMount(){
// 监听haschange事件,当以触发hashchange事件就是去改变当前的状态之后再同步hash的值
window.addEventListener('hashchange', () = > {
this.setState({
...this.state.location,
pathname: window.location.hash.slice(1) || '/',
state: this.locationState
})
});
window.location.hash = window.location.hash || '/';
}
render() {
let self = this;
let history = {
location: this.state.location,
push(to) {
if(typeof to === 'object') { // 有可能用户传递的是个包含路径和状态的对象
let {pathname, state} = to;
that.locationState = state;
window.location.hash = pathname;
} else { //传递的字符串
window.location.hash = to
}
}
block(prompt){
history.prompt = prompt
}
unblock() {
history.prompt = null;
}
}
let routerValue = {
location: that.state.location,
history
}
return (
<RouterContext.Provider value= {routerValue}>
{this.props.chilren}
</RouterContext.Provider>
)
}
}
这里我们会实现Route路由组件,他很多时候代表的是一条路由规则。hashRouter中的state、location、pathname是通过上下文传递出去的。
- react-router-dom/Route.js
import React from 'react';
import RouterContext from './RouterContext';
import pathToRegexp from ' path-to-regexp';
export default class Route extends React.Component {
static contextType = RouterContext; // this.context.location.pathname
// component是route组件里面传过来的那个component属性值也就是该条路由所对应的组件,这里我将它重命名为RouteComponent。
// path是该路由组件上传过来的path路径用来做匹配的
// excat 代表的是该条路由上的路径是不是精确匹配和`path-to-regexp`的参数对应。
// 组件的渲染有三种方式分别是传入一个render函数进行渲染、传入一个children而且改children属性是一个函数执行即可实现渲染、最后就是直接按照匹配的组件进行渲染。
render() {
let { path='/', component: RouteComponent, excat= false, render, children } = this.props;
let path = typeof path === 'object' ? path.pathname : path;
let pathname = this.context.location.pathname;
let paramNames = [];
let regexp = pathToRegexp(path, paramNames, {end: exact});
paramNames = paramNames.map(p=>p.name);
let matched = pathname.match(regexp);
let routeProps = {
location: this.context.location,
history: this.context.history
};
if(matched) {
let [url, ...values] = matched;
let params = values.reduce((memo, cur, index) => {
memo[paramNames[index]] = cur;
return memo;
}, {})
let match - {
url,
path,
isExact: pathname === url,
params
}
routerProps.match = match;
if(RouteComponent) {
return <RouteCompoponent {...routerProps}/>
} else if(render) {
return render(routerProps);
} else if(children) {
children(routerProps);
} else {
return null;
}
} else {
if(children) {
return children(routerProps);
} else {
return null;
}
}
}
}
// match 属性的存在与否很大程度上和这个组件是否是路由渲染出来的正相关
// isExact 表示的是是否为精确匹配,同时也是代表了pathname和url是否完全相同
- react-router-dom/RouterContext.js
// 很多人会问上面那个为什么有个文件不知道在哪,那么接下来我就来实现一下RouterContext文件
// 这个其实贼简单,不信你看看就知道了
import React from 'react';
export default const context = React.createContext();
// 要不要太简单了一点
- 接下来我来实现一下Link,其实细细分析一下来看,其实这个东西只不过是个标签而已,也没有那么神奇。和vue的router-link有异曲同工之妙(业界很多人都说vue临摹借鉴的。我觉得好用就行,技术嘛相互促进。)
- react-router-dom/Link.js
<!--这里面我觉得还是用函数组件来实现比较好,毕竟都已经functional programming-->
import React from 'react';
import RouterContext from 'RouterContext';
//注意千万不要写href并且赋值为具体路径,因为他会绕过路由的拦截走a标签的属性
export default function Link(props) {
return (
<RouterContext.Consumer>
{
routerValue => {
<a {...props}
onClick ={
() => {
routerValue.history.push(props.to)
}
}
>
{
prpos.children
}
</a>
}
}
</RouterContext.Consumer>
)
}
- 接下来我来实现一下Switch。他的职责很简单就是负责进行子组件的匹配,只会渲染第一个匹配上的子组件。和
vue
的router-view
很像。 - react-router-dom/Switch.js
import React, {useContext} from 'react';
import RouterContext from 'RouterContext';
import pathToRegexp from 'path-to-regexp';
// 这里我又引入了一个新东西useContext是的他就是传说中的hooks之一了。
// useContext是获取上下文对象的第三种方式
// static contextType (类中使用)
// Consumer(函数中使用)
// 还可以ReactHooks useContext获取上下文对像
export default function (props) {
let routerContext = useContext(RouterContext);
let children = props.children;
children = Array.isArray(children) ? children : [children];
let pathname = routerContext.location.pathname;// 从上下文中取出当前的路径
for(let i = 0; i < children.length; i++) {
// 千万记住这里的child是React元素或者虚拟DOM并不是什么组件
// 相当于是`React.createElement(Route,{exact,path,component})`的结果,
// `{type:Route,props:{exact,path,component}}`
let child = children[i];
let { path = '/', component, exact = false} = child.props;
<!--这里面就用到了路由处理的正则库不明白的可以看看我之前的一篇介绍path-to-regexp的文章-->
let regexp = pathToRegexp(path, [], {end: exact});
<!--用当前的路径去匹配-->
let matched = pathname.match(regexp);
<!--匹配到了直接渲染子组件-->
if(matched) {
return child;
}
}
<!--没有匹配的子组件则return null-->
return null
}
- 重定向组件,实现的是在通过url未匹配到对应组件时自动进行重定向操作。
- react-router-dom/Redirect.js
import React, {useContext} from 'react';
import RouterContext from './RouterContext';
export default function (props) {
let routerContext = useContext(RouterContext);
if(!props.from || props.from === routerContext.location.pathname) {
routerContext.history.push(props.to);
}
return null;
}
- 导航条上面实现高亮的组件。
- react-router-dom/NavLink.js
- react-router-dom/NavLink.css
.acitve {
color: #425658;
background: #eeeddd;
}
import React from 'react';
import './NavLink.css'
import { Route, Redirect, Link } from './index';
export default function (props) {
let { to, exact, children } = props;
return (
<Route
path={to}
exact={exacr}
children={
routerProps => {
<Link
className={
routerProps.match? 'active' : '' to={to}
}>
{children}
</Link>
}
}>
</Route>
)
}
- Prompt阻止跳转的组件,没有DOM结构
- react-router-dom/Prompt.js
import React, {useContext} from 'react';
import RouterContext from './RouterContext';
export default function (props) {
let routerContext = useContext(RouterContext);
let {bool, msg} = props;
if(bool) {
routerContext.history.block(msg);
} else {
routerContext.history.unblock()
}
return null
}
- 写两个高级组件
<!--1、withContext.js-->
import React from 'react';
import { Route } './index';
export default function (OldComponent) {
reutnr props => {
<Route>
render={
routerProps =><OldComponent
{...props}
{...routerProps}/>
}
</Route>
}
}
export default function (OldComponent) {
return (
<RouterContext.Consumer>
{
contextValue => (
<div>
<OldComponent/>
</div>
)
}
</RouterContext.Consumer>
)
}
- 顺便把BrowserRouter也实现一下
- react-router-dom/BrowserRouter
import React, { useState, useEffect } from 'react';
import RouterContext from "./RouterContext.js";
export default function BrowserRouter(props) {
let [currentState, setCurrentState] = useState({ location: { pathname: window.location.pathname } });
useEffect(() => {
window.onpushstate = (state, pathname) => {
setCurrentState({
location: {
...currentState.location,
pathname,
state
}
});
}
window.onpopstate = (event) => {
setCurrentState({
location: {
...currentState.location,
pathname: window.location.pathname,
state: event.state
}
});
}
}, []);
const globalHistory = window.history;
let history = {
location: currentState.location,
push(to) {
if (history.prompt) {
let target = typeof to === 'string' ? { pathname: to } : to;
let yes = window.confirm(history.prompt(target));
if (!yes) return;
}
if (typeof to === 'object') {//传的是一个对象 {pathname,state}
let { pathname, state } = to;
globalHistory.pushState(state, null, pathname);
} else {//就是个字符串
globalHistory.pushState(null, null, to);
}
},
block(prompt) {
history.prompt = prompt;
},
unblock() {
history.prompt = null;
}
}
let routerValue = {
location: currentState.location,
history
}
return (
<RouterContext.Provider value={routerValue}>
{props.children}
</RouterContext.Provider>
)
}