由于SPA单页应用以及前后端分离的思想的出现,前端路由在当今的前端开发中是必不可少的。一些流行框架vue、react都有对应的实现vue-router、react-router等。
hash路由:一个明显的标志就是带有#,我们主要是通过监听url中的hash变化来进行路由跳转。
hash的优势就是具备好的兼容性,唯一的不好就是url中会一直存在#不够美观,看起来更像是hack实现。
下面基于hash实现一个简单的路由。(采用ES6的语法)
class Routers {
constructor() {
// 以键值对形式储存路由
this.routes = {};
// 当前路由的URL
this.currentUrl = '';
// 记录出现过的hash
this.history = [];
// 作为指针,默认指向this.history的末尾,根据后退前进指向history中不同的hash
this.currentIndex = this.history.length - 1;
this.refresh = this.refresh.bind(this);
this.backOff = this.backOff.bind(this);
// 默认不是后退操作
this.isBack = false;
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
// 将path路径与对应的callback函数存储
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 刷新
refresh() {
// 获取当前URL中的hash路径
this.currentUrl = location.hash.slice(1) || '/';
if(!this.isBack) {
if (this.currentIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.currentIndex + 1);
}
this.history.push(this.currentUrl);
this.currentIndex++;
}
// 执行当前hash路径的callback函数
this.routes[this.currentUrl]();
this.isBack = false;
}
// 后退
backOff() {
// 后退操作设置为true
this.isBack = true;
// 如果指针小于0的话就不存在对应的hash路由
this.currentIndex <= 0 ? (this.currentIndex = 0) : (this.currentIndex = this.currentIndex - 1);
// 随着后退,location.hash页应该随之变化
location.hash = `#${this.history[this.currentIndex]}`;
// 执行指针目前指向hash路由对应的callback
this.routes[this.history[this.currentIndex]]();
}
}
// 调用
Router.route('/',function() {
});
部分API介绍
window.history.back(); // 后退
window.history.forward(); // 前进
window.history.go(-3); // 后退3个页面
// history.pushState用于在浏览历史中添加历史记录,但是不触发跳转
// 三个参数
/*
state: 一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title: 新页面的标题,但是所有浏览器目前都忽略这个值,因此可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
*/
// history.replaceState方法的参数与pushState方法一模一样,区别是它修改浏览历史中当前记录,而非添加记录,同样不触发跳转。
/*
popstate事件,每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。仅仅调用pushState和replaceState方法,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外该事件只针对同一个文档,如果浏览历史切换导致加载不同的文档,该事件也不会触发。
*/
下面基于新的标准实现一个简单路由
class Routers {
constructor() {
this.routes = {};
// 在初始化时监听popstate事件
this._bindPopState();
}
// 初始化路由
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 将路径和对应的回调函数加入hashMap存储
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 触发路由对应回调
go(path) {
history.pushState({path:path}, null, path);
this.routes[path] && this.routes[path]();
}
// 监听popstate事件
_bindPopState() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();
});
}
}
// 创建路由
const routes = [
{// 首页
path: '/',
component: () => import('src/pages/home/index.vue')
},
{// 首页更多功能
path: '/functionManage',
component: () => import('src/pages/home/functionManage.vue')
}
]
// router路由管理
const router = new VueRouter({
routes,
})
// router实例注入根实例
window.vm = new Vue({
router
})
通过mode参数控制路由的实现模式。
const router = new VueRouter({
// HTML5 history 模式
mode: 'history',
base: process.env.NODE_ENV === 'production' ? process.env.PROXY_PATH : '',
routes,
})
在入口文件中需要实例化一个 VueRouter 的实例对象 ,然后将其传入 Vue 实例的 options 中。
var VueRouter = function VueRouter(options) {
// match匹配函数
this.matcher = createMatcher(options.routes || [], this);
// 根据mode实例化具体的History,默认为'hash'模式
var mode = options.mode || 'hash';
// 通过supportsPushState判断浏览器是否支持'history'模式
// 如果设置的是'history'但是如果浏览器不支持的话,'history'模式会退回到'hash'模式
// fallback 是当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = 'hash';
}
this.mode = mode;
// 根据不同模式选择实例化对应的History类
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break;
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break;
}
}
//作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类,两者对应关系:
modehistory:
'history': HTML5History;
'hash': HashHistory;
'abstract': AbstractHistory;
在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;
VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作
1、hash虽然出现在url中,但不会被包括在http请求中,它是用来指导浏览器动作的,对服务器没有影响,改变hash不会重新加载页面。
2、监听hash
window.addEventListener(‘hashchagne’, function, false)
3、每一次改变hash(window.location.hash),都会在浏览器访问历史中增加一个记录。
Vue作为渐进式的前端框架,本身的组件定义中应该是没有有关路由内置属性_route,如果组件中要有这个属性,应该是在插件加载的地方,即VueRouter的install()方法中混入Vue对象的,install.js的源码:
function install (Vue) {
...
Vue.mixin({
beforeCreate: function beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
}
通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个Vue实例,该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。
$router.push()–>HashHistory.push()–>History.transitionTo()–>History.updateRoute()–>{app._route=route}–>vm.render()
HashHistory.push()方法:将路由添加到浏览器访问历史栈顶(直接对window.location.hash赋值)
HashHistory.replace()方法:替换掉当前路由(调用window.location.replace)
监听地址栏:
上面的VueRouter.push()和VueRouter.replace()是可以在vue组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此还需要监听浏览器地址栏中路由的变化 ,并具有与通过代码调用相同的响应行为,在HashHistory中这一功能通过setupListeners监听hashchange实现:相当于直接调用了replace方法。
History interface是浏览器历史记录栈提供的接口,通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。
HTML5引入了history.pushState()和history.replaceState()方法,他们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate配合使用。
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)
复制代码
stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本
title:所添加记录的标题
url:所添加记录的url(可选的)
pushState和replaceState两种方法的共同特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// 加了 try...catch 是因为 Safari 有调用 pushState 100 次限制
// 一旦达到就会抛出 DOM Exception 18 错误
const history = window.history
try {
if (replace) {
// replace 的话 key 还是当前的 key 没必要生成新的
history.replaceState({ key: _key }, '', url)
} else {
// 重新生成 key
_key = genKey()
// 带入新的 key 值
history.pushState({ key: _key }, '', url)
}
} catch (e) {
// 达到限制了 则重新指定新的地址
window.location[replace ? 'replace' : 'assign'](url)
}
}
// 直接调用 pushState 传入 replace 为 true
export function replaceState (url?: string) {
pushState(url, true)
}
复制代码代码结构以及更新视图的逻辑与hash模式基本类似,只不过将对window.location.hash()直接进行赋值window.location.replace()改为了调用history.pushState()和history.replaceState()方法。
在HTML5History中添加对修改浏览器地址栏URL的监听popstate是直接在构造函数中执行的:
constructor (router: Router, base: ?string) {
window.addEventListener('popstate', e => {
const current = this.current
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
handleScroll(router, route, current, true)
}
})
})
}
复制代码以上就是’hash’和’history’两种模式,都是通过浏览器接口实现的。
一般的需求场景中,hash模式与history模式是差不多的,根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:
1、pushState设置的新url可以是与当前url同源的任意url,而hash只可修改#后面的部分,故只可设置与当前同文档的url
2、pushState设置的新url可以与当前url一模一样,这样也会把记录添加到栈中,而hash设置的新值必须与原来不一样才会触发记录添加到栈中
3、pushState通过stateObject可以添加任意类型的数据记录中,而hash只可添加短字符串
4、pushState可额外设置title属性供后续使用
hash模式仅改变hash部分的内容,而hash部分是不会包含在http请求中的(hash带#):
http://oursite.com/#/user/id //如请求,只会发送http://oursite.com/
所以hash模式下遇到根据url请求页面不会有问题
而history模式则将url修改的就和正常请求后端的url一样(history不带#)
http://oursite.com/user/id
如果这种向后端发送请求的话,后端没有配置对应/user/id的get路由处理,会返回404错误。
官方推荐的解决办法是在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。
参考链接:https://juejin.im/post/5b08c9ccf265da0dd527d98d