前端路由和SPA(单页应用)原理解析和实现

1,578 阅读4分钟

前端路由

路由的概念原本来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。

在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)

传统路由模式

每一次页面跳转的时候,后台服务器都会给返回一个新的html文档。 比如下面的例子

html 文档

<!-- home页面 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <ul>
    <li><a href='/home'>home</a></li>
    <li><a href='/about'>about</a></li>

    <div id="routeView">
        Home
    </div>
  </ul>
</body>
</html>


<!-- About页面 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <ul>
    <li><a href='/home'>home</a></li>
    <li><a href='/about'>about</a></li>

    <div id="routeView">
        About
    </div>
  </ul>
</body>
</html>

服务端配置

这里采用nodejs ,KOA框架

const Koa = require("koa");
const Router = require("koa-router");
const path = require("path");
const views = require("koa-views");
let app = new Koa();
let router = new Router();
//home.html 和 about.html 放置在 views 文件加下
app.use(views(path.join(__dirname, "views/"), { extension: "html" }));
//路由匹配规则
router.get("/home", index);
router.get("/", index);
router.get("/about", about);

//对应的渲染函数
async function index(ctx) {
  await ctx.render("home");
}
async function about(ctx) {
  await ctx.render("about");
}
app.use(router.routes());
app.listen(3333);

image.png

image.png

以上便是传统的路由跳转实现,需要服务端配合,每次路径改变时后端会去匹配,成功后返回一个新的html文档,因此也叫做多页应用。

前端路由实现

不同于传统路由,前端路并不由服务端控制,url改变时并不刷新页面,渲染的仍然是同一个html文件,因此也叫做SPA应用。(Single Page Application)

  • 前端路由实现的要点
    • 如何改变 URL 却不引起页面刷新?
    • 如何检测 URL 变化了? 下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。

Hash 模式

  • hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新
  • 通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:通过浏览器前进后退改变 URL、通过<a>标签改变 URL、通过window.location改变URL,这几种情况改变 URL 都会触发 hashchange 事件

html部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <ul>
    <!-- 定义路由 -->
    <li><a href="#/home">home</a></li>
    <li><a href="#/about">about</a></li>

    <!-- 渲染路由对应的 UI -->
    <div id="routeView"></div>
  </ul>
</body>
</html>

js部分

// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('hashchange', onHashChange)

// 路由视图
let routerView = null

function onLoad () {
  routerView = document.querySelector('#routeView')
  onHashChange()
}


// 路由变化时,根据路由渲染对应 UI
function onHashChange () {
  switch (location.hash) {
    case '#/home':
      routerView.innerHTML = 'Home'
      break
    case '#/about':
      routerView.innerHTML = 'About'
      break
    default:
      break
  }
}

从以上的例子中可以看到,通过监听页面的hash(url)变化,可以通过js动态的渲染视图内容。

image.png

image.png

history模式

这种类型是通过html5的最新history api来实现的。

  • history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
  • history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现

html部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <ul>
    <li><a href='/history/home'>home</a></li>
    <li><a href='/history/about'>about</a></li>

    <div id="routeView"></div>
  </ul>
</body>

</html>

js部分:

window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('popstate', onPopState)

// 路由视图
let routerView = null

function onLoad () {
  routerView = document.querySelector('#routeView')
  onPopState()

  // 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
  const linkList = document.querySelectorAll('a[href]')
  linkList.forEach(el => el.addEventListener('click', function (e) {
    e.preventDefault()
    history.pushState(null, '', el.getAttribute('href'))
    onPopState()
  }))
}

// 路由变化时,根据路由渲染对应 UI
function onPopState () {
  switch (location.pathname) {
    case '/history/home':
      routerView.innerHTML = 'Home'
      return
    case '/history/about':
      routerView.innerHTML = 'About'
      return
    default:
      return
  }
}

image.png

image.png 和普通的url一样,但是也有缺点 ,就是一刷新页面 页面就会丢,

因为只要刷新 这个url就会请求服务器,然而服务器上根本没有这个资源,所以就会报404,解决方案就需要配置一下服务器端

const Koa = require("koa");
const Router = require("koa-router");
const path = require("path");
const views = require("koa-views");
let app = new Koa();
let router = new Router();
app.use(views(path.join(__dirname, "views/"), { extension: "html" }));
//正确匹配 http://localhost:8888/history,渲染html
router.get("/history", index)

async function index(ctx) {
  await ctx.render("history");
}

app.use(router.routes());
//如果没有匹配到 比如 http://localhost:8888/history/home,则又返回初始页面(首页)
app.use(async (ctx,next)=>{
    await next();
    if(parseInt(ctx.status)==404){
        ctx.response.redirect("/history")
    }
})

app.listen(8888);

小结

以上通过 hash模式和histroy 简单介绍了前端路由的原理和实现方法,均是原生写法,不依赖任何框架。 下次我们简单探讨一下vue-router的实现原理