三分鐘實現一個react-router-dom5.0的路由攔截(導航守衛)

不同於vue,通過在路由裏設置meta元字符實現路由攔截。在使用 Vue ,框架提供了路由守衛功能,用來在進入某個路有前進行一些校驗工作,如果校驗失敗,就跳轉到 404 或者登陸頁面,比如 Vue 中的 beforeEnter 函數:

...
router.beforeEach(async(to, from, next) => {
    const toPath = to.path;
    const fromPath = from.path;
})
...

react實現路由攔截的基本思路還是利用Route 的render函數。通過判斷攔截條件來實現不同的組件的跳轉,從而實現攔截。在之前的版本中,React Router 也提供了類似的 onEnter 鉤子,但在 React Router 4.0 版本中,取消了這個方法。React Router 4.0 以後採用了聲明式的組件,路由即組件,要實現路由守衛功能,就得我們自己去寫了。
如果不使用路由守衛,Router 組件是這樣子的:

import * as React from 'react';
import { HashRouter,Switch,Route,Redirect } from 'react-router-dom';

import Index from "./page/index";
import Home from "./page/home";
import ErrorPage from "./page/error";
import Login from "./page/login";
 
export const Router = () => (
    <HashRouter>
        <Switch>
            <Route path="/" exact component={Index}/>
            <Route path="/login" exact component={Login}/>
            <Route path="/home" exact component={Home}/>
            <Route path="/404" exact component={ErrorPage}/>
            <Redirect to="/404" />
        </Switch>
    </HashRouter>
);

上面的 Router 組件,包含了三個頁面:

  • 登陸
  • 主頁
  • 404 頁面

以及四個路由:

  • 根路由
  • 登陸路由
  • 主頁路由
  • 404 路由

其中,根路由和 /home 路由,都定向到了主頁路由。
以上是一個基本的路由定義,可以在登陸/主頁和 404 頁面之間來回跳轉,但也有一些問題:

  • 非登陸狀態下,可以直接跳轉到主頁
  • 登陸狀態下,也可以輸入 /login 路由跳轉到登錄頁

現在,我們想完成這樣的功能:

  • 非登陸狀態下,無法直接跳轉到主頁,如果在非登陸狀態下進行主頁跳轉,需要重定向至登陸路由
  • 登陸狀態下,無法跳轉至登錄頁,如果在登陸狀態下進行登陸頁跳轉,需要重定向至主頁路由

要完成這個功能,有兩種方案:

  • 在每個組件中,根據 props 上的 history 對象來進行跳轉
  • 進行全局的路由守衛處理

首先在跟目錄src下創建一個routerMap.js文件,代碼如下: 

 

import Index from "./page/index";
import Home from "./page/home";
import ErrorPage from "./page/error";
import Login from "./page/login";
export default [
  { path: "/", name: "App", component: Index, auth: true },
  { path: "/home", name: "Home", component: Home, auth: true },
  { path: "/login", name: "Login", component: Login },
  { path: "/404", name: "404", component: ErrorPage },
];

將 auth 設置爲 true,表示該路由需要權限校驗。
然後,定義 Router 組件,在App.js中,該組件是經過高階組件包裝後的結果:

//App.js
import React, { Component } from "react";
import { BrowserRouter as Router, Switch } from "react-router-dom";
import FrontendAuth from "./FrontendAuth";
import routerMap from "./routerMap";
class App extends Component {
  // eslint-disable-next-line no-useless-constructor
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <Router>
        <div>
          <Switch>
            <FrontendAuth routerConfig={routerMap} />
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;

所有的路由跳轉,都交給 FrontendAuth 高階組件代理完成。下面是 FrontendAuth 組件的實現:

import React, { Component } from "react";
import { Route, Redirect } from "react-router-dom";
class FrontendAuth extends Component {
  // eslint-disable-next-line no-useless-constructor
  constructor(props) {
    super(props);
  }
  render() {
    const { routerConfig, location } = this.props;
    const { pathname } = location;
    const isLogin = sessionStorage.getItem("username");
    console.log(pathname, isLogin);
    console.log(location);
    // 如果該路由不用進行權限校驗,登錄狀態下登陸頁除外
    // 因爲登陸後,無法跳轉到登陸頁
    // 這部分代碼,是爲了在非登陸狀態下,訪問不需要權限校驗的路由
    const targetRouterConfig = routerConfig.find(
      (item) => item.path === pathname
    );
    console.log(targetRouterConfig);
    if (targetRouterConfig && !targetRouterConfig.auth && !isLogin) {
      const { component } = targetRouterConfig;
      return <Route exact path={pathname} component={component} />;
    }
    if (isLogin) {
      // 如果是登陸狀態,想要跳轉到登陸,重定向到主頁
      if (pathname === "/login") {
        return <Redirect to="/" />;
      } else {
        // 如果路由合法,就跳轉到相應的路由
        if (targetRouterConfig) {
          return (
            <Route path={pathname} component={targetRouterConfig.component} />
          );
        } else {
          // 如果路由不合法,重定向到 404 頁面
          return <Redirect to="/404" />;
        }
      }
    } else {
      // 非登陸狀態下,當路由合法時且需要權限校驗時,跳轉到登陸頁面,要求登陸
      if (targetRouterConfig && targetRouterConfig.auth) {
        return <Redirect to="/login" />;
      } else {
        // 非登陸狀態下,路由不合法時,重定向至 404
        return <Redirect to="/404" />;
      }
    }
  }
}
export default FrontendAuth;

創建登錄的界面src/page/login.js,代碼如下:

import React, { Component } from "react";
import { Redirect } from "react-router-dom"; //重定向使用
class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: "",
      rediectToReferrer: false, // 是否重定向之前的界面
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSumit = this.handleSumit.bind(this);
  }
  // 處理用戶名、密碼的變化
  handleChange(e) {
    if (e.target.name === "username") {
      this.setState({
        username: e.target.value,
      });
    } else if (e.target.name === "password") {
      this.setState({
        password: e.target.value,
      });
    }
  }
  // 提交登錄表單
  async handleSumit(e) {
    e.preventDefault();
    const username = this.state.username;
    const password = this.state.password;
    if (username.length === 0 || password.length === 0) {
      alert("用戶名或密碼不能爲空!");
      return;
    }
    // 保存信息到sessionStorage
    sessionStorage.setItem("username", username);
    // 登錄成功後,設置redirectToReferrer爲true;
    this.setState({
      rediectToReferrer: true,
    });
    let RedirectUrl = this.props.location.state
      ? this.props.location.state.from.pathname
      : "/";
    // 登陸成功之後的跳轉
    this.props.history.push(RedirectUrl);
  }
  render() {
    return (
      <form className="login" onSubmit={this.handleSumit}>
        <div>
          <label htmlFor="">
            用戶名:
            <input
              type="text"
              name="username"
              value={this.state.username}
              onChange={this.handleChange}
            />
          </label>
        </div>
        <div>
          <label htmlFor="">
            密碼:
            <input
              type="password"
              name="password"
              value={this.state.password}
              onChange={this.handleChange}
            />
          </label>
        </div>
        <input type="submit" value="登錄" />
      </form>
    );
  }
}
export default Login;

全部代碼目錄如下:

頁面上的路由跳轉,都由 FrontendAuth 高階組件代理了,在 Switch 組件內部,不再是 Route 組件,而只有一個 FrontendAuth 組件。
FrontendAuth 組件接收一個名爲 config 的 Props,這是一份路由表。同時,由於 FrontendAuth 組件放在了 Switch 組件內部,React Router 還自動爲 FrontendAuth 注入了 location 屬性,當地址欄的路由發生變化時,就會觸發 location 屬性對象上的 pathname 屬性發生變化,從而觸發 FrontendAuth 的更新(調用 render 函數)。
FrontendAuth 的 render 函數中,根據 pathname 查找到路由表中的相關配置,如果該配置中指定了無需校驗,就直接返回相應的 Route 組件。
如果查找到的配置需要進行校驗,再根據是否登陸進行處理,具體可以查看代碼中的註釋。

總結一下,實現路由守衛需要考慮到以下的問題:

  1. 未登錄情況下,訪問不需要權限校驗的合法頁面:允許訪問
  2. 登陸情況下,訪問登陸頁面:禁止訪問,跳轉至主頁
  3. 登陸情況下,訪問除登陸頁以外的合法頁面:允許訪問
  4. 登陸情況下,訪問所有的非法頁面:禁止訪問,跳轉至 404
  5. 未登錄情況下,訪問需要權限校驗的頁面:禁止訪問,跳轉至登陸頁
  6. 未登錄情況下,訪問所有的非法頁面:禁止訪問,跳轉至 404

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章