Сделать React более удобным в использовании - (Краткий разбор принципа работы react-router)

внешний интерфейс JavaScript Express React.js

Серия статей, которые делают react более удобным в использовании:

  1. Сделать реакцию более удобной в использовании - (базовый анализ реакции)
  2. Сделать React более удобным в использовании - (Краткий разбор принципа работы react-router)
  3. Сделать react более удобным в использовании - (Краткий разбор принципа react-redux)

Внешняя маршрутизация и внутренняя маршрутизация

Когда я только пришел в индустрию, я всегда понимал, что такое одностраничное приложение, грубо говоря, это путать front-end роутинг и back-end роутинг, теперь проследим их:

  1. Внешняя маршрутизация: отображение страницы контролируется внешним интерфейсом js. Ввод хэш-значения в URL-адрес не отправит запрос в фоновый режим, поэтому внешний интерфейс может контролировать, какая страница отображается и отображается с помощью сопоставление хэша со страницей.
  2. Фоновая маршрутизация: отображение страницы обрабатывается фоном в соответствии с URL-адресом, а затем возвращается в браузер.Нехэш-адрес будет отправлять запросы на сервер (historyAPI не будет отправлять запросы, которые будут представлены позже)

Если вы все еще не понимаете, вы можете использовать экспресс для создания локального сервера, чтобы увидеть эффект (ps: зачем использовать экспресс, потому что koa ленив, вам нужно скачать плагин koa-router):

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.send('welcome to home');
 }) 
app.get('/a', function (req, res) {
   res.send('welcome to a');
})
var server = app.listen(8081)

Введите localhost:8081/ в браузере

home
Введите localhost:8081/#a в браузере.
#a
Введите localhost в браузере: 8081 / A
/a
Объединив изображение и приведенное выше утверждение, вы должны знать разницу между внешней маршрутизацией и фоновой маршрутизацией.Помимо хэш-маршрутизации, есть еще один способ изменить URL-адрес и не отправлять запросы в фоновый режим.Это history.pushState( ), обратите внимание на совместимую обработку:
history-push
history-result
Но есть проблема с этим методом.Если нажать клавишу Enter еще раз, он отправит запрос в фон.Если у фонового маршрута нет соответствующего совпадения, он сообщит об ошибке 404, которую вообще нужно обрабатывать на заднем фоне.

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.send('welcome to home');
 }) 
var server = app.listen(8081)

history-error

ядро роутера

хэш-маршрутизация

В основном для прослушивания события hashchange, а затем получения данных для повторного рендеринга страницы.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a href="#/a">pageALink</a>
    <a href="#/b">pageBLink</a>
    <span id='body'></span>
    <script>
        window.addEventListener('hashchange',(e)=>{
            document.getElementById('body').innerHTML = window.location
        },false)
    </script>
</body>
</html>

history.push реализует маршрутизацию

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a onClick="go('/a')">pageALink</a>
    <a onClick="go('/b')">pageBLink</a>
    <span id='body'></span>
    <script>
        function go (pathname){
            window.history.pushState({},null,pathname);
            document.getElementById('body').innerHTML = window.location;
        }
        
        //pushState和replaceState是无法触发popstate事件
        //这里主要处理浏览器前进后退功能,不加下面的代码就无法实现前进后退功能
        window.addEventListener('popstate',(e)=>{
            let pathname = window.location;
            document.getElementById('body').innerHTML = window.location;
        })
    </script>
</body>
</html>

Анализ принципа в react-router

Базовый состав react-router

  1. BrowserRouter или hashRouter используется для рендеринга компонента, представленного маршрутизатором.
  2. Маршрут используется для сопоставления путей компонентов и фильтрации компонентов, которые необходимо отобразить.
  3. Переключатель используется для фильтрации только тех компонентов, которые необходимо визуализировать.
  4. Ссылка напрямую отображает компонент страницы
  5. Перенаправление похоже на ссылку, которая срабатывает, когда ни один маршрут не совпадает успешно.
import BrowserRouter from './BrowserRouter';
import Route from './Route';
import Link from './Link';
import Switch from './Switch';
import Redirect from './Redirect';
export {
  BrowserRouter,
  Route,
  Link,
  Switch,
  Redirect
}

BrowserRouter

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import {BrowserRouter as Router,Route} from './react-router-dom'
render(<Router>
  <Render/>
  <div>
      <Route path="/" component={Home}>
      <Route path="/user" component={User}>
  </div>
</Router>,window.root);

Из приведенного выше использования вы можете понять, что BrowserRouter на самом деле является компонентом, который имеет следующие функции:

  1. Сохраните текущий посещенный путь и повторно визуализируйте компонент, представленный маршрутом, при изменении пути.
  2. Прослушивание popstate, когда путь изменяется, состояние будет изменено, чтобы сохранить новый путь доступа, тем самым повторно отрисовывая компонент, представленный Route
  3. Предоставляет методы для изменения URL-адреса и состояния для использования внутренними встроенными компонентами, тем самым запуская повторную визуализацию страницы.
import React from 'react';
import {Provider} from './context';
// 
// 想染路径变化 刷新组件 路径定义在状态中 路径变化就更新状态
export default class BrowserRouter extends React.Component{
  state = {
    // 获取打开网页时的默认路径
    location:{
      pathname: window.location.pathname || '/',
    }
  }
  
  
  componentWillMount(){
    window.addEventListener('popstate',()=>{
      let pathname = window.location.pathname;
      this.handleChangeState(pathname);
    },false);
  }
  
  //当浏览器的路由改变时触发,改变state从而重新渲染组件
  handleChangeState(pathname){
    this.setState({
      location:{
        ...this.state.location,
        pathname
      }
    })
  }
  
  // 渲染Route,
  render(){ 
    let that = this;
    let value = {
      ...this.state,
      history:{
        push(pathname){
          // 这个方法主要是提供给Link使用的
          // 当点击Link时,会改变浏览器url并且重新渲染组件
          window.history.pushState({},null,pathname);
          that.handleChangeState(pathname);
        }
      }
    }
    return( 
    <Provider value={value}>
        {this.props.children}   //嵌入的Route组件
    </Provider>
    )
  }
}

Route

Маршрут в основном сопоставляет путь представляемого компонента с текущим URL-адресом (state.pathname).Если совпадение успешно, компонент, который он представляет, будет возвращен, и компонент, представленный им, будет отрендерен, в противном случае он вернет null.

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import {BrowserRouter as Router,Route} from './react-router-dom'
render(<Router>
  <Link to="/">首页 </Link>
  
  /*由于Link不会有点击后的样式变化,所以通常使用下面这用方法自定义link*/
  <Route path="/user" children={(match)=>{
    return <li><a className={match?'active':''}>用户</a></li>}
  }
  <Render/>
  
  <div>
      <Route path="/" component={Home}>
      <Route path="/user" component={User}>
      /*采用render参数会执行对应的函数*/
      <Route path="/user" render={(props)=>{
        return <user/>
      }}/>
  </div>
</Router>,window.root);
import React from 'react';
import {Consumer} from './context';
// 路径转化成正则,在另一篇文章【koa会用也会写——(koa-router)】可以找到其原理
import pathToRegExp from 'path-to-regexp';

// 不是通过Route渲染出来的组件没有match、location、history三个属性
export default class Route extends React.Component{
  render(){
    return <Consumer>
      {(value)=>{
        // BrowserRouter中state.pathname和浏览器url一致
        let {pathname} = value.location; 
        
        // Route组件上的参数
        let {path='/',component:Component,render,children,exact=false} = this.props; 
        
        //用来保存匹配路径的参数键值 /user/:name/:id => [name,id]
        let keys = []; 
        
        //将Route的path参数转化为正则表达式
        let reg = pathToRegExp(path,keys,{end:exact});
        
        
        if(reg.test(pathname)){
            let result = pathname.match(reg); 
            let match = {}
            
            // 将获取路径参数exp:{id:xxx,name:xxx}
            if(result){
              let [,...arr] = result;
              match.params = keys.reduce((memo,next,idx)=>{
                memo[keys[idx].name]=arr[idx]
                return memo;
              },{});
            }
            
            // 将匹配路径的参数和原来的参数合并传给Route代表的组件
            let props = {
                ...value,match
            }
            
            // component直接渲染组件
            // render执行render(props)
            // children不管是否匹配都会执行children(props)
            if(Component){
              return <Component {...props}></Component>
             }else if(render){
              return render(props);
             }else if(children){
              return render(props);
             }
        }else{
           // children 不管是否匹配到都会
           if(children){
              return render(props);
           }
           return null //Route的路径不匹配返回null,不渲染Route代表的组件
        }
      }}
    </Consumer>
  }
}

Switch

Компонент Switch на самом деле представляет собой слой компонентов, обернутых вне Route.Он будет фильтровать Route и возвращать единственный Route.Если Без Switch компоненты, представленные несколькими маршрутами, могут быть отображены.

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article';
import {BrowserRouter as Router,Route,Switch} from './react-router-dom'
render(<Router>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
    </Switch>
</Router>,window.root);
import React from 'react';
import {Consumer} from './context';
import pathToRegExp from 'path-to-regexp';
export default class Switch extends React.Component{
  render(){
    return <Consumer>
      {(value)=>{
        // BrowserRouter中state.pathname和浏览器url一致
        let pathname = value.location.pathname;
        
        // 将Route的path对url进行匹配,匹配成功返回唯一的Route
        React.Children.forEach(this.props.children,(child)=>{
          let {path='/',exact=false} = child.props;
          let reg = pathToRegExp(path,[],{end:exact});
          if(reg.test(pathname)){
            return child    
          }
        })
      }}
    </Consumer>
  }
}

Redirect

Для маршрутов, которые не совпадают, будет перенаправлено перенаправление рендеринга по умолчанию.Фактически, это прямое изменение URL-адреса и state.pathname в BrowserRouter для повторного рендеринга компонента.

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article';
import {BrowserRouter as Router,Route,Link,Switch,Redirect} from './react-router-dom'
render(<Router>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
      <Redirect to="/"/>
    </Switch>
</Router>,window.root);
import React from 'react';
import {Consumer} from './context';
export default class Redirect extends React.Component{
  render(){
    return <Consumer>
      {({history})=>{   //修改url,重新渲染组件
          history.push(this.props.to);
          return null
      }}
    </Consumer>
  }
}

Link

Подобно компоненту Redirect, разница в том, что Redirect напрямую вызывает метод в контексте для изменения URL-адреса, в то время как Link необходимо щелкнуть, чтобы вызвать метод в контексте.

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article';
import {BrowserRouter as Router,Route,Link,Switch,Redirect} from './react-router-dom'
render(<Router>
  <Link to="/">首页 </Link>
  <Link to="/user">用户</Link>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
      <Redirect to="/"/>
    </Switch>
</Router>,window.root);
import React from 'react';
import {Consumer} from './context';
export default class Link extends React.Component{
  render(){
    return <Consumer>
      {({history})=>{   //点击触发回调用,修改url,重新渲染组件
          return <a onClick={()=>{
            history.push(this.props.to)
          }}>{this.props.children}</a>
      }}
    </Consumer>
  }
}

withRoute

Компоненты, отрендеренные не Route, не имеют трех атрибутов match, location и history, но если вы хотите использовать эти три атрибута, что вам делать? Этот подход называется компонентами более высокого порядка.

import React from 'react';
import Route from './Route'
let withRouter = (Component) =>{
  return ()=>{
    return <Route component={Component}></Route>
  }
}
export default withRouter;
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
class withRouterLink extends Component {
  change = ()=>{
   this.props.history.push('/withRouterLink') // url变化,组件的跳转
  }
  render() {
    return (
      <div className="navbar-brand" onClick={this.change}>withRouter</div>
    )
  }
}
// 高阶组件
export default withRouter(Logo)
import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article.js';
import withRouterLink from './components/withRouterLink.js';
import {BrowserRouter as Router,Route,Link,Switch,Redirect} from './react-router-dom'
render(<Router>
  <Link to="/">首页 </Link>
  <Link to="/user">用户</Link>
  <withRouterLink></withRouterLink>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
    </Switch>
</Router>,window.root);

Перехват входа и повторный вход в систему

Как правило, веб-страницы будут иметь функцию входа и регистрации, если нет входа, многие страницы ограничены в доступе, и после входа они переходят на исходную страницу.

import Index from './pages/index.js';
import Protected from './pages/Protected'
export default class App extends Component {
  render() {
    return (
      <Router>
       <Index>
          <Switch>
            <Route path="/home" exact={true} component={Home}/>
            <Protected path="/profile" component={Profile}/>
            <Route path="/login" component={Login}/>
            <Redirect to="/home"/>
          </Switch>
       </Index>
      </Router>
    )
  }
}
import React, { Component } from 'react'
import {Route,Redirect} from 'react-router-dom'
export default class Protected extends Component {
  render() {
   
    let login = localStorage.getItem('login');
    
    // this.props里面有 path 有component
    //如果用户没有登录重定向到登录页
    return login?<Route {...this.props}></Route>:<Redirect to={{pathname:"/login",state:{"from":'/profile'}}}/>
  }
}
import React, { Component } from 'react'
export default class Login extends Component {
  render() {
    console.log(this.props)
    return (
      <div>
        <button onClick={()=>{
          // 通过参数识别 跳转是否正确
          localStorage.setItem('login','ok');
          
          //拿到profile页面跳转到login页面传的from
          if(this.props.location.state){
            this.props.history.push(this.props.location.state.from);
          }else{
            this.props.history.push('/');
          }
        }} className="btn btn-danger">登录</button>
         <button onClick={()=>{
          localStorage.clear('login');
        }} className="btn btn-danger">退出</button>
      </div>
    )
  }
}

Эпилог

Когда люди используют фреймворк, всегда возникает обсессивно-компульсивное расстройство желания знать, почему он используется таким образом.В противном случае пользоваться фреймворком неудобно.Не требуется знать его принцип из исходного кода, но его нужно психологически убедить.