原文链接:

前端路由跳转基本原理

前述

前端三大框架AngularReactVue都推行单页面应用SPA开发模式,这是因为在路由切换时,替换DOM Tree中发生修改的DOM部分,来减少原来因为多页面应用跳转带来巨大的性能损耗。

他们都有自己典型的路由解决方案:@Angular/router、react-router、vue-router

一般来说,这些路由插件总是提供俩种不同的路由方式:HashHistory,有时候也会提供非浏览器环境下的路由方式Abstract,在vue-router中使用外观模式将不同的几种路由方式提供了一个一致的高层接口,让我们可以在不同路由方式中切换。

Hash 和 History 除了外观上的不同之外。还有一个重要的区别:Hash方法的状态保存需要另行传递,而HTML5 History原生提供了自定义状态传递的能力,我们可以直接利用其来传递信息。

1.Hash

1.1 相关api

MDN:Location

BOM中的location对象

MDN上的例子:

var url = document.createElement('a');
url.href = 'https://developer.mozilla.org/en-US/search?q=URL#search-results-close-container';
console.log(url.href); // https://developer.mozilla.org/en-US/search?q=URL#search-results-close-container
console.log(url.protocol); // https:
console.log(url.host); // developer.mozilla.org
console.log(url.hostname); // developer.mozilla.org
console.log(url.port); // (blank - https assumes port 443)
console.log(url.pathname); // /en-US/search
console.log(url.search); // ?q=URL
console.log(url.hash); // #search-results-close-container
console.log(url.origin); // https://developer.mozilla.org

注意: Hash方法利用了相当于页面锚点的功能,所以与原来的通过锚点定位来进行页面滚动定位的方法冲突,导致定位到错误的路由路径,因此需要采用别方法。

1.2 实例

原理是吧目标路由和对应的回调记录下来,点击跳转触发hashchange的时候获取当前路径并执行对应的回调,效果:

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<title>Document</title>
</head> <body>
<ul>
<li><a href="#/">/</a></li>
<li><a href="#/page1">/page1</a></li>
<li><a href="#/page2">/page2</a></li>
</ul>
<div class="content-div"></div>
<script>
// 创建路由类
class RouterClass {
constructor() {
this.routes = {} // 记录路径标识符对应的 cb
this.currentUrl = '' // 记录hash只为方便执行 cb
window.addEventListener('load', () => this.render())
window.addEventListener('hashchange', () => this.render())
} // 初始化
static init() {
window.Router = new RouterClass()
} // 注册路由 和 回调 @param - path - 路径
// @param - cb - 回调
route(path, cb) {
// 将路径及其对应的方法添加到 this.routes 对象中
this.routes[path] = cb || function() {}
} // 记录当前 hash,执行cb
render() {
this.currentUrl = location.hash.slice(1) || '/'
this.routes[this.currentUrl]() // 默认页面
}
} // 调用方法,监听 load 和 hashchange 事件
RouterClass.init()
// 过去div 并 给div中添加数据
const ContentDom = document.querySelector('.content-div')
const changeContent = content => ContentDom.innerHTML = content // 调用方法
Router.route('/', () => changeContent('默认页面'))
Router.route('/page1', () => changeContent('page1页面'))
Router.route('/page2', () => changeContent('page2页面'))
</script>
</body> </html>

如果希望使用脚本来控制Hash路由的后退,可以将经历的路由路径记录下来,路由后退跳转的实现是对location.hash进行赋值。但是这样会引发重新hashchange事件,第二次进入render。所以我们需要增加一个标志位,来标明进入render方法是因为回退进入的还是用户跳转进入的。

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<title>Document</title>
</head> <body> <ul>
<li><a href="#/">/</a></li>
<li><a href="#/page1">page1</a></li>
<li><a href="#/page2">page2</a></li>
</ul>
<div class='content-div'></div> <button>back</button> <script>
class RouterClass {
constructor() {
this,isBack = false
this.routes = {} // 记录路径标识符对应的cb
this.currentStack = [] // hash 栈
window.addEventListener('load', () => this.render())
window.addEventListener('hashchange', () => this.render())
} // 初始化
static init() {
window.Router = new RouterClass()
} // 记录 path 对应的 cb 和 cb 的 回调
route(path, cb) {
this.routes[path] = cb || function() {}
} // 入栈当前hash,执行 cb 跳转页面
render() {
if(this.isBack) { // 如果是由backoff进入,则置false之后return
this.isBack = false
return
}
this.currentUrl = location.hash.slice(1) || '/'
this.historyStack.push(this.currentUrl)
this.routes[this.currentUrl]()
} // 路由后退
back() {
this.isback = true
this.historyStack.pop() // 移除当前 hash, 回退到上一个
const { length } = this.historyStack
if(!length) return
let prev = this.historyStack[length -1] // 拿到要回退到的目标hash
location.hash = `#${ prev }`
this.currentStack = prev
this.routes[prev]() // 执行对应cb
}
}
RouterClass.init()
const BtnDom = document.querySelector('button')
const ContentDom = document.querySelector('.content-div')
const changeContent = content => ContentDom.innerHTML = content Router.route('/', () => changeContent('默认页面'))
Router.route('/page1', () => changeContent('page1页面'))
Router.route('/page2', () => changeContent('page2页面')) // bind() 可以改变函数内部的this的指向,并返回一个新的函数,你必须调用它才会被执行
BtnDom.addEventListener('click', Router.back.bind(Router), false)
</script>
</body> </html>

2. HTML5 History Api

2.1 相关 Api

window.history.back(); // 这和用户点击浏览器回退按钮的效果相同
window.history.forward(); // 向前跳转
window.history.go(n); // 跳转到 history 中指定的一个点
window.history.go(-1); // 向后移动一个页面 (等同于调用 back())
window.history.go(1); // 向前移动一个页面, 等同于调用了 forward()
window.history.length; // 长度属性的值来确定的历史堆栈中页面的数量

添加和修改历史记录中的条目

 history.pushState() // 追加一条新的历史记录
 // history.pushState('状态对象:历史记录的标题', '标题:历史记录的描述', url)
 
 history.replaceState() // 替换当前的历史记录为一条新的记录
 // history.replaceState('历史记录的标题', '历史记录的描述', url)
 
 // window.onpopstate 事件 历史切换事件

HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。

pushState() 方法的例子

假设在http://mozilla.org/foo.html中执行了以下js代码:

let stateObj = {
foo: "bar",
}; history.pushState(stateObj, "page 2", "bar.html");

这将使浏览器地址栏显示为`http://mozilla.org/bar.html,但是并不会导致浏览器加载bar.html,甚至不会检查bar.html是否存在。

假设用户又访问了http://google.com,然后点击了返回按钮,此时地址栏现实的是`http://mozilla.org/bar.htmlhistory.state中包含了stateObj的一份拷贝。页面此时展现为bar.html。切页面被重新加载了,所以popstate事件将不会被触发。

如果我们再次点击返回按钮,页面URL会变为http://mozilla.org/foo.html,文档对象document会触发另外一个 popstate 事件,这一次的事件对象state object为null。 这里也一样,返回并不改变文档的内容,尽管文档在接收 popstate 事件时可能会改变自己的内容,其内容仍与之前的展现一致。

replaceState() 方法的例子

history.replaceState() 的使用与 history.pushState() 非常相似,区别在于  replaceState()  是修改了当前的历史记录项而不是新建一个。 注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。

假设http://mozilla.org/foo.html执行了如下JavaScript代码:

let stateObj = {
foo: "bar",
}; history.pushState(stateObj, "page 2", "bar.html");

然后,假设http://mozilla.org/bar.html执行了如下 JavaScript:

history.replaceState(stateObj, "page 3", "bar2.html");

这将会导致地址栏显示http://mozilla.org/bar2.html,但是浏览器并不会去加载bar2.html 甚至都不需要检查 bar2.html 是否存在。

假设现在用户重新导向到了http://www.microsoft.com,然后点击了回退按钮。这里,地址栏会显示http://mozilla.org/bar2.html。假如用户再次点击回退按钮,地址栏会显示http://mozilla.org/foo.html,完全跳过了bar.html。

popstate 事件

每当活动的历史记录项发生变化时, popstate 事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝。

使用示例请参见 window.onpopstate

获取当前状态

页面加载时,或许会有个非null的状态对象。这是有可能发生的,举个例子,假如页面(通过pushState()replaceState() 方法)设置了状态对象而后用户重启了浏览器。那么当页面重新加载时,页面会接收一个onload事件,但没有 popstate 事件。然而,假如你读取了history.state属性,你将会得到如同popstate 被触发时能得到的状态对象。

你可以读取当前历史记录项的状态对象state,而不必等待popstate 事件, 只需要这样使用history.state 属性:

let currentState = history.state;

2.2 实例

将之前的例子改造一下,在需要路由跳转的地方使用 history.pushState 来入栈并记录 cb

前进后退的时候监听 popstate 事件拿到之前传给 pushState 的参数并执行对应 cb,因为借用了浏览器自己的 Api。

<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>h5 router</title>
</head> <body>
<ul>
<li><a href="/">/</a></li>
<li><a href="/page1">page1</a></li>
<li><a href="/page2">page2</a></li>
</ul>
<div class='content-div'></div>
<script>
class RouterClass {
constructor(path) {
this.routes = {} // 记录路径标识符对应的cb
history.replaceState({ path }, null, path)
this.routes[path] && this.routes[path]()
window.addEventListener('popstate', e => {
console.log(e, ' --- e')
const path = e.state && e.state.path
this.routes[path] && this.routes[path]()
})
} /**
* 初始化
*/
static init() {
window.Router = new RouterClass(location.pathname)
} /**
* 记录 path 对应 cb
* @param path 路径
* @param cb 回调
*/
route(path, cb) {
this.routes[path] = cb || function() {}
} /**
* 触发路由对应回调
* @param path
*/
go(path) {
history.pushState({
path
}, null, path)
this.routes[path] && this.routes[path]()
}
} RouterClass.init()
const ul = document.querySelector('ul')
const ContentDom = document.querySelector('.content-div')
const changeContent = content => ContentDom.innerHTML = content Router.route('/', () => changeContent('默认页面'))
Router.route('/page1', () => changeContent('page1页面'))
Router.route('/page2', () => changeContent('page2页面')) ul.addEventListener('click', e => {
if (e.target.tagName === 'A') {
e.preventDefault()
Router.go(e.target.getAttribute('href'))
}
})
</script>
</body> </html>

[VUE HTML5 History 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html#html5-history-%E6%A8%A1%E5%BC%8F)

JS高级学习笔记(9) 之 转:前端路由跳转基本原理的更多相关文章

  1. 转:JS高级学习笔记(8)- JavaScript执行上下文和执行栈

    必看参考: 请移步:博客园 JavaScript的执行上下文 深入理解JavaScript执行上下文和执行栈 JavaScript 深入之执行上下文 写在开头 入坑前端已经 13 个月了,不能再称自己 ...

  2. JS高级学习笔记(6)- 事件循环

    参考文章:深入理解JS引擎的执行机制        JavaScript 异步.栈.事件循环.任务队列 我的笔记:ES系列之Promise async 和 await Event Loop 前提 js ...

  3. JS高级学习笔记(1)- 数据类型及转换规则

    必读: Javascript对象Oject的强制类型转换 JavaScript筑基篇(二)->JavaScript数据类型 聊一聊valueOf和toString 深入理解JavaScript系 ...

  4. JS高级学习笔记(10) 之 js 时怎么解析HTML标签的

    DOM 节点类型 浏览器渲染过程 浏览器是怎么把HTML标签语言和JavaScript联系在一起的,这就是我们常说的DOM. 浏览器中的DOM解析器把HTML翻译成对象(object),然后JavaS ...

  5. JS高级学习笔记(2)之js多线程

    参考大神:Javascript多线程 web worker ---- 6.Web Worker 概述 截图过来: 线程之间的通信 let worker = new Worker(‘js文件路径’) 主 ...

  6. Python高级学习笔记

    Python高级学习笔记,此笔记中包含Linux操作系统.Html+CSS+JS.网络协议等. 所有思维导图为本人亲手所画,请勿用于商用. 大哥们,求点赞哦. 第一天笔记:链接 第二天笔记:链接 第三 ...

  7. js再学习笔记

    #js再学习笔记 ##基本 1.js严格区分大小写   2.js末尾的分号可加,也可不加   3.六种数据类型(使用typeof来检验数据的类型) `typeof` - undefined: `var ...

  8. js高级程序设计笔记之-addEventListener()与removeEventListener(),事件解除与绑定

    js高级程序设计笔记之-addEventListener()与removeEventListener(),事件解除与绑定 addEventListener()与removeEventListener( ...

  9. JS数组学习笔记

    原文:JS数组学习笔记 最近在备课数组,发现很多ES5的方法平时很少用到.细节比较多,自己做了大量例子和整理,希望对大家了解JavaScript中的Array有所帮助. 概念 数组是值的有序集合.每个 ...

随机推荐

  1. OO第三次博客作业(第三单元总结)

    (1)梳理JML语言的理论基础.应用工具链情况 Java 建模语言(JML)将注释添加到 Java 代码中,这样我们就可以确定方法所执行的内容,而不必说明它们如何做到这一点.有了 JML,我们就可以描 ...

  2. Java数组去重的方法

    //第一种方式:最开始想到的是利用Set集合的不可重复性进行元素过滤 public static Object[] oneClear(Object[] arr){  Set set = new Has ...

  3. SpringAOP源码跟踪及学习

    Spring 版本 4.3.2 在拿到 Bean 实例以后,会经历一系列的初始化工作,如:工厂回调.init 方法.后处理器在 Bean 初始化前后的处理等,在一般情况下(非 factory-meth ...

  4. 038、Java中逻辑运算之非运算“!”

    01.代码如下: package TIANPAN; /** * 此处为文档注释 * * @author 田攀 微信382477247 */ public class TestDemo { public ...

  5. 简述DDD,战略设计

    从What.How.Why三个层面进行了梳理. What:DDD是什么?DDD是用来解决软件复杂度的问题,是一种软件思想. Why:为什么DDD可以解决软件复杂度?对于规模造成的复杂度,可以借助限界上 ...

  6. Web application architecture overview

  7. delphi的dbgrid控件点击title排序

    procedure TfrmMain.DBGridEhTitleClick(Column: TColumnEh);var i : integer;begin for i:= 1 to DBGridEh ...

  8. 十、Vue:Vuex实现data(){}内数据多个组件间共享

    一.概述 官方文档:https://vuex.vuejs.org/zh/installation.html 1.1vuex有什么用 Vuex:实现data(){}内数据多个组件间共享一种解决方案(类似 ...

  9. PHPmyadmin Getshell(10.25 第二十七天)

    PHPmyadmin Getshell的方法(1)show global variables like '% secure-file-priv%' 如果该参数设置为空或者指定的文件夹可以利用,然后写木 ...

  10. WTL之VS2013环境搭建

    新版博客已经搭建好了,有问题请访问 htt://www.crazydebug.com 从国外回来,在老家入职了新公司,做c++开发,刚到新公司要熟悉公司的项目代码,目前公司在做一个主播聚合平台,界面采 ...