总所周知,随着前端应用的业务功能起来越复杂,用户对于使用体验的要求愈来愈高,单面(SPA
)成为前端应用的主流形式。而大型单页应用最显著特色之一就是采用的前端路由跳转子页面系统,经过改变页面的URL
,在不从新请求页面的状况下,更新页面视图。javascript
更新视图可是浏览器不从新渲染整个页面,只是从新渲染部分子页面,加载速度快,页面反应灵活,这是 SPA
的优点,这也是前端路由原理的核心,这会给人一种仿佛在操做 APP
同样的感受,目前在浏览器环境中实现这一功能的方式主要有两种:css
URL
的 hash(#)
H5
新增方法 History interface
URL
的Hash(#)
在 H5
尚未流行开来时,通常 SPA
都采用 url
的 hash(#)
做为锚点,获取到 # 以后的值,并监听其改变,再进行渲染对应的子页面。网易云音乐官网就是利用的此技术。html
例如,你的地址为http://localhost:8888/#/abc
那么利用 location.hash
输出的内容就为 #/abc
。前端
那么我就先从 location
这个对象提及。vue
先来看看location
的官方属性有哪些java
属性 | 描述 |
---|---|
hash | 设置或返回从 # 开始的 URL (锚) |
host | 设置或返回主机名和当前 URL 的端口号 |
hostname | 设置或返回当前 URL 的主机名 |
href | 设置或返回完整的 URL |
pathname | 设置或返回当前 URL 的路径部分 |
port | 设置或返回当前 URL 的端口号 |
protocol | 设置或返回当前 URL 的协议 |
search | 设置或返回从 ? 开始的 URL 部分 |
由上表格能够知道,咱们能够轻易的获取到 # 以后的部分,那么拿到这个部分咱们怎么监听其变化以及对应的子页面进行改变呢?jquery
window
对象中有一个事件是专门监听hash
的变化,那就是onhashchange
,首先咱们须要监听此事件:git
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script> window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
复制代码
可见此时咱们已经彻底监听到了 URL
的变化,页面上的内容也对应改变了。 那么,该如何载入不一样的页面呢,目前来讲有三种方式:github
import
一个 JS
文件,文件内部 export
模版字符串AJAX
加载对应的 HTML
模版第一种方式已经演示过,不过这种方式局限性太大,下面我会演示另外两种方式加载页面。ajax
import
方式定义一个 JS
文件,名为 demo1.js
,在里面输入内容:
const str = ` <div> 我是import进来的JS文件 </div> `
export default str
复制代码
在主文件里 import
进来,并进行测试(使用 Chrome
必定要使用服务器开启,或者直接用火狐打开):
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<!-- 在 HTML 导入文件记得要加上 type="module" -->
<script type="module"> import demo1 from './demo1.js' document.querySelector('#id').innerHTML = demo1 window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
复制代码
可见导入文件已经生效,目前大部分框架编译事后是采用相似此种方式处理。
例如,vue
框架,.vue
文件是一个自定义的文件类型,用类 HTML
语法描述一个 Vue
组件。每一个 .vue
文件包含三种类型的顶级语言块 <template>
, <script>
和 <style>
,vue-loader
会解析文件,提取每一个语言块,若有必要会经过其它 loader
处理,最后将他们组装成一个 CommonJS
模块,module.exports
出一个 Vue.js
组件对象。。
AJAX
方式本篇文章是详解路由机制,AJAX
就直接采用 JQuery
这个轮子。
定义一个 HTML
文件,名为 demo2.html
,在里面写入一些内容(因为主页面已经有head
,body
等根标签,此文件只需写入须要替换的标签):
<div>
我是AJAX加载进来的HTML文件
</div>
复制代码
咱们在主文件里写入,并进行测试:
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="module"> // import demo1 from './demo1.js' // document.querySelector('#id').innerHTML = demo1 $.ajax({ url: './demo2.html', success: (res) => { document.querySelector('#id').innerHTML = res } }) window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
复制代码
可见,利用 AJAX
加载进来的文件也已经生效。
既然加载不一样页面的内容都已经生效,那么只须要包装一下咱们的监听,利用观察者模式封装路由的变化:
<body>
<h1 id="id">我是空白页</h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script type="module"> import demo1 from './demo1.js' // 建立一个 newRouter 类 class newRouter { // 初始化路由信息 constructor() { this.routes = {}; this.currentUrl = ''; } // 传入 URL 以及 根据 URL 对应的回调函数 route(path, callback = () => {}) { this.routes[path] = callback; } // 切割 hash,渲染页面 refresh() { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl] && this.routes[this.currentUrl](); } // 初始化 init() { window.addEventListener('load', this.refresh.bind(this), false); window.addEventListener('hashchange', this.refresh.bind(this), false); } } // new 一个 Router 实例 window.Router = new newRouter(); // 路由实例初始化 window.Router.init(); // 获取关键节点 var content = document.querySelector('#id'); Router.route('/id1', () => { content.innerHTML = 'id1' }); Router.route('/id2', () => { content.innerHTML = demo1 }); Router.route('/id3', () => { $.ajax({ url: './demo2.html', success: (res) => { content.innerHTML = res } }) }); </script>
复制代码
效果以下:
至此,利用 hash(#)
进行前端路由管理都已实现。
H5
新增方法 History interface
上面使用的 hash
法实现路由当然不错,可是问题就是实在太丑~ 若是在微信或者其余不显示 URL
的 APP
中使用,倒也无所谓,可是若是在通常的浏览器中使用就会遇到问题了。
由此,H5
的 History
模式,解决了这一问题。
在 H5
以前, History
仅仅只有一下几个 API
:
API | 说明 |
---|---|
back() |
回退到上次访问的 URL (与浏览器点击后退按钮相同) |
forward() |
前进到回退以前的 URL (与浏览器点击向前按钮相同) |
go(n) |
n 接收一个整数,移动到该整数指定的页面,好比go(1) 至关于forward() ,go(-1) 至关于 back() ,go(0) 至关于刷新当前页面 |
若是移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败。
然而,到了 H5
的时代,新的 H5
则赋予了其更多的新特性:
默认状况下,浏览器会缓存当前会话页面,这样当下一个页面点击后退按钮,或前一个页面点击前进按钮,浏览器便会从缓存中提取并加载此页面,这个特性被称为“往返缓存”。
PS: 此缓存会保留页面数据、DOM和js状态,其实是将整个页面无缺完好地保留。
浏览器支持度: IE10+
JS
对象(不大于640kB),主要用于在 popstate
事件中做为参数被获取。若是不须要这个对象,此处能够填 null
null
url
必须与当前 url
处于同一个域,不然将抛出异常,此参数若没有特别标注,会被设为当前文档 url
栗子:
// 如今是 localhost/1.html
const stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
// 浏览器地址栏将当即变成 localhost/2.html
// 但!!!
// 不会跳转到 2.html
// 不会检查 2.html 是否存在
// 不会在 popstate 事件中获取
// 不会触发页面刷新
// 这个方法仅仅是添加了一条最新记录
复制代码
除此以外,仍有几点须要注意:
url
设为锚点值时不会触发 hashchange
XSS
、 CSRF
等攻击方式浏览器支持度: IE10+
pushstate
popstate
浏览器支持度: IE10+
state
。定义:每当同一个文档的浏览历史(即 history
对象)出现变化时,就会触发 popstate
事件。
注意:若仅仅调用 pushState
方法或 replaceState
方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript
调用 back
、 forward
、 go
方法时才会触发。另外,该事件只针对同一个文档,若是浏览历史的切换,致使加载不一样的文档,该事件也不会触发。
栗子:
window.onpopstate= (event) => {
  console.log(event.state) //当前历史记录的state对象
}
复制代码
了解了这么多内容,那么就让咱们开始实现 History
模式的路由吧!
咱们将上面的 HTML
稍稍改造下,请你们耐心分析:
<body>
<h1 id="id">我是空白页</h1>
<a class="route" href="/id1">id1</a>
<a class="route" href="/id2">id2</a>
<a class="route" href="/id3">id3</a>
</body>
复制代码
import demo1 from './demo1.js'
// 建立一个 newRouter 类
class newRouter {
// 初始化路由信息
constructor() {
this.routes = {};
this.currentUrl = '';
}
route(path, callback) {
this.routes[path] = (type) => {
if (type === 1) history.pushState( { path }, path, path );
if (type === 2) history.replaceState( { path }, path, path );
callback()
};
}
refresh(path, type) {
this.routes[this.currentUrl] && this.routes[this.currentUrl](type);
}
init() {
window.addEventListener('load', () => {
// 获取当前 URL 路径
this.currentUrl = location.href.slice(location.href.indexOf('/', 8))
this.refresh(this.currentUrl, 2)
}, false);
window.addEventListener('popstate', () => {
this.currentUrl = history.state.path
this.refresh(this.currentUrl, 2)
}, false);
const links = document.querySelectorAll('.route')
links.forEach((item) => {
// 覆盖 a 标签的 click 事件,防止默认跳转行为
item.onclick = (e) => {
e.preventDefault()
// 获取修改以后的 URL
this.currentUrl = e.target.getAttribute('href')
// 渲染
this.refresh(this.currentUrl, 2)
}
})
}
}
// new 一个 Router 实例
window.Router = new newRouter();
// 实例初始化
window.Router.init();
// 获取关键节点
var content = document.querySelector('#id');
Router.route('/id1', () => {
content.innerHTML = 'id1'
});
Router.route('/id2', () => {
content.innerHTML = demo1
});
Router.route('/id3', () => {
$.ajax({
url: './demo2.html',
success: (res) => {
content.innerHTML = res
}
})
});
复制代码
演示图以下所示:
通常场景下,hash
和 history
均可以,除非你更在乎颜值,#
符号夹杂在 URL
里看起来确实有些不太美丽。 另外,根据 Mozilla Develop Network 的介绍,调用 history.pushState()
相比于直接修改 hash
,存在如下优点:
pushState()
设置的新 URL
能够是与当前 URL
同源的任意 URL
;而 hash
只可修改 #
后面的部分,所以只能设置与当前 URL
同文档的 URL
pushState()
设置的新 URL
能够与当前 URL
如出一辙,这样也会把记录添加到栈中;而 hash
设置的新值必须与原来不同才会触发动做将记录添加到栈中pushState()
经过 stateObject
参数能够添加任意类型的数据到记录中;而 hash
只可添加短字符串;pushState()
可额外设置 title
属性供后续使用。这么一看 history
模式充满了 happy,感受彻底能够替代 hash
模式,但其实 history
也不是样样都好,虽然在浏览器里游刃有余,但真要经过 URL
向后端发起 HTTP
请求时,二者的差别就来了。尤为在用户手动输入 URL
后回车,或者刷新(重启)浏览器的时候。
hash
模式下,仅 hash
符号以前的内容会被包含在请求中,如 http://www.qqq.com
,所以对于后端来讲,即便没有作到对路由的全覆盖,也不会返回 404
错误。history
模式下,前端的 URL
必须和实际向后端发起请求的 URL
一致,如 http://www.qqq.com/book/id
。若是后端缺乏对 /book/id
的路由处理,将返回 404
错误。Vue-Router
官网里如此描述:“不过这种模式要玩好,还须要后台配置支持……因此呢,你要在服务端增长一个覆盖全部状况的候选资源:若是 URL
匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app
依赖的页面。”Apache
或 Nginx
)进行简单的路由配置,同时搭配前端路由的 404
页面支持。最后很差意思推广一下我基于 Taro
框架写的组件库:MP-ColorUI。
能够顺手 star 一下我就很开心啦,谢谢你们。