路由
首发于: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 路由
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使用的一个演示:
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 路由
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 服务器,那么需要自己进行一些配置。
// 如果使用 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;