从了解Hash和Html5 History 到简单实现路由
Hash
hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分),在页面中的hash有多种功能意义:
锚点
url: http://www.example.com/index.html#jump
dom: <a name="jump"></a> 或者 <div id="jump" >
浏览器读取到hash之后自动滚动到该对应元素所在位置的可视区域内
不附加在请求上
意味着它不管怎么变化都不会影响请求URL,即它只针对浏览器的.
浏览器: http://www.example.com/index.html#jump
服务器: http://www.example.com/index.html
注意: 有种情况是你会在URL上带#符号,但是你本意不是作为hash使用的,例如回调地址或者传参之类,这时候浏览器只会当做hash处理,所以需要先转码.
// 未转码
浏览器: http://www.example.com/index.html?test=#123
服务器: http://www.example.com/index.html?test=
// 转码
浏览器: http://www.example.com/index.html?test=%23123
服务器: http://www.example.com/index.html?test=%23123
改变访问历史但不会触发页面刷新
这个大家都知道,尽管它不会跳转也不会刷新,但是你能通过点击浏览器前进后退发现它也会被添加去访问历史记录里.(低版本IE不考虑)
缺点
- 搜索引擎不友好
- 难以追踪用户行为
思路
当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号),然后根据hash值做些路由跳转处理的操作.具体参数可以访问location查看
http://www.example.com/index.html#jump
最基本的路由实现方法监听事件根据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
DOM window 对象通过 history 对象提供了对浏览器的会话历史的访问。它暴露了很多有用的方法和属性,允许你在用户浏览历史中向前和向后跳转
向前和向后跳转
window.history.back();
window.history.forward();
跳转到 history 中指定的一个点
你可以用 go() 方法载入到会话历史中的某一特定页面, 通过与当前页面相对位置来标志 (当前页面的相对位置标志为0).
window.history.go();
添加历史记录中的条目
不会立即加载页面的情况下改变了当前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 会包含一个当前历史记录状态对象的拷贝。
获取当前状态
页面加载时,或许会有个非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>
注意,该方法不支持本地运行,只能线上运作或者启动服务器查看效果
html5_demo.html:26 Uncaught DOMException: Failed to execute 'pushState' on 'History': A history state object with URL 'file:///C:/b' cannot be created in a document with origin 'null' and URL 'file:///C:/work/project/router_demo/src/html5_demo.html'.
at HTMLAnchorElement.<anonymous> (file:///C:/work/project/router_demo/src/html5_demo.html:26:15)
(anonymous) @ html5_demo.html:26
简单封装路由库
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)
}
}
广州VI设计公司https://www.houdianzi.com
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)
}
}