大师前传-ReactRouter

148 阅读5分钟

React路由原理

  • 不同的路径渲染不同的组件
  • 有两种实现方式
    • HashRouter: 利用hash实现路由切换
    • BrowserRouter:实现h5 Api实现路由的切换

HashRouter

  • 利用hash实现路由切换
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <title>React App</title>
</head>

<body>
  <div id="root"></div>
  <a href="#/a">去a</a>
  <a href="#/b">去b</a>

  <script>
    window.addEventListener('hashchange', () => {
      console.log(window.location.hash);
    });
  </script>
</body>
</html>

BrowserRouter

  • 利用h5 Api实现路由的切换
history
  • history对象提供了操作浏览器历史会话的接口
  • historylength属性声明了浏览器历史列表中的元素数量
  • pushState HTML5引进了history.pushState() 和history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate配合使用
  • onpopstate window.onpopstate是popstate事件在window对象上的事件处理程序
pushState
  • pushState会往history中写入一个对象,他造成的结果便是,History length +1 、url改变、该索引History对应有一个State对象,这个是会若是点击浏览器的后腿,便会触发popstate事件,将刚刚的存入数据对象读出
  • poshState会改变History
  • 每次使用时候会为该索引的State加入我们自定义数据
  • 每次我们会根据State的信息还原当前的view,于是用户点击后退便有了与浏览器后退前进一致的感受
  • pushState()需要三个参数:一个状态对象,一个标题(目前被忽略),和可选的一个URL
  • 调用history.pushState()或者history.replaceState()不会触发popState事件,popState事件只会在浏览器某些行为下触发,比如点击后退、前进按钮(或者在Javascript中调用history.back()、history.forward()、history.go()方法)
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <title>React App</title>
</head>

<body>
  <div id="root"></div>
  <script>
    window.onpopstate = (event) => {
      console.log({ state: event.state, pathname: window.location.pathname, type: 'popstate' });
    };
    window.onpushstate = (event) => {
      console.log(event);
    };
    (function (history) {
      var pushState = history.pushState;
      history.pushState = function (state, title, pathname) {
        if (typeof window.onpushstate == "function") {
          window.onpushstate({ state, pathname, type: 'pushstate' });
        }
        return pushState.apply(history, arguments);
      };
    })(window.history);

    //绑定事件处理函数. 
    setTimeout(() => {
      history.pushState({ page: 1 }, "title 1", "/page1");
    }, 1000);
    setTimeout(() => {
      history.pushState({ page: 2 }, "title 2", "/page2");
    }, 2000);

    setTimeout(() => {
      history.replaceState({ page: 3 }, "title 3", "/page3");
    }, 3000);

    setTimeout(() => {
      history.back();
    }, 4000);

    setTimeout(() => {
      history.go(1);//向前进一步
    }, 5000);
    // page1 => page2 => page3 => page2 => page3
  </script>
</body>
</html>

跑通路由

src\index.tsx

cnpm i  react-router-dom @types/react-router-dom path-to-regexp -S

import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
    <Router>
        <div>
            <Route path="/" component={Home} />
            <Route path="/user" component={User} />
            <Route path="/profile" component={Profile} />
        </div>
    </Router>
    , document.getElementById('root'));

src\components\Home.tsx

import React, { Component } from 'react';
export default class Home extends Component {
    render() {
        return (
            <div>Home</div>
        )
    }
}

src\components\User.tsx

import React, { Component } from 'react';
import { RouteComponentProps } from '../react-router-dom';
interface Params { }
type Props = RouteComponentProps<Params> & {

}
export default class User extends Component {
    render() {
        console.log(this.props);
        return (
            <div>User</div>
        )
    }
}

实现路由

src\history\index.tsx

export * from './types';

src\history\types.tsx

export interface Location {
    pathname: string;
    state?: any;
}
export interface History {
    location: Location;
}

src\react-router-dom\index.tsx

import HashRouter from './HashRouter';
import Route from './Route';
export {
    HashRouter,
    Route
}
export * from './types';

src\react-router-dom\types.tsx

import { History } from '../history';
export type Location = History['location'];
export interface ContextValue {
    location?: Location
}
export interface match<Params = {}> {
    params: Params;
    isExact: boolean;
    path: string;
    url: string;
}
export interface RouteComponentProps<Params = {}> {
    history: History;
    location: Location;
    match: match<Params>;
}

src\react-router-dom\context.tsx

import { ContextValue } from './types';
import { createContext } from 'react';
export default createContext<ContextValue>({});

src\react-router-dom\HashRouter.tsx

import React, { Component } from 'react'
import Context from './context';
import { ContextValue, Location } from './types';
interface Props { }
interface State {
    location: Location;
}
export default class HashRouter extends Component<Props, State> {
    state = {
        location: {
            pathname: window.location.hash.slice(1),
            state: null
        }
    }
    componentWillMount() {
        window.addEventListener('hashchange', (event: HashChangeEvent) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname: window.location.hash.slice(1) || '/'
                }
            });
        });
        window.location.hash = window.location.hash || '/';
    }
    render(): React.ReactNode {
        let value: ContextValue = {
            location: this.state.location
        }
        return (
            <Context.Provider value={value}>
                {this.props.children}
            </Context.Provider>
        )
    }
}

src\react-router-dom\Route.tsx

import React, { Component,ComponentType } from 'react';
import RouterContext from './context';
interface Props {
    path: string;
    component: ComponentType<any>
}
export default class Route extends Component<Props> {
    static contextType = RouterContext;
    render() {
        let { path, component: Component } = this.props;
        let pathname = this.context.location.pathname;
        if (pathname.startsWith(path)) {
            return <Component />
        } else {
            return null;
        }
    }
}

实现Link

src\index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
+import { HashRouter as Router, Route, Link } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
    <Router>
        <div>
            <ul>
                <li><Link to="/">Home</Link></li>
                <li><Link to="/user">User</Link></li>
                <li><Link to="/profile">Profile</Link></li>
            </ul>
            <Route path="/" component={Home} exact />
            <Route path="/user" component={User} />
            <Route path="/profile" component={Profile} />
        </div>
    </Router>
    , document.getElementById('root'));

src\history\types.tsx

export interface Location {
    pathname: string;
    state?: any;
}
export interface History {
    location: Location;
    push(path: string, state?: any): void;
}

src\react-router-dom\types.tsx

import { History } from '../history';
export type Location = History['location'];
export interface ContextValue {
    location?: Location;
+    history?: History
}
export interface match<Params = {}> {
    params: Params;
    isExact: boolean;
    path: string;
    url: string;
}
export interface RouteComponentProps<Params = {}> {
    history: History;
    location: Location;
    match?: match<Params>;
}

src\react-router-dom\Link.tsx

import React, { Component } from 'react'
import RouterContext from './context';
import { LocationDescriptor } from '../history';
export interface LinkProps {
    to: LocationDescriptor;
}
export default class Link extends Component<LinkProps> {
    static contextType = RouterContext;
    render() {
        return (
            <a {...this.props} onClick={() => this.context.history.push(this.props.to)}>{this.props.children}</a>
        )
    }
}

src\react-router-dom\HashRouter.tsx

import React, { Component } from 'react'
import Context from './context';
import { ContextValue } from './types';
+import { LocationDescriptor, Location } from '../history';
interface Props { }
interface State {
    location: Location;
}
export default class HashRouter extends Component<Props, State> {
    locationState: any
    state = {
        location: {
+            pathname: window.location.hash.slice(1),
+            state: null
        }
    }
    componentWillMount() {
        window.addEventListener('hashchange', (event: HashChangeEvent) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname: window.location.hash.slice(1) || '/',
+                    state: this.locationState
                }
            });
        });
        window.location.hash = window.location.hash || '/';
    }
    render(): React.ReactNode {
+       let that = this;
        let value: ContextValue = {
            location: this.state.location,
            history: {
                location: this.state.location,
+               push(to: LocationDescriptor) {
+                    if (typeof to === 'object') {
+                        let { pathname, state } = to;
+                        that.locationState = state;
+                        window.location.hash = pathname!;
+                    } else {
+                        window.location.hash = to;
+                    }
                }
            }
        }
        return (
            <Context.Provider value={value}>
                {this.props.children}
            </Context.Provider>
        )
    }
}

Redirect&Switch

src\index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
+import { HashRouter as Router, Route, Link, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
    <Router>
        <>
            <div className="navbar navbar-inverse">
                <div className="container-fluid">
                    <div className="navbar-heading">
                        <div className="navbar-brand">珠峰架构</div>
                    </div>
                    <ul className="nav navbar-nav">
                        <li><Link to="/">Home</Link></li>
                        <li><Link to="/user">User</Link></li>
                        <li><Link to="/profile">Profile</Link></li>
                    </ul>
                </div>
            </div>
            <div className="container">
                <div className="row">
                    <div className="col-md-12">
+                        <Switch>
+                            <Route path="/" exact component={Home} />
+                            <Route path="/user" component={User} />
+                            <Route path="/profile" component={Profile} />
+                            <Redirect to="/" />
+                        </Switch>
                    </div>
                </div>
            </div>
        </>
    </Router>
    , document.getElementById('root'));

src\react-router-dom\index.tsx

import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
+import Switch from './Switch';
+import Redirect from './Redirect';
export {
    HashRouter,
    Route,
    Link,
+    Switch,
+    Redirect
}
export * from './types';

src\react-router-dom\Switch.tsx

import React, { Component } from 'react'
import Context from './context';
import { pathToRegexp } from 'path-to-regexp';
interface Props {
    children: Array<JSX.Element>
}
export default class Switch extends Component<Props> {
    static contextType = Context;
    render() {
        let pathname = this.context.location.pathname;
        if (this.props.children) {
            for (let i = 0; i < this.props.children.length; i++) {
                let child: JSX.Element = this.props.children[i];
                let { path = '/', component: Component, exact = false } = child.props;
                let regxp = pathToRegexp(path, [], { end: exact });
                let result = pathname.match(regxp);
                if (result) {
                    return child;
                }
            }
        }
        return null;
    }
}

src\react-router-dom\Redirect.tsx

import React, { Component } from 'react'
import { LocationDescriptor } from '../history';
interface Props {
    to: LocationDescriptor;
}
export default class  extends Component<Props> {
    static contextType = Context;
    render() {
        this.context.history.push(this.props.to);
        return null;
    }
}

受保护的路由

src\index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route, Link, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
+import Protected from './components/Protected';
+import Login from './components/Login';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
    <Router>
        <>
            <div className="navbar navbar-inverse">
                <div className="container-fluid">
                    <div className="navbar-heading">
                        <div className="navbar-brand">珠峰架构</div>
                    </div>
                    <ul className="nav navbar-nav">
                        <li><Link to="/">Home</Link></li>
                        <li><Link to="/user">User</Link></li>
                        <li><Link to="/profile">Profile</Link></li>
                    </ul>
                </div>
            </div>
            <div className="container">
                <div className="row">
                    <div className="col-md-12">
                        <Switch>
                            <Route path="/" exact component={Home} />
                            <Route path="/user" component={User} />
+                            <Route path="/login" component={Login} />
+                            <Protected path="/profile" component={Profile} />
                            <Redirect to="/" />
                        </Switch>
                    </div>
                </div>
            </div>
        </>
    </Router>
    , document.getElementById('root'));

src\components\Protected.tsx

import React from 'react'
import { Route, Redirect } from '../react-router-dom';
interface Props extends Record<string, any> {
    path: string;
    component: React.ComponentType<any>;
}
export default (props: Props) => {
    let { component: RouteComponent, path } = props;
    return (
        <Route path={path} render={
            (props: any) => (
                localStorage.getItem('logined') ? <RouteComponent {...props} /> : <Redirect to={{ pathname: '/login', state: { from: props.location.pathname } }} />
            )
        } />
    )
}

withRouter

src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route, MenuLink, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import Protected from './components/Protected';
import Login from './components/Login';
+import NavHeader from './components/NavHeader';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
    <Router>
        <>
            <div className="navbar navbar-inverse">
                <div className="container-fluid">
+                    <NavHeader title="欢迎来到珠峰架构" />
                    <ul className="nav navbar-nav">
                        <li><MenuLink exact to="/">Home</MenuLink></li>
                        <li><MenuLink exact to="/user">User</MenuLink></li>
                        <li><MenuLink exact to="/profile">Profile</MenuLink></li>
                    </ul>
                </div>
            </div>
            <div className="container">
                <div className="row">
                    <div className="col-md-12">
                        <Switch>
                            <Route path="/" exact component={Home} />
                            <Route path="/user" component={User} />
                            <Route path="/login" component={Login} />
                            <Protected path="/profile" component={Profile} />
                            <Redirect to="/" />
                        </Switch>
                    </div>
                </div>
            </div>
        </>
    </Router>
    , document.getElementById('root'));

src\components\NavHeader.tsx

import React from 'react';
import { RouteComponentProps } from '../react-router-dom';
import { withRouter } from '../react-router-dom';
//只有当一个组件是通过路由Route渲染出来的话才会有RouteComponentProps里的属性
interface NavHeaderProps {
    title: string;
}

class NavHeader extends React.Component<RouteComponentProps & NavHeaderProps> {
    render() {
        return (
            <div className="navbar-header">
                <div
                    onClick={(event: React.MouseEvent) => this.props.history.push('/')}
                    className="navbar-brand">{this.props.title}</div>
            </div>
        )
    }
}
export default withRouter<NavHeaderProps>(NavHeader);

src\react-router-dom\withRouter.tsx

import React from 'react';
import { Route, RouteComponentProps } from './';
export default function <NavHeaderProps>(OldComponent: React.ComponentType<NavHeaderProps & RouteComponentProps>) {
    return (props: NavHeaderProps) => (
        <Route render={
            (routeProps: RouteComponentProps) => <OldComponent {...props} {...routeProps} />
        } />
    )
}

BrowserRouter

public\index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <title>React App</title>
+  <script>
+    (function (history) {
+      var pushState = history.pushState;
+      history.pushState = function (state, title, pathname) {
+        if (typeof window.onpushstate == "function") {
+          window.onpushstate(state, pathname);
+        }
+        return pushState.apply(history, arguments);
+      };
+    })(window.history);
+  </script>
</head>

<body>
  <div id="root"></div>
</body>
</html>

src\index.tsx

import ReactDOM from 'react-dom';
+import { BrowserRouter as Router, Route, MenuLink, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';

src\react-router-dom\BrowserRouter.tsx

import React, { Component } from 'react'
import Context from './context';
import { Message } from './';
import { LocationDescriptor, Location } from '../history';
declare global {
    interface Window {
        onpushstate: (state: any, pathname: string) => void;
    }
}

export default class BrowserRouter extends Component {
    state = {
        location: { pathname: '/' }
    }
    message: Message | null
    componentDidMount() {
        window.onpopstate = (event: PopStateEvent) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname: document.location.pathname,
                    state: event.state
                }
            });
        };
        window.onpushstate = (state: any, pathname: string) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname,
                    state
                }
            });
        };
    }
    render() {
        let that = this;
        let value = {
            location: that.state.location,
            history: {
                push(to: LocationDescriptor) {
                    if (that.message) {
                        let allow = window.confirm(that.message(typeof to == 'object' ? to : { pathname: to }));
                        if (!allow) return;
                    }
                    if (typeof to === 'object') {
                        let { pathname, state } = to;
                        window.history.pushState(state, '', pathname);
                    } else {
                        window.history.pushState('', '', to);
                    }
                },
                block(message: Message) {
                    that.message = message;
                }
            }
        }
        return (
            <Context.Provider value={value}>
                {this.props.children}
            </Context.Provider>
        )
    }
}