SPA模式的页面开发是基于前端路由来实现的,在路由发生变化的时候仅仅去替换需要改变的组件而不需要继续向后端进行页面的请求(当然可能需要进行一些数据请求),这样的页面的切换会显得十分流畅。
实现前端路由的方式主要有两种:利用路由的hash以及HTML5里面的history
hash模式
通过改变url的hash部分来实现前端页面切换,因为只改变hash值是不会再次向后端发起请求页面的,这种模式即使刷新当前的页面也不会出现history模式中找不到页面的情况
关于hash的一些补充
-
location.href:返回完整的 URL
-
location.hash:返回 URL 的锚部分
-
location.pathname:返回 URL 路径名
-
hashchange 事件:当 location.hash 发生改变时,将触发这个事件
# http://sherlocked93.club/base/#/page1 { "href": "http://sherlocked93.club/base/#/page1", "pathname": "/base/", "hash": "#/page1" }
所以在前端路由切换的时候只改变hash部分的值,同时监听hashchange事件,根据hash值来进行组件的挂载。同时可以通过维护一个hash历史的数组来实现路由的前进和后退。
关于前进后退:
如果是点击浏览器自身的前进后退按钮自然是会自动改变hash,但是如果是点击页面中的back按钮的话就需要自己去通过数组来寻找上一个路由
-
实现没有自定义back按钮的前端路由,此时只有浏览器的前进后退按钮来改变hash值,这时候就不要维护一个历史数组,因为浏览器的前进后退会自动改变hash值,只需要根据hash值来render相应的组件即可
<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>hash 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> </body> <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[path] = cb || function() {} } /** * 记录当前hash,执行cb */ render() { this.currentUrl = location.hash.slice(1) || '/' this.routes[this.currentUrl]() } } RouterClass.init() 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> </html>
-
实现有自定义back按钮的前端路由,这时候需要维护一个历史记录的数组
<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>hash 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> <button>back</button> // 添加一个back按钮 </body> <script> class RouterClass { constructor() { this.isBack = false this.routes = {} // 记录路径标识符对应的cb this.currentUrl = '' // 记录hash只为方便执行cb this.historyStack = [] // hash栈 window.addEventListener('load', () => this.render()) window.addEventListener('hashchange', () => this.render()) } static init() { window.Router = new RouterClass() } /** * 记录path对应cb * @param path * @param cb 回调 */ route(path, cb) { this.routes[path] = cb || function() {} } /** * 入栈当前hash,执行cb */ render() { if (this.isBack) { // 如果是由backoff进入,则置false之后return this.isBack = false // 其他操作在backoff方法中已经做了 return } this.currentUrl = location.hash.slice(1) || '/' this.historyStack.push(this.currentUrl) this.routes[this.currentUrl]() // console.log('refresh事件 Stack:', this.historyStack, ' currentUrl:', 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.currentUrl = prev this.routes[prev]() // 执行对应cb // console.log('点击后退,当前stack:', this.historyStack, ' currentUrl:', this.currentUrl) } } 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页面')) BtnDom.addEventListener('click', Router.back.bind(Router), false) </script> </html>
history模式
利用HTML5的history API来实现前端路由
history对象的方法有:
- history.pushState()
- history.go()
- history.forward() // 相当于history.go(1)
- history.back() // 相当于history.go(-1)
window.history.pushState(null, null, "https://www.baidu.com/?name=orange");
通过监听window.onpopstate来做一些操作,每次页面切换的时候(不论前进还是后退)都会触发popstate事件,
注意,在执行pushState的时候会发生页面切换,但是此时不会触发popstate事件
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'))
}
})
注意
history的pushState不支持跨域模式
history模式是有缺陷需要解决的,因为history模式是直接改变URL的,所以如果前端路由到一个URL,而后端实际上是没有这个地址的响应的,那么如果在该页面直接刷新的话就会向后端发出该URL的请求,也就获取不到响应的页面,会报404错误,所以需要进行处理,可以通过把无法识别的URL指向index.html来解决
网友评论