深入了解 React Router 原理

说到 React 我们一定离不开和 Router 打交道。不管 Vue Router 和 React Router ,他们的原理都是差不多的。这篇文章会从一个简单的例子一直拓展到真正的 React Router。

什么是路由

路由(routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程

上面就是百度百科对路由的定义。比如我想去某个地方,那这个东西就带我去那个地方,这个东西就叫路由。

一个例子

先来说说需求,假设我们有两个组件 Login 和 Register,和两个对应的按钮。点击 Login 按钮就显示 Login 组件,点击 Register 显示 Register 组件。

我们这里使用 Hooks API 来创建 App 组件的自身状态。UI 代表了当前显示的是哪个组件的名字。

function Login() {
  return <div>Register</div>;
}

function Register() {
  return <div>Login</div>;
}

function App() {
  let [UI, setUI] = useState('Login');
  let onClickLogin = () => {
    setUI('Login')
  }
  let onClickRegister = () => {
    setUI('Register') 
  }
  let showUI = () => {
    switch(UI) {
      case 'Login':
        return <Login/>
      case 'Register':
        return <Register/>
    }
  }
  return (
    <div className="App">
      <button onClick={onClickLogin}>Login</button>
      <button onClick={onClickRegister}>Register</button>
      <div>
          {showUI()}
      </div>
    </div>
  );
}

这个其实就是路由的雏形了,每个页面对应着一个组件,然后在不同状态下去切换 。

使用 hash 来切换

当然我们更希望看到的是

不同 url -> 不同页面 -> 不同组件

我们先用 url 里的 hash 做尝试:

  1. 在进入页面的时候获取当前 url 的 hash 值,根据这个 hash 值去更新 UI 从而通过 showUI() 来切换到对应的组件
  2. 同时添加 onClick 事件点击不同按钮时,就在 url 设置对应的 hash,并切换对应的组件

这时候组件 App 可以写成这样:

function App() {
  // 进入页面时,先初始化当前 url 对应的组件名
  let hash = window.location.hash
  let initUI = hash === '#login' ? 'login' : 'register'

  let [UI, setUI] = useState(initUI);
  let onClickLogin = () => {
    setUI('Login')
    window.location.hash = 'login'
  }
  let onClickRegister = () => {
    setUI('Register') 
    window.location.hash = 'register'
  }
  let showUI = () => {
    switch(UI) {
      case 'Login':
        return <Login/>
      case 'Register':
        return <Register/>
    }
  }
  return (
    <div className="App">
      <button onClick={onClickLogin}>Login</button>
      <button onClick={onClickRegister}>Register</button>
      <div>
          {showUI()}
      </div>
    </div>
  );
}

这样其实已经满足我们的要求了,如果我在地址栏里输入 localhost:8080/#login,就会显示 <Login/>。但是这个 “#” 符号不太好看,如果输入 localhost:8080/login 就完美了。

使用 pathname 切换

如果要做得像上面说的那样,我们只能用 window.location.pathname 去修改 url 了。只要把上面代码里的 hash 改成 pathname 就好了,那么组件 App 可以写成这样:

function App() {
  // 进入页面时,先初始化当前 url 对应的组件名
  let pathname = window.location.pathname
  let initUI = pathname === '/login' ? 'login' : 'register'

  let [UI, setUI] = useState(initUI);
  let onClickLogin = () => {
    setUI('Login')
    window.location.pathname = 'login'
  }
  let onClickRegister = () => {
    setUI('Register') 
    window.location.pathname = 'register'
  }
  let showUI = () => {
    switch(UI) {
      case 'Login':
        return <Login/>
      case 'Register':
        return <Register/>
    }
  }
  return (
    <div className="App">
      <button onClick={onClickLogin}>Login</button>
      <button onClick={onClickRegister}>Register</button>
      <div>
          {showUI()}
      </div>
    </div>
  );
}

但是这里有个问题,每次修改 pathname 的时候页面会刷新,这是完全不符合我们的要求的,还不如用 hash 好。

使用 history 切换

幸运的是 H5 提供了一个好用的 history API,使用 window.history.pushState() 使得我们即可以修改 url 也可以不刷新页面,一举两得。

现在只需要修改点击回调里的 window.location.pathname = 'xxx' 就可以了,用 window.history.pushState() 去代替。

function App() {
  // 进入页面时,先初始化当前 url 对应的组件名
  let pathname = window.location.pathname
  let initUI = pathname === '/login' ? 'login' : 'register'

  let [UI, setUI] = useState(initUI);
  let onClickLogin = () => {
    setUI('Login')
    window.history.pushState(null, '', '/login')
  }
  let onClickRegister = () => {
    setUI('Register') 
    window.history.pushState(null, '', '/register')
  }
  let showUI = () => {
    switch(UI) {
      case 'Login':
        return <Login/>
      case 'Register':
        return <Register/>
    }
  }
  return (
    <div className="App">
      <button onClick={onClickLogin}>Login</button>
      <button onClick={onClickRegister}>Register</button>
      <div>
          {showUI()}
      </div>
    </div>
  );
}

到此,一个 Router 就已经被我们实现了。当然这个 Router 功能不多,不过这就是 Vue Router 和 React Router 的思想,他们是基于此来开发更多的功能而已。

约束

在前端使用路由要有个前提,那就是后端要将全部的路径都指向首页,即 index.html。否则后端会出现 404 错误。

什么叫全部路径都指向首页呢?我们想一下正常的多页网页是怎么样的:如果访问了一个不存在的路径,如 localhost:8080/fuck.html,那么后端会返回一个 error.html,里面内容显示 “找不到网页”,这种情况就是后端处理网页的路由了。因为正是后端根据不同 url 返回不同的 xxx.html 呀。

如果前端使用路由,那么后端将全部路径都指向 index.html当我们访问到一个不存在路径时,如 localhost:8080/fuck,后端不管三七二十一返回 index.html。但是这个 index.html 里有我们写的 JS 代码(React 打包后的)呀,这 JS 代码其中就包含了我们做的路由。所以我们的路由发现不存在这个路径时,就切换到 Error 组件来充当 “找不到网页” 的 HTML 文件。这就叫前端控制路由。

React Router

现在我们使用 React Router 来重写这个代码。看官网的时候一定要看这个官网,别看 Github 的那个,因为那个是 v3.0 的。

react-router 和 react-router-dom

先说说这两个鬼的不同。当我还是先手的时候总是被这两个东西搞蒙。

react-router: 实现了路由的核心功能。
react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能。

说到底就是 react-router-domreact-router 的加强版呗。那为毛不两个合在一起呢?像 Vue Router 那样多好。因为 React Native 也要路由系统呀。所以还有一个库叫 react-router-native,这个库也是基于 react-router 的,它类似 react-router-dom,加入了 React Native 运行环境下的一些功能。关系图如下:

关系图

所以我们写网站的时候一般引入 react-router-dom 就可以了。

重构

使用了 React Router 之后代码就可以精简成下面这样了。

import { BrowserRouter as Router, Route, Link } from "react-router-dom";

function Login() {
  return <div>Register</div>;
}

function Register() {
  return <div>Login</div>;
}

function App() {
  return (
    <Router>
        <div className="App">
            <Link to="/login">Login</Link>
            <Link to="/register">Register</Link>

            <Route path="/login" component={Login}></Route>
            <Route path="/register" component={Register}></Route>
        </div>
    </Router>
  );
}

可以看到 React Router 帮我们做了很多的事。比如正则的匹配,路由的切换等等。更多的用法就看上面的那个官网就可以了。

推荐阅读更多精彩内容

  • 在这个教程里,我们会从一个例子React应用开始学习react-router-dom。其中你会学习如何使用Link...
    uncle_charlie阅读 33,297评论 1 40
  • 一、基本用法 React Router 安装命令如下。 $ npm install -S react-router...
    sunnyghx阅读 3,231评论 0 6
  • 喝了两粒液体钙之后 顿时又有了想你的力气 那天站点前相拥着的踱步 好像把前半生所有的 委屈和温柔都给了你 我倘使是...
    Dianee阅读 324评论 1 3
  • 今天好像就这么被荒废了。
    倪飞飞飞飞飞阅读 150评论 0 2
  • 曾经有一段时间,经常问孩子,今天高兴的事是什么?意在培养孩子发现美、寻找快乐的积极向上的情绪状态。一开始孩子并不能...
    巴西芒果阅读 217评论 0 0
  • 说起来,这次的行程不在计划之中。 为了约束派派的行为,我与他订立了一个合同,有模有样的,他在合同上签上了自已的名字...
    寻觅一路阅读 39评论 0 0