Skip to content

前端路由

元信息

  • 目标:掌握"URL 变了、页面没刷新"的原理——路由即状态;能用 hash 与 History API 两种机制实现可收藏、可分享的客户端路由,并知晓 history 模式的服务端改造
  • 关键概念:路由、路由表、静态路由、动态路由、hash、onhashchange、History API、pushState、popstate
  • 关联阶段:cp3
  • 常见误区:以为 URL 一变就会触发服务端请求(hash 与 pushState 改动不会刷新页面);以为链接只写 onclick 不写 href 也行(丢失可收藏与分享能力);以为 history 模式无需服务端配合(直接访问深链会命中服务端路由,须统一重定向回首页)

什么是路由 route?

指符号到实体、地址到实物之间的对应或映射关系,典型例子如ip地址到主机的映射

  • route: 路由项,即一个映射规则
  • router:
    • 路由表:作为一组相关路由项的集合容器
    • 路由器:管理路由项、执行路由转换的设备

服务端路由

由服务端定义,将URL映射到服务端资源的路由,即当用户在浏览器中访问url时,将触发一个服务端请求和获得一个服务端响应,一般有两种定义方式:

  • Web服务器(如Nginx)定义:一般是页面或文件等静态资源的简单映射,即静态路由
  • 服务端脚本定义:如php\jsp\asp等,功能更灵活强大,可实现动态路由,可对应于访问服务器上的任何资源,如文件、数据库、进程等

静态路由与动态路由

  • 静态路由:一对一映射关系,代表服务器端的静态内容或资源,形如 x.com/posts/1.html
  • 动态路由:一对多映射关系,通过查询参数等代表服务器端的动态内容或资源, 形如x.com/posts/show.php?id=[id]

客户端路由:隐式定义

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

上述的两个a链接,相当于两个客户端路由,当它们被触发时,不会触发服务端请求,而是触发客户端的脚本

  • 文本为 index<a>元素 =》一个内联匿名函数
  • 文本为 show<a>元素 =》一个show()函数,该函数接受一个动态的id参数

隐式定义问题是:一改变了a元素的语义和功能;二无法被分享和收藏

客户端路由:显式声明

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>
  • 给每一个动态页面或客户端函数显式命名或编址,当用户访问该地址时,即渲染对应的动态页面或调用对应的客户端函数
  • 有了地址,就可以支持浏览器收藏、上一步、下一步等Web传统交互方式
    • 可收藏:意味着可生成分享的链接,可以实现互联、可以恢复到应用的任一状态,这是Web应用相比于桌面应用的最大优势
  • 以路由为中心,利于代码的组织,如模块化、可重用等

客户端路由的实现机制:基于Hash

http://x.com/index.html为例,浏览器的行为是:

  • 如果该url的路径(path)部分发生变化,譬如url改变为:http://x.com/index.html?key=sport,则浏览器将视其为一个新的网络地址,即向服务端发起新的http请求,这意味着刷新当前的页面
    • 由于发起了新的http请求,需要由服务端来处理,故这类路由一般默认为服务端路由
  • 但如果改变的是锚点#之后内容,譬如上述url改变为http://x.com/index.html#posts/show/32,因为#的语义是页内寻址,浏览器视为保持当前页面不变,即不会向服务端发送新的请求从而导致页面被刷新
    • 由于保持当前页面不变,仍由客户端来处理,则可以利用hash的这一特性来实现客户端路由

基于hash的实现

锚点#之后的内容称之为hash,浏览器提供onhashchange事件以监听hash的变化,则只需提供事件处理函数如下:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

基于hash的路由实例

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

使用示例

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

客户端路由的实现机制:基于History API

在 HTML5 规范中,提供了增强的 History API , 无需#的后缀,而直接将URL改造为前端路由

  • history.pushState(state,title,url); // 添加新的状态到历史状态栈
  • history.replaceState(state,title,url); // 用新的状态代替当前状态

开发者使用上述API,管理浏览器的浏览历史记录,并且地址栏里的URL变化不会导致浏览器触发服务端请求和页面刷新

  • 浏览器的默认交互操作,如上一步、下一步、刷新等都会触发history state的出栈或入栈
  • 提供 onpopstate 事件,监听history state的变化

详情参考 https://developer.mozilla.org/zh-CN/docs/Web/API/History

基于history的路由示例

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

相关库 https://github.com/mjackson/history

路由实现技术比较

  • hash 兼容低版本浏览器,且不会与服务端路由发生冲突,但URL稍显冗长
  • history API 更符合URL格式,
    • 开发人员需要显式地拦截所有的链接事件,将事件处理代码与history state进行关联
    • 可能与服务端路由冲突,需要对服务端进行改造

history API模式下的路由冲突与改造

假设服务端路由为:

  • x.com/ => index.html
  • x.com/posts => posts/index.php

客户端路由为:

  • x.com/posts => function postsIndex(){}
  1. 初始访问/时,浏览器将访问服务端的index.html,该文档会加载客户端路由的相关js文件。此时,当点击/posts链接时,由于click事件被客户端路由拦截,它不会触发服务端请求
  2. 但如果将/posts分享或收藏时,即直接在浏览器中访问/posts,则显然会触发服务端请求posts/index.php,这对于单页应用而言,可能是个错误
  3. 解决办法是,在服务端做改造,即拦截所有除/之外的服务端路由,将其统一重定向到/

简单的使用办法是使用http-server-spa 包,它默认会支持上述解决办法,无需单独配置

前端路由库 director.js

https://github.com/flatiron/director 是一个简单的 client-side router库,支持基于hash技术的路由,方便处理对hash的监听和解析,使得代码更清晰、易用

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

详见 https://www.cnblogs.com/Showshare/p/director-chinese-tutorial.html

前端路由库 page.js

page.js 支持基于History api的客户端路由,并且提供了诸如重定向,页面刷新等实用功能

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

需要配合 http-server-spa 使用

路由库与例子

扩展阅读