跳转到内容

路由

首发于:2021-01-04

路由的原本是一个后端的概念,源于服务器,在服务端中路由描述的是 URL 与处理函数之间的映射关系。通过不同的路由来请求不同的资源。

后端路由

在浏览器地址栏切换不同的 URL 时,每次都要向后台服务器发出请求,服务器响应请求,给浏览器发送相应的资源,浏览器页面也会进行刷新。

举个例子:比如你部署了一个 Apache 的 web 服务器,服务器路径下放了各种 html、js、css 等文件,你可以根据文件路径在浏览器上访问这些资源;又或者你用 PHP、Java 或者 Node.JS 开发一些后台服务器,当你访问对应的 URL 的时候后端就会给你返回你想要的 HTML 文件。

后端路由可以分担一些前端的压力,因为 HTML 文件的拼接,数据的拼接都由服务器来完成。不过当网速比较慢的时候页面加载延时会加剧,用户体验不好。

前端路由

前端路由的本质就是对一些 DOM 的显隐操作,访问不同路径的时候会显示不同的页面组件。随着 SPA(Single Page Application) 的崛起,前端路由也越来越流行,单页应用不仅页面之间数据交互是无刷新的,页面跳转也是无刷新的,这也大大节省了频繁请求后台数据带来的性能开销。前端路由不只是页面切换,更是代码组织的方式。

前端路由的原理

简单的讲就是使用浏览器 API 检测 URL 的变化,截获 URL 地址,然后解析,匹配到相应的页面组件,渲染 DOM。

前端路由有两种模式:hash 模式和 history 模式。

hash 模式

hash 不是指的散列表那个哈希,而是“#”,也被称作锚点,本身就是用来做页面定位的。#后面的值变化会触发 onhsahchange事件,通过 hash 值变化来显示不同的页面(第一次载入执行 load 事件)。

实现一个简单的 hash 路由

jsx
import React from 'react'
import ReactDOM from 'react-dom'

class HashRouter {
  constructor(routes) {
    // 当前的URL
    this.currentUrl = ''
    this.routes = routes
    this._refresh = this._refresh.bind(this)
    // 第一次加载的时候执行 load
    window.addEventListener('load', this._refresh, false)
    // hash 路由变化的时候触发
    window.addEventListener('hashchange', this._refresh, false)
  }

  // 获取 hash 路径,如果没有就返回 /
  _getHashPath(url) {
    const index = url.indexOf('#')
    if (index >= 0) {
      return url.slice(index + 1)
    }
    return '/'
  }

  _refresh(event) {
    let curURL = ''
    if (event.newURL) {
      // hash 路由变化,由 hashchange 事件触发
      curURL = this._getHashPath(event.newURL || '')
    } else {
      // 第一次加载的时候进入,由 load 事件触发
      curURL = this._getHashPath(window.location.hash)
    }
    this.currentUrl = curURL

    let route = null
    // 匹配路由
    for (let i = 0; i < this.routes.length; i++) {
      const item = this.routes[i]
      if (this.currentUrl === item.path) {
        route = item
        break
      }
    }
    // 若没有匹配到,则使用最后一个路由
    if (!route) {
      route = this.routes[this.routes.length - 1]
    }
    // 渲染当前的组件
    ReactDOM.render(route.component, document.getElementById('root'))
  }
}

// 先定义几个路由
const routes = [
  {
    path: '/',
    name: 'home',
    component: <Home />,
  },
  {
    path: '/about',
    name: 'about',
    component: <About />,
  },
  {
    path: '*',
    name: '404',
    component: <NotFound404 />,
  },
]

new HashRouter(routes)

function Home() {
  return (
    <h1>Home</h1>
  )
}

function About() {
  return (
    <h1>About</h1>
  )
}

function NotFound404() {
  return (
    <h1>NotFound404</h1>
  )
}

history 模式

history 模式会用到 window.history 中的方法:

  • back():后退到上一个路由;
  • forward():前进到下一个路由,如果有的话;
  • go(number):进入到任意一个路由,正数为前进,负数为后退;
  • pushState(obj, title, url):前进到指定的 URL,不刷新页面;
  • replaceState(obj, title, url):用 URL 替换当前的路由,不刷新页面;

这五种方法都可以修改页面 URL,而不发送请求。

如果服务端没有新更新的 URL 时,一刷新浏览器就会报错,因为刷新浏览器后,是真实地向服务器发送了一个 HTTP 的网页请求。因此若要使用 history 路由,需要服务端的支持。

第一次载入执行 load 事件;

popstate 事件可以监听 back、forward 还有 go 事件;

pushState 与 replaceState 无法被监听到,所以两个方法的监听需要我们自己来实现。window.dispatchEvent是一个事件触发器,可以借助它来实现。下面代码是对window.dispatchEvent使用的一个演示:

js
const listener = function (type) {
  // 准备被监听的原始操作
  const origin = window.history[type]
  return function () {
    // arguments 是执行的时候传入的参数
    const func = origin.apply(this, arguments)
    const newEvent = new Event(type)
    newEvent.arguments = arguments
    window.dispatchEvent(newEvent)
    return func
  }
}
window.history.pushState = listener('pushState')
window.history.replaceState = listener('replaceState')

window.addEventListener('pushState', (e) => {
  console.log(e)
}, false)

window.addEventListener('replaceState', (e) => {
  console.log(e)
}, false)

window.history.pushState({ text: 'pushState' }, 'pushState', '/pushState')
window.history.replaceState({ text: 'replaceState' }, 'replaceState', '/replaceState')

实现一个简单的 history 路由

jsx
import React from 'react'
import ReactDOM from 'react-dom'

// 先定义几个路由
const routes = [
  {
    path: '/',
    name: 'home',
    component: <Home />,
  },
  {
    path: '/about',
    name: 'about',
    component: <About />,
  },
  {
    path: '*',
    name: '404',
    component: <NotFound404 />,
  },
]

function Home() {
  return (
    <h1>Home</h1>
  )
}

function About() {
  return (
    <h1>About</h1>
  )
}

function NotFound404() {
  return (
    <h1>NotFound404</h1>
  )
}

class HistoryRouter {
  constructor(routes) {
    this.routes = routes
    this.currentUrl = ''
    this._refresh = this._refresh.bind(this)
    this._addStateListener()
    window.addEventListener('load', this._refresh, false)
    window.addEventListener('popstate', this._refresh, false)
    window.addEventListener('pushState', this._refresh, false)
    window.addEventListener('replaceState', this._refresh, false)
  }

  _addStateListener() {
    const listener = function (type) {
      // 准备被监听的原始操作
      const origin = window.history[type]
      return function () {
        // arguments 是执行的时候传入的参数
        const func = origin.apply(this, arguments)
        const newEvent = new Event(type)
        newEvent.arguments = arguments
        window.dispatchEvent(newEvent)
        return func
      }
    }
    window.history.pushState = listener('pushState')
    window.history.replaceState = listener('replaceState')
  }

  _refresh() {
    this.currentUrl = window.location.pathname
    let route = null
    // 匹配路由
    for (let i = 0; i < this.routes.length; i++) {
      const item = this.routes[i]
      if (this.currentUrl === item.path) {
        route = item
        break
      }
    }
    // 若没有匹配到,则使用最后一个路由
    if (!route) {
      route = this.routes[this.routes.length - 1]
    }
    // 渲染当前的组件
    ReactDOM.render(route.component, document.getElementById('root'))
  }
}
new HistoryRouter(routes)

如果是用的 webpack 的开发环境那么,这段代码是可以正常执行的,即便是刷新页面也可以正常返回页面,这是 webpack dev server 做了处理的。

如果是使用其它的 web 服务器,那么需要自己进行一些配置。

js
// 如果使用 nginx 作为 web 服务器,可以使用以下代码在控制台进行页面切换,不过一刷新就会404
window.history.pushState({}, '', '/about') // 显示 about 界面

window.history.pushState({}, '', '/') // 显示 home 界面

window.history.pushState({}, '', '/abc') // 显示 NotFound404 界面

以 nginx 为例,可以将配置文件进行一下修改,在找不到页面的时候还是返回 index.html 界面:

默认如下:
#error_page  404              /404.html;

修改为如下即可:
error_page  404              /index.html;

京ICP备18043750号