什么是前端路由?
路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。
路由就是浏览器地址栏中的 url
与所见网页的对应关系
前端路由的实现方式:
基于hash
(ocation.hash+hashchange
事件)
展示层面也就是切换 #
后面的内容,呈现给用户不同的页面。现在越来越多的单页面应用,基本都是基于 hash
实现
特性:
url
中 hash
值的变化并不会重新加载页面hash
值的改变,都会在浏览器的访问历史中增加一个记录,也就是能通过浏览器的回退、前进按钮控制 hash
的切换hashchange
事件,监听到 hash
值的变化,从而响应不同路径的逻辑处理基于istory
新API
(history.pushState()+popState
事件)
window.history.pushState(null, null, "http://www.google.com");
这两个 API
的相同之处是都会操作浏览器的历史记录,而不会引起页面的刷新。不同之处在于,pushState
会增加一条新的历史记录,而 replaceState
则会替换当前的历史记录
详见History API -MDN
在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。
如何实现前端路由?
要实现前端路由,需要解决两个核心:
如何改变 URL 却不引起页面刷新?
如何检测 URL 变化了?
下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。
hash 实现。当 一个窗口的 hash (URL 中 # 后面的部分)改变时就会触发 hashchange 事件
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新
通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:通过浏览器前进后退改变 URL、通过标签改变 URL、通过window.location改变URL,这几种情况改变 URL 都会触发 hashchange 事件
history 实现
HTML5引入了 history.pushState() 和history.replaceState方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate
合使用
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState或标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和
标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
pushState()
需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL. 让我们来解释下这三个参数详细内容:
状态对象 — 状态对象state是一个JavaScript对象,通过pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate事件就会被触发,且该事件的state属性包含该历史记录条目状态对象的副本。
状态对象可以是能被序列化的任何东西。原因在于Firefox将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有640k的大小限制。如果你给 pushState()
方法传了一个序列化后大于640k的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage
以及 localStorage
.
标题 — Firefox 目前忽略这个参数,但未来可能会用到。在此处传一个空字符串应该可以安全的防范未来这个方法的更改。或者,你可以为跳转的state传递一个短标题。
URL — 该参数定义了新的历史URL记录。注意,调用 pushState()
后浏览器并不会立即加载这个URL,但可能会在稍后某些情况下加载这个URL,比如在用户重新打开浏览器时。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState()
会抛出一个异常。该参数是可选的,缺省为当前URL。
在某种意义上,调用 pushState()
与 设置 window.location = "#foo"
类似,二者都会在当前页面创建并激活新的历史记录。但 pushState()
具有如下几条优点:
window.location
仅当你只修改了哈希值时才保持同一个 document
。window.location = "#foo";
在当前哈希不是 #foo
的情况下, 仅仅是新建了一个新的历史记录项。注意 pushState()
绝对不会触发 hashchange
事件,即使新的URL与旧的URL仅哈希不同也是如此。
标题
在之后会被浏览器用到,那么这个数据是可以被使用的(哈希则不然)。history.replaceState()
的使用与 history.pushState()
非常相似,区别在于 replaceState()
是修改了当前的历史记录项而不是新建一个。 注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。
replaceState()
的使用场景在于为了响应用户操作,你想要更新状态对象state或者当前历史记录的URL。
每当处于激活状态的历史记录条目发生变化时,popstate
事件就会在对应window
对象上触发. 如果当前处于激活状态的历史记录条目是由
history.pushState()
方法创建。或者由history.replaceState()方法修改过
的, 则popstate事件对象的
state
属性包含了这个历史记录条目的state对象的一个拷贝。
调用history.pushState()
或者history.replaceState()
不会触发popstate事件. popstate
事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()
方法).
当网页加载时,各浏览器对popstate
事件是否触发有不同的表现,Chrome 和 Safari会触发popstate
事件, 而Firefox不会。
页面加载时,或许会有个非null的状态对象。这是有可能发生的,举个例子,假如页面(通过pushState()
或 replaceState()
方法)设置了状态对象而后用户重启了浏览器。那么当页面重新加载时,页面会接收一个onload事件,但没有 popstate 事件。然而,假如你读取了history.state属性,你将会得到如同popstate 被触发时能得到的状态对象。
你可以读取当前历史记录项的状态对象state,而不必等待popstate
事件, 只需要这样使用history.state
属性:
let currentState = history.state;
原生JS版前端路由实现
基于上节讨论的两种实现方式,分别实现 hash 版本和 history 版本的路由,示例使用原生 HTML/JS 实现,不依赖任何框架。
html代码结构:
js代码:
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('hashchange', onHashChange)
// 路由视图
var routerView = null
function onLoad () {
routerView = document.querySelector('#routeView')
onHashChange()
}
// 路由变化时,根据路由渲染对应 UI
function onHashChange () {
switch (location.hash) {
case '#/home':
routerView.innerHTML = 'Home'
return
case '#/about':
routerView.innerHTML = 'About'
return
default:
return
}
}
html结构:
js代码:
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('popstate', onPopState)
// 路由视图
var routerView = null
function onLoad () {
routerView = document.querySelector('#routeView')
onPopState()
// 拦截 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
var 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 '/home':
routerView.innerHTML = 'Home'
return
case '/about':
routerView.innerHTML = 'About'
return
default:
return
}
}