单页面应用利用了JavaScript动态变换网页内容,避免了页面重载;路由则提供了浏览器地址变化,网页内容也跟随变化,二者结合起来则为咱们提供了体验良好的单页面web应用javascript
路由须要实现三个功能:html
①浏览器地址变化,切换页面;前端
②点击浏览器【后退】、【前进】按钮,网页内容跟随变化;vue
③刷新浏览器,网页加载当前路由对应内容java
在单页面web网页中,单纯的浏览器地址改变,网页不会重载,如单纯的hash网址改变网页不会变化,所以咱们的路由主要是经过监听事件,并利用js实现动态改变网页内容,有两种实现方式:webpack
hash
路由: 监听浏览器地址hash值变化,执行相应的js切换网页 history
路由: 利用history API实现url地址改变,网页内容改变git
首先定义一个Router
类github
class Router {
constructor(obj) {
// 路由模式
this.mode = obj.mode
// 配置路由
this.routes = {
'/index' : 'views/index/index',
'/index/detail' : 'views/index/detail/detail',
'/index/detail/more' : 'views/index/detail/more/more',
'/subscribe' : 'views/subscribe/subscribe',
'/proxy' : 'views/proxy/proxy',
'/state' : 'views/state/stateDemo',
'/state/sub' : 'views/state/components/subState',
'/dom' : 'views/visualDom/visualDom',
'/error' : 'views/error/error'
}
this.init()
}
}
复制代码
路由初始化init()
时监听load
,hashchange
两个事件:web
window.addEventListener('load', this.hashRefresh.bind(this), false);
window.addEventListener('hashchange', this.hashRefresh.bind(this), false);
复制代码
浏览器地址hash值变化直接经过a标签连接实现数组
<nav id="nav" class="nav-tab">
<ul class='tab'>
<li><a class='nav-item' href="#/index">首页</a></li>
<li><a class='nav-item' href="#/subscribe">观察者</a></li>
<li><a class='nav-item' href="#/proxy">代理</a></li>
<li><a class='nav-item' href="#/state">状态管理</a></li>
<li><a class='nav-item' href="#/dom">虚拟DOM</a></li>
</ul>
</nav>
<div id="container" class='container'>
<div id="main" class='main'></div>
</div>
复制代码
hash值变化后,回调方法:
/** * hash路由刷新执行 */
hashRefresh() {
// 获取当前路径,去掉查询字符串,默认'/index'
var currentURL = location.hash.slice(1).split('?')[0] || '/index';
this.name = this.routes[this.currentURL]
this.controller(this.name)
}
/** * 组件控制器 * @param {string} name */
controller(name) {
// 得到相应组件
var Component = require('../' + name).default;
// 判断是否已经配置挂载元素,默认为$('#main')
var controller = new Component($('#main'))
}
复制代码
有位同窗留言要实现路由懒加载,参考vue的实现方式,这里贴出来,但愿你们多提意见:
/** * 懒加载路由组件控制器 * @param {string} name */
controller(name) {
// import 函数会返回一个 Promise对象,属于es7范畴,须要配合babel的syntax-dynamic-import插件使用
var Component = ()=>import('../'+ name);
Component().then(resp=>{
var controller = new resp.default($('#main'))
})
}
复制代码
考虑到存在多级页面嵌套路由的存在,须要对嵌套路由进行处理:
改造后的路由刷新方法为:
hashRefresh() {
// 获取当前路径,去掉查询字符串,默认'/index'
var currentURL = location.hash.slice(1).split('?')[0] || '/index';
// 多级连接拆分为数组,遍历依次加载
this.currentURLlist = currentURL.slice(1).split('/')
this.url = ""
this.currentURLlist.forEach((item, index) => {
// 导航菜单激活显示
if (index === 0) {
this.navActive(item)
}
this.url += "/" + item
this.name = this.routes[this.url]
// 404页面处理
if (!this.name) {
location.href = '#/error'
return false
}
// 对于嵌套路由和兄弟路由的处理
if (this.oldURL && this.oldURL[index]==this.currentURLlist[index]) {
this.handleSubRouter(item,index)
} else {
this.controller(this.name)
}
});
// 记录连接数组,后续处理子级组件
this.oldURL = JSON.parse(JSON.stringify(this.currentURLlist))
}
/** * 处理嵌套路由 * @param {string} item 连接list中当前项 * @param {number} index 连接list中当前索引 */
handleSubRouter(item,index){
// 新路由是旧路由的子级
if (this.oldURL.length < this.currentURLlist.length) {
// 相同路由部分不从新加载
if (item !== this.oldURL[index]) {
this.controller(this.name)
}
}
// 新路由是旧路由的父级
if (this.oldURL.length > this.currentURLlist.length) {
var len = Math.min(this.oldURL.length, this.currentURLlist.length)
// 只从新加载最后一个路由
if (index == len - 1) {
this.controller(this.name)
}
}
}
复制代码
这样,一个hash路由组件就实现了
使用时,只需new一个Router实例便可:new Router({mode:'hash'})
window.history
属性指向 History 对象,是浏览器的一个属性,表示当前窗口的浏览历史,History 对象保存了当前窗口访问过的全部页面地址。更多了解History对象,可参考阮一峰老师的介绍: History 对象
webpack开发环境下,须要在devServer对象添加如下配置:
historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], } 复制代码
history路由主要是经过history.pushState()
方法向浏览记录中添加一条历史记录,并同时触发js回调加载页面
当【前进】、【后退】时,会触发history.popstate
事件,加载history.state
中存放的路径
history路由实现与hash路由的步骤相似,因为须要配置路由模式切换,页面中全部的a连接都采用了hash类型连接,history路由初始化时,须要拦截a标签的默认跳转:
/** * history模式劫持 a连接 */
bindLink() {
$('#nav').on('click', 'a.nav-item', this.handleLink.bind(this))
}
/** * history 处理a连接 * @param e 当前对象Event */
handleLink(e) {
e.preventDefault();
// 获取元素路径属性
let href = $(e.target).attr('href')
// 对非路由连接直接跳转
if (href.slice(0, 1) !== '#') {
window.location.href = href
} else {
let path = href.slice(1)
history.pushState({
path: path
}, null, path)
// 加载相应页面
this.loadView(path.split('?')[0])
}
}
复制代码
history路由初始化须要绑定load
、popstate
事件
this.bindLink()
window.addEventListener('load', this.loadView.bind(this, location.pathname));
window.addEventListener('popstate', this.historyRefresh.bind(this));
复制代码
浏览是【前进】或【后退】时,触发popstate
事件,执行回调函数
/** * history模式刷新页面 * @param e 当前对象Event */
historyRefresh(e) {
const state = e.state || {}
const path = state.path.split('?')[0] || null
if (path) {
this.loadView(path)
}
}
复制代码
history路由模式首次加载页面时,能够默认一个页面,这时能够用history.replaceState
方法
if (this.mode === 'history' && currentURL === '/') {
history.replaceState({path: '/'}, null, '/')
currentURL = '/index'
}
复制代码
对于404页面的处理,也相似
history.replaceState({path: '/error'}, null, '/error')
this.loadView('/error')
复制代码
更多源码请访问Github