如何监听路由变化?SPA实现原理及DEMO

4,234 阅读5分钟

本篇文章主要讲解常见单页应用路由库的实现思路。通过其实现方法我们可以了解到一些浏览器的工作原理,并且深入学习理解一些平时不常用的API。

由于业务需要,我们要监控页面的访问次数。对于传统的多页应用来说,很简单,我们只需要在页面加载完毕后上报就可以了。但是对于单页应用来说就没那么简单了。首先我们要去搞清楚单页应用的一些原理,了解它是怎么实现的,以及我们如何去监听页面的变化。

SPA & MPA

SPA

SPA(Single Page Applications),单页应用。指的是应用的不同模块/功能之间的切换都是在同一页面里面的。

通常不同模块之间的URL的hash会不一样。比如https://test.com/#/loginhttps://test.com/#/user

MPA

MAP(Multiple Page Applications),多页应用。指的是应用的不同模块/功能都是不同页面之间呈现的。

通常不同页面都是不同的html,比如https://test.com/login.htmlhttps://test.com/user.html

区别

一般单页应用拥有更好的用户体验,更快的页面切换速度。而多页应用的SEO更友好,拥有更快的首页加载速度。

SPA的实现原理

了解了单页应用之后,我们可以明确一点,就是单页应用的路由切换,是在同一页面下进行的。也就是说浏览器不会刷新/跳转页面。 但是它的URL确实是变化了(hash改变了)。也正是因为hash变化不会引起浏览器的刷新行为,所以SPA才依赖于它。

1. 直接改变hash

所以第一个我们能自己想到的SPA的实现方法就是手动更改hash,然后监听hash变化改变路由视图。

原理

window.addEventListener('hashchange', renderView)

window.location.hash = '/login'

DIY

<div>
  <a href="#/login">登录</a>
  <a href="#/about">关于</a>
</div>
<div class="view"></div>
const $view = document.querySelector(".view");

const routes = {
  "/login": "LOGIN",
  "/about": "ABOUT",
};

window.addEventListener("hashchange", (e) => {
  const route = getHash(e.newURL);
  if (route) {
    renderView(route);
  }
});

function renderView(route) {
  const content = routes[route];
  $view.innerHTML = content;
}

function getHash(url) {
  const href = window.location.href;
  const i = href.indexOf("#");

  const hash = i >= 0 ? href.slice(i + 1) : "";

  return hash;
}

2. pushState

原理

当然直接修改hash已经能很好的实现SPA了,但是我们还能做得更好,更elegant,那就是使用浏览器为我们提供的更强大的方法,也就是HistoryAPI。

history.pushStatehistory.replaceState都可以用来改变浏览器的URL而不造成刷新。

DIY

我们只需改变部分代码

<div>
  <a href="/login">登录</a>
  <a href="/about">关于</a>
</div>
<div class="view"></div>
const $links = document.querySelectorAll("a");

$links.forEach((element) => {
  element.addEventListener("click", (e) => {
    e.preventDefault();

    const path = e.target.getAttribute("href");
    renderView(path);
    window.history.pushState({}, "", "#" + path);
  });
});

注意

这里需要注意的是,pushStatereplaceState不会触发hashchangepopstate事件。也就是我们需要在pushState/replaceState的同时去更改视图。

3. 区别

不管是hash改变还是pushstate,都会向历史记录里插入一条记录。

但还有很多不同点:

  • 使用 history.pushState() 可以改变referrer
  • 使用 history.pushState() 可以改变任意同源URL而不局限于hash值。 也就是说你在https://test.com/foo.html可以调用history.pushState(null, '', '/bar.html')将URL改为https://test.com/bar.html而不刷新页面,也不会去请求/bar.html(假设其不存在)。有什么用呢?你可以看到这样的URL已经和MPA的URL一样了,这很有利于网站的SEO。但是如果用户在https://test.com/bar.html刷新页面,那就糟了,因为根本不存在bar.html这个资源。所以这也是为什么如果SPA要使用这种URL展示模式需要后端的支持。服务器需要把所有页面都代理到入口页面https://test.com/foo.html上去。再由入口页面做路由跳转。
  • 使用 history.pushState(state, title, url) 可以传入一个状态对象state。 也就是你可以附加一些数据到跳转的页面上,而hash模式的话你需要将数据放到URL上。

Vue Router实现原理

首先可以确定的一点的是,它的无刷新路由切换最基础的支持就是我们上面提到的hash和history两种实现方式。

这里我们简单扼要的介绍下它是如何改变视图的。

// ...
this._router = this.$options.router
// ...
Vue.util.defineReactive(this, '_route', this._router.history.current)

首先来看下这两行代码,用过VueRouter的都知道this.$options.router就是VueRouter的实例。 但是this._router.history.current表示的是什么呢?它表示的就是当前路由,在每次你this.$router.push/this.$router.replace的时候,current都会更新。

最关键的是这里新定义了一个响应式属性_route。当响应式属性更新时,依赖这个属性的组件都会更新。

你该想到了吧,RouterView就依赖了这个属性_route。它会根据_route的改变而更新组件渲染内容(重新执行render函数)。而_route又会对应有当前路由匹配的组件,这些匹配的组件就是RouterView要渲染的内容。

可以说VueRouter将Vue的响应式设计运用得十分到位了。

同样的原理,也适用于ReactRouterDom上。

如何监听

实现是知道了,那么我们咋监听这玩意?还没有个statechange事件!hashchange倒是有,但是我们需要更完善的解决方案。

不能监听变化,我们只能拦截调用了。就像最开始学习window.onload时使用的方法。

window.onload = () => {
  console.log("1");
};

const _onload = window.onload;
window.onload = (e) => {
  _onload(e);
  console.log(2);
};

不过现在我们有更高级的方法Proxy,以下是一个简单示例。

history.pushState = new Proxy(history.pushState, {
  apply: function (target, thisBinding, args) {
    console.log('就这?');
    return target.apply(thisBinding, args);
  },
});

同样的方法,我们还可以代理consolexhr。从而监听网页上的任何活动!

缺点是Proxy是ES6内容,只适用于现代浏览器。古老浏览器需要polyfill。

历史精选

  1. 如何在10分钟之内完成一个业务页面 - Vue的封装艺术
  2. 新手也能看懂的虚拟滚动实现方法
  3. Axios源码分析

原文-我的小破站