众所周知,vue和react是大家普遍在使用的前端框架,而框架在构建单页面应用的时候都缺少不了路由, vue对应的有vue-router,react对应的有react-router-dom,而在react-router-dom之前有一个叫做react-router的依赖,那么它们两个有什么区别呢
react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件
也就是说react-router有的组件或者子项,react-router-dom一定有
BrowserRouter和HashRouter 这两个组件,前者使用pushState和popState事件构建路由,基于history模式,后者使用window.location.hash和hashchange事件构建路由,基于hash模式,那么什么是history,什么是hash呢?
先说说hash
HTML中的hash(#号)
1、#的涵义
#代表网页中的一个位置。右面的字符就是代表的位置信息:如
http://localhost:8081/cbuild/index.html#first就代表网页index.html的first位置。浏览器读取这个URL后,会自动将first位置滚动至可视区域。
为网页制定标识符: 一是使用锚点,比如。 二是使用id属性,比如<divid="print" >。
2、HTTP请求不包括#
比如:http://localhost:8081/cbuild/index.html#first
浏览器实际发出的请求是这样的: GET /index.html HTTP/1.1 不包含#first
3、#后的字符
在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。 比如,下面URL的原意是指定一个颜色值: www.example.com/?color=#fff 但是,浏览器实际发出的请求是: GET /?color= HTTP/1.1 Host: www.example.com 可以看到,"#fff"被省略了。只有将#转码为%23,浏览器才会将其作为实义字符处理。也就是说,上面的网址应该被写成: example.com/?color=%23f…
4、改变#不触发网页重载
单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。 比如,从 www.example.com/index.html#… 改成 www.example.com/index.html#… 浏览器不会重新向服务器请求index.html。
5、改变#会改变浏览器的访问历史
每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置。 这对于ajax应用程序特别有用,可以用不同的#值,表示不同的访问状态,然后向用户给出可以访问某个状态的链接。 值得注意的是,上述规则对IE6和IE7不成立,它们不会因为#的改变而增加历史记录。
6、window.location.hash读取#值
window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录。
7、onhashchange事件
这是一个HTML 5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。 它的使用方法有三种: window.onhashchange = func;
window.addEventListener("hashchange",func, false); 对于不支持onhashchange的浏览器,可以用setInterval监控location.hash的变化。8、Google抓取#的机制
默认情况下,Google的网络蜘蛛忽视URL的#部分。 但是,Google还规定,如果你希望Ajax生成的内容被浏览引擎读取,那么URL中可以使用"#!",Google会自动将其后面的内容转成查询字符串_escaped_fragment_的值。 比如,Google发现新版twitter的URL如下: twitter.com/#!/username 就会自动抓取另一个URL: twitter.com/?escaped_fragment=/username 通过这种机制,Google就可以索引动态的Ajax内容。

思路
当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号),然后根据hash值做些路由跳转处理的操作.具体参数可以访问location查看

最基本的路由实现方法监听事件根据location.hash判断界面
<!DOCTYPE html>
<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>Document</title>
</head>
<body>
<ul>
<li>
<a href="#/a">a</a>
</li>
<li>
<a href="#/b">b</a>
</li>
<li>
<a href="#/c">c</a>
</li>
</ul>
<div id="view"></div>
<script>
var view = null;
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,该事件快于onLoad,所以需要在这里操作
window.addEventListener('DOMContentLoaded', function () {
view = document.querySelector('#view');
viewChange();
});
// 监听路由变化
window.addEventListener('hashchange', viewChange);
// 渲染视图
function viewChange() {
switch (location.hash) {
case '#/b':
view.innerHTML = 'b';
break;
case '#/c':
view.innerHTML = 'c';
break;
default:
view.innerHTML = 'a';
break;
}
}
</script>
</body>
</html>
History
首先我们在浏览器里来看一下history

-
History.length (只读)
返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。例如,在一个新的选项卡加载的一个页面中,这个属性返回1。
-
History.state (只读)
返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待popstate 事件而查看状态而的方式。
-
History.scrollRestoration
允许Web应用程序在历史导航上显式地设置默认滚动恢复行为。此属性可以是自动的(auto)或者手动的(manual)。
history的方法
-
History.back()
前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).
注意:当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。
-
History.forward()
在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1).
注意:当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。。
-
History.go()
通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为-1的时候为上一页,参数为1的时候为下一页. 当整数参数超出界限时,例如: 如果当前页为第一页,前面已经没有页面了,我传参的值为-1,那么这个方法没有任何效果也不会报错。调用没有参数的 go() 方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为url参数的IE有点不同)。传0会刷新当前页面。
添加历史记录中的条目
不会立即加载页面的情况下改变了当前URL地址,往历史记录添加一条条目,除非刷新页面等操作
history.pushState(state, title , URL);
三个参数
-
状态对象
state是一个JavaScript对象,popstate事件的state属性包含该历史记录条目状态对象的副本。
状态对象可以是能被序列化的任何东西。原因在于Firefox将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有640k的大小限制。如果你给 pushState() 方法传了一个序列化后大于640k的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.
-
标题
Firefox 目前忽略这个参数,但未来可能会用到。在此处传一个空字符串应该可以安全的防范未来这个方法的更改。或者,你可以为跳转的state传递一个短标题。
-
URL
新的历史URL记录。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。
注意: pushState() 绝对不会触发 hashchange 事件,即使新的URL与旧的URL仅哈希不同也是如此。
更改历史记录中的当前条目
不会立即加载页面的情况下改变了当前URL地址,并改变历史记录的当前条目,除非刷新页面等操作
history.pushState(state, title , URL);三个参数
- 状态对象
state是一个JavaScript对象,popstate事件的state属性包含该历史记录条目状态对象的副本。
状态对象可以是能被序列化的任何东西。原因在于Firefox将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有640k的大小限制。如果你给 pushState() 方法传了一个序列化后大于640k的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.
- 标题
Firefox 目前忽略这个参数,但未来可能会用到。在此处传一个空字符串应该可以安全的防范未来这个方法的更改。或者,你可以为跳转的state传递一个短标题。
- URL
新的历史URL记录。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。
注意: pushState() 绝对不会触发 hashchange 事件,即使新的URL与旧的URL仅哈希不同也是如此。
更改历史记录中的当前条目
不会立即加载页面的情况下改变了当前URL地址,并改变历史记录的当前条目,除非刷新页面等操作
`history.replaceState(state, title , URL);`
popstate 事件
每当活动的历史记录项发生变化时, popstate 事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate 事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝。
window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
//绑定事件处理函数.
history.pushState({page: 1}, "title 1", "?page=1"); //添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1
history.pushState({page: 2}, "title 2", "?page=2"); //添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2
history.replaceState({page: 3}, "title 3", "?page=3"); //修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为3
history.back(); // 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // 弹出 "location: http://example.com/example.html, state: null
history.go(2); // 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}
既然 history.pushState 和 history.replaceState 都不会触发页面的更新,我们就需要手动给 window 对象添加 pushState 和 replaceState 事件,这个很重要!
const listenWrapper = function (type) {
const _func = history[type];
return function () {
console.log(this);
const func = _func.apply(this, arguments);
const e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return func;
};
};
history.pushState = listenWrapper('pushState');
history.replaceState = listenWrapper('replaceState');
window.addEventListener('pushState', function (e) {
console.log(e)
});
解释一下:
1. 在pushState执行的时候创建自定义事件
2. 在pushSatate外部写自定义事件的监听事件
3. 在pushState执行的时候执行自定义事件
获取当前状态
页面加载时,或许会有个非null的状态对象。这是有可能发生的,举个例子,假如页面(通过pushState() 或 replaceState() 方法)设置了状态对象而后用户重启了浏览器。那么当页面重新加载时,页面会接收一个onload事件,但没有 popstate 事件。然而,假如你读取了history.state属性,你将会得到如同popstate 被触发时能得到的状态对象。
你可以读取当前历史记录项的状态对象state,而不必等待popstate 事件
思路

监听点击事件禁止默认跳转操作,手动利用history实现一套跳转逻辑,根据location.pathname渲染界面.
<!DOCTYPE html>
<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>Document</title>
</head>
<body>
<ul>
<li>
<a href="/a">a</a>
</li>
<li>
<a href="/b">b</a>
</li>
<li>
<a href="/c">c</a>
</li>
</ul>
<div id="view"></div>
<script>
var view = null;
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,该事件快于onLoad,所以需要在这里操作
window.addEventListener('DOMContentLoaded', function () {
view = document.querySelector('#view');
document
.querySelectorAll('a[href]')
.forEach(e => e.addEventListener('click', function (_e) {
_e.preventDefault();
history.pushState(null, '', e.getAttribute('href'));
viewChange();
}));
viewChange();
});
// 监听路由变化
window.addEventListener('popstate', viewChange);
// 渲染视图
function viewChange() {
switch (location.pathname) {
case '/b':
view.innerHTML = 'b';
break;
case '/c':
view.innerHTML = 'c';
break;
default:
view.innerHTML = 'a';
break;
}
}
</script>
</body>
</html>
简单封装路由库
API
基本的路由方法:
router.push(url, onComplete)
router.replace(url, onComplete)
router.go(n)
router.back()
router.stop()
<!DOCTYPE html>
<html>
<head>
<title>router</title>
</head>
<body>
<ul>
<li onclick="router.push('/a', ()=>console.log('push a'))">push a</li>
<li onclick="router.push('/b', ()=>console.log('push b'))">push b</li>
<li onclick="router.replace('/c', ()=>console.log('replace c'))">replace c</li>
<li onclick="router.go(1)">go</li>
<li onclick="router.back(-1)">back</li>
<li onclick="router.stop()">stop</li>
</ul>
<div id="view"></div>
</body>
</html>
初始化
import Router from '../router'
window.router = new Router('view', {
routes: [
{
path: '/a',
component: '<p>a</p>'
},
{
path: '/b',
component: '<p>b</p>'
},
{
path: '/c',
component: '<p>c</p>'
},
{ path: '*', redirect: '/index' }
]
}, 'hash')// 或者'html5'
router类
import HashHstory from "./HashHistory";
import Html5History from "./Html5History";
export default class Router {
constructor(wrapper, options, mode = 'hash') {
this._wrapper = document.querySelector(`#${wrapper}`)
if (!this._wrapper) {
throw new Error(`你需要提供一个容器元素插入`)
}
// 是否支持HTML5 History 模式
this._supportsReplaceState = window.history && typeof window.history.replaceState === 'function'
// 匹配路径
this._cache = {}
// 默认路由
this._defaultRouter = options.routes[0].path
this.route(options.routes)
// 启用模式
this._history = (mode !== 'hash' && this._supportsReplaceState) ? new Html5History(this, options) : new HashHstory(this, options)
}
// 添加路由
route(routes) {
routes.forEach(item => this._cache[item.path] = item.component)
}
// 原生浏览器前进
go(n = 1) {
window.history.go(n)
}
// 原生浏览器后退
back(n = -1) {
window.history.go(n)
}
// 增加
push(url, onComplete) {
this._history.push(url, onComplete)
}
// 替换
replace(url, onComplete) {
this._history.replace(url, onComplete)
}
// 移除事件
stop() {
this._history.stop()
}
}
Hash Class
export default class HashHistory {
constructor(router, options) {
this.router = router
this.onComplete = null
// 监听事件
window.addEventListener('load', this.onChange)
window.addEventListener('hashchange', this.onChange)
}
onChange = () => {
// 匹配失败重定向
if (!location.hash || !this.router._cache[location.hash.slice(1)]) {
window.location.hash = this.router._defaultRouter
} else {
// 渲染视图
this.router._wrapper.innerHTML = this.router._cache[location.hash.slice(1)]
this.onComplete && this.onComplete() && (this.onComplete = null)
}
}
push(url, onComplete) {
window.location.hash = `${url}`
onComplete && (this.onComplete = onComplete)
}
replace(url, onComplete) {
// 优雅降级
if (this.router._supportsReplaceState) {
window.location.hash = `${url}`
window.history.replaceState(null, null, `${window.location.origin}#${url}`)
} else {
// 需要先看看当前URL是否已经有hash值
const href = location.href
const index = href.indexOf('#')
url = index > 0
? `${href.slice(0, index)}#${url}`
: `${href}#${url}`
// 域名不变的情况下不会刷新页面
window.location.replace(url)
}
onComplete && (this.onComplete = onComplete)
}
// 移除事件
stop() {
window.removeEventListener('load', this.onChange)
window.removeEventListener('hashchange', this.onChange)
}
}
HTML5 Class
export default class Html5Hstory {
constructor(router, options) {
this.addEvent()
this.router = router
this.onComplete = null
// 监听事件
window.addEventListener('popstate', this.onChange)
window.addEventListener('load', this.onChange)
window.addEventListener('replaceState', this.onChange);
window.addEventListener('pushState', this.onChange);
}
// pushState/replaceState不会触发popstate事件,所以我们需要自定义
addEvent() {
const listenWrapper = function (type) {
const _func = history[type];
return function () {
const func = _func.apply(this, arguments);
const e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return func;
};
};
history.pushState = listenWrapper('pushState');
history.replaceState = listenWrapper('replaceState');
}
onChange() {
// 匹配失败重定向
if (location.pathname === '/' || !this.router._cache[location.pathname]) {
window.history.pushState(null, '', `${window.location.origin}${this.router._defaultRouter}`);
} else {
// 渲染视图
this.router._wrapper.innerHTML = this.router._cache[location.pathname]
this.onComplete && this.onComplete() && (this.onComplete = null)
}
}
push(url, onComplete) {
window.history.pushState(null, '', `${window.location.origin}${url}`);
onComplete && (this.onComplete = onComplete)
}
replace(url, onComplete) {
window.history.replaceState(null, null, `${window.location.origin}${url}`)
onComplete && (this.onComplete = onComplete)
}
// 移除事件
stop() {
window.removeEventListener('load', this.onChange)
window.removeEventListener('popstate', this.onChange)
window.removeEventListener('replaceState', this.onChange)
window.removeEventListener('pushState', this.onChange)
}
}
以上是所有内容了
