前端路由hash、history原理及简单的实践下
阅读目录
一:什么是路由?前端有哪些路由?他们有哪些特性?
路由是根据不同的url地址来显示不同的页面或内容的功能,这个概念很早是由后端提出的。
后端之前是这么做的,当我们访问 http://xxx.abc.com/xx 的时候,大致流程可以想象成这样的:
1. 浏览器向服务器发出请求。
2. 服务器监听到80端口,如果有请求过来,那么就解析url地址。
3. 服务器根据客户端的路由配置,然后就返回相应的信息(比如html字符串、json数据或图片等)。
4. 浏览器根据数据包的 Content-Type来决定如何解析数据。
如上就是后端路由最初始的实现方式,那么既然有后端路由,那为什么还需要我们前端路由呢?后端路由有一个很大的缺点就是每次路由切换的时候都需要去刷新页面,然后发出ajax请求,然后将请求数据返回回来,那么这样每次路由切换都要刷新页面对于用户体验来说就不好了。因此为了提升用户体验,我们前端路由就这样产生了。它就可以解决浏览器不会重新刷新了。
在理解路由之前,我们下面看下History API有哪些方法:
DOM window对象通过history对象提供了对当前会话浏览历史的访问,在html4中有如下方法:
window.history.length: 返回当前会话浏览过的页面数量。
window.history.go(?delta): 接收一个整数作为参数,按照当前页面在会话浏览历史记录中的位置进行移动。如果参数为0、undefined、null、false 将刷新页面,相当于执行window.location.reload()方法。如果参数大于浏览器浏览的数量,或小于浏览前的数量的话,什么都不会做。
window.history.back(). 移动到上一页。相当于点击浏览器的后退按钮,等价于 window.history.go(-1);
window.history.forward(). 移动到下一页,相当于点击浏览器的前进按钮,等价于window.history.go(1).
在html5中,History API 新增了操作会话浏览历史记录的功能。如下新增的几个方法:
window.history.state. 该参数是只读的,表示与会话浏览历史的当前记录相关联的状态对象。如下图所示:
window.history.pushState(data, title, ?url): 在会话浏览历史记录中添加一条记录。
window.history.replaceState(data, title, ?url): 该方法用法和history.pushState方法类似,但是该方法的含义是将修改会话浏览历史的当前记录,而不是新增一条记录。也就是说把当前的浏览地址换成 replaceState之后的地址,但是浏览历史记录的总长度并没有新增。
注意:执行上面两种方法后,url地址会发生改变。但是不会刷新页面。因此有了这些基本知识后,我们再来看下前端路由。
那么前端路由也有2种模式,第一种是hash模式,第二种是history模式。我们来分别看下这两种知识点及区别如下:
1. hash模式
hash路由模式是这样的:http://xxx.abc.com/#/xx。 有带#号,后面就是hash值的变化。改变后面的hash值,它不会向服务器发出请求,因此也就不会刷新页面。并且每次hash值发生改变的时候,会触发hashchange事件。因此我们可以通过监听该事件,来知道hash值发生了哪些变化。比如我们可以如下简单的监听:
function hashAndUpdate () {
// todo 匹配 hash 做 dom 更新操作
} window.addEventListener('hashchange', hashAndUpdate);
我们先来了解下location有哪些属性,如下:
// 完整的url
location.href // 当前URL的协议,包括 :; 比如 https:
location.protocol /* 主机名和端口号,如果端口号是80(http)或443(https), 那就会省略端口号,比兔 www.baidu.com:8080 */
location.host // 主机名:比如:www.baidu.com
location.hostname // 端口号;比如8080
location.port // url的路径部分,从 / 开始; 比如 https://www.baidu.com/s?ie=utf-8,那么 pathname = '/s'了
location.pathname // 查询参数,从?开始;比如 https://www.baidu.com/s?ie=utf-8 那么 search = '?ie=utf-8'
location.search // hash是页面中的一个片段,从 # 开始的,比如 https://www.baidu.com/#/a/b 那么返回值就是:"#/a/b"
location.hash
location.href
我们通过改变location.href来改变对应的url,看看是否会刷新页面,我们做如下测试可以看到,使用location.href 改变url后并不会刷新页面,如下代码在控制台中演示:
location.hash
改变hash不会触发页面跳转,因为hash链接是当前页面中的某个片段,所以如果hash有变化,那么页面将会滚动到hash所连接的位置。但是页面中如果不存在hash对应的片段,则没有任何效果。比如 a链接。这和 window.history.pushState方法类似,都是不刷新页面的情况下更改url。如下也可以看到操作并没有刷新url,如下演示:
hash 和 pushState 对比有如下缺点:
1. hash只能修改url的片段标识符的部分。并且必须从#号开始,但是pushState且能修改路径、查询参数和片段标识符。pushState比hash更符合前端路由的访问方式,更加优雅(因为不带#号)。
2. hash必须和原先的值不同,才能新增会话浏览历史的记录,但是pushState可以新增相同的url的记录,如下所示:
1.1 使用hashchange事件来监听url hash的改变
我们来演示下,我们使用node启动一个服务,然后有一个index.html页面,该页面引入了一个js文件,该js文件有如下js代码:
window.addEventListener('hashchange', function(e) {
console.log(e)
});
如上代码就是监听hash值发生变化的事件,然后我们访问该index.html页面后,然后在控制台中,做如下操作,如下图演示:
如上可以看到;不管我们是通过location接口直接改变hash值,还是我们通过history直接前进或后退操作(改变hash变化),我们都可以看到都能通过 hashchange该事件进行监听到url hash的改变。并且不会刷新页面。
2. history模式
HTML5的History API为浏览器的全局history对象增加了该扩展方法。它是一个浏览器的一个接口,在window对象中提供了onpopstate事件来监听历史栈的改变,只要历史栈有信息发生改变的话,就会触发该事件。提供了如下事件:
window.addEventListener('popstate', function(e) {
console.log(e)
});
history提供了两个操作历史栈的API: history.pushState 和 history.replaceState
history.pushState(data[,title][,url]); // 向历史记录中追加一条记录
history.replaceState(data[,title][,url]); // 替换当前页在历史记录中的信息。
如上html5中新增了上面这两个方法,该两个方法也可以改变url,页面也不会重新刷新。下面我们也可以来做个demo,来监听下popstate事件,现在在我js里面放入如下js代码:
window.addEventListener('popstate', function(e) {
console.log(e)
});
然后我们访问页面,如下所示:
如上图所示,我们使用location.hash, history.go(-1), history.pushState 等方法操作都会触发 popstate 事件,并且浏览器的url地址也会跟着改变。只会改变url地址,且不会重新刷新页面。
hash模式的特点:
hash模式在浏览器地址栏中url有#号这样的,比如(http://localhost:3001/#/a). # 后面的内容不会传给服务端,也就是说不会重新刷新页面。并且路由切换的时候也不会重新加载页面。
history模式的特点:
浏览器地址没有#, 比如(http://localhost:3001/a); 它也一样不会刷新页面的。但是url地址会改变。
二:如何实现简单的hash路由?
实现hash路由需要满足如下基本条件:
1. url中hash值的改变,并不会重新加载页面。
2. hash值的改变会在浏览器的访问历史中增加一条记录,我们可以通过浏览器的后退,前进按钮控制hash值的切换。
3. 我们可以通过hashchange事件,监听到hash值的变化,从而加载不同的页面显示。
触发hash值的变化有2种方法:
第一种是通过a标签,设置href属性,当点击a标签之后,地址栏会改变,同时会触发hashchange事件。比如如下a链接:
<a href="#/test1">测试hash1</a>
第二种是通过js直接赋值给location.hash,也会改变url,触发hashchange事件。
location.hash = '#/test1';
因此我们下面可以实现一个简单的demo,html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hash路由demo</title>
</head>
<body>
<ul>
<li><a href="#/">我是主页</a></li>
<li><a href="#/a">我是a页面</a></li>
<li><a href="#/b">我是b页面</a></li>
</ul>
</body>
</html>
然后我们hash.js 代码如下:
class HashRouter {
constructor() {
// 存储hash与callback键值对
this.routes = {};
// 保存当前的hash
this.currentHash = ''; // 绑定事件
const hashChangeUrl = this.hashChangeUrl.bind(this); // 页面加载事件
window.addEventListener('load', hashChangeUrl, false);
// 监听hashchange事件
window.addEventListener('hashchange', hashChangeUrl, false);
}
// path路径和callback函数对应起来,并且使用 上面的this.routes存储起来
route(path, callback) {
this.routes[path] = callback || function() {};
}
hashChangeUrl() {
/*
获取当前的hash值
location.hash 获取的值为:"#/a, 因此 location.hash.slice(1) = '/a' 这样的
*/
this.currentHash = location.hash.slice(1) || '/';
// 执行当前hash对应的callback函数
this.routes[this.currentHash]();
}
}
// 初始化
const Router = new HashRouter();
const body = document.querySelector('body');
const changeColor = function(color) {
body.style.backgroundColor = color;
};
// 注册函数
Router.route('/', () => {
changeColor('red');
});
Router.route('/a', () => {
changeColor('green');
});
Router.route('/b', () => {
changeColor('#CDDC39');
});
如上就是一个非常简化的hash路由了,首先我们代码也是非常的简化(我相信大家都能看懂),首先如上js代码有一个route函数,该函数的作用就是初始化对应的路由和函数进行绑定起来,把他们保存到 this.routes 对象里面去,然后使用 hashchange 事件进行监听,如果触发了该事件,就找到该路由,然后触发对应的函数即可。我们点击某个a链接就会调用对应的函数,或者我们可以在控制台中使用 location.hash = '/b'; 来改变值也会触发的。
查看效果
三:如何实现简单的history路由?
代码如下:
class HistoryRoutes {
constructor() {
// 保存对应键和函数
this.routes = {}; // 监听popstate事件
window.addEventListener('popstate', (e) => {
const path = this.getState();
this.routes[path] && this.routes[path]();
});
}
// 获取路由路径
getState() {
const path = window.location.pathname;
return path ? path : '/';
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
init(path) {
history.replaceState(null, null, path);
this.routes[path] && this.routes[path]();
}
go(path) {
history.pushState(null, null, path);
this.routes[path] && this.routes[path]();
}
} window.Router = new HistoryRoutes();
console.log(location.pathname);
Router.init(location.pathname); const body = document.querySelector('body'); const changeColor = function(color) {
body.style.backgroundColor = color;
};
// 注册函数
Router.route('/', () => {
changeColor('red');
});
Router.route('/a', () => {
changeColor('green');
});
Router.route('/b', () => {
changeColor('#CDDC39');
}); const ul = document.querySelector('ul');
ul.addEventListener('click', e => {
console.log(e.target);
if (e.target.tagName === 'A') {
e.preventDefault();
Router.go(e.target.getAttribute('href'));
}
});
四:hash和history路由一起实现
首先看下项目目录结构如下:
|----项目demo
| |--- .babelrc # 解决es6语法问题
| |--- node_modules # 所有依赖的包
| |--- dist # 打包后的页面 访问该页面使用:http://0.0.0.0:7799/dist
| |--- js
| | |--- base.js
| | |--- hash.js
| | |--- history.js
| | |--- routerList.js
| | |--- index.js
| |--- package.json # 依赖的包文件
| |--- webpack.config.js # webpack打包文件
| |--- index.html # html 页面
index.html 页面如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hash+history路由demo</title>
</head>
<body>
<div id="app"></div>
<ul class="list">
<li><a href="/">我是主页</a></li>
<li><a href="/hash">我是hash页面</a></li>
<li><a href="/history">我是history页面</a></li>
</ul>
</body>
</html>
js/base.js 代码如下:
const ELEMENT = document.querySelector('#app'); export class BaseRouter {
constructor(list) {
this.list = list;
}
render(state) {
let ele = this.list.find(ele => ele.path === state);
ele = ele ? ele : this.list.find(ele => ele.path === '*');
ELEMENT.innerText = ele.component;
}
}
如上代码base.js 该类就一个构造函数,在hash.js 或 history.js 会继承该类,因此hash或history类都有该render() 方法。该render方法作用就是找到对应的路径是否等于 routerlist中的path,如果找到的话,就把对应的component里面的内容赋值给 id为app的元素。
js/hash.js 代码如下:
import { BaseRouter } from './base.js'; // hash路由继承了BaseRouter
export class HashRouter extends BaseRouter {
constructor(list) {
super(list);
this.handler();
// 监听hash事件变化,并且重新渲染页面
window.addEventListener('hashchange', (e) => {
this.handler();
});
}
// 渲染
handler() {
const state = this.getState();
this.render(state);
}
// 获取当前的hash
getState() {
const hash = window.location.hash;
return hash ? hash.slice(1) : '/';
}
// 获取完整的url
getUrl(path) {
const href = window.location.href;
const index = href.indexOf('#');
const base = index > -1 ? href.slice(0, index) : href;
return `${base}#${path}`;
}
// hash值改变的话,实现压入
push(path) {
window.location.hash = path;
}
// 替换功能
replace(path) {
window.location.replace(this.getUrl(path));
}
// 模拟history.go 功能,实现前进/后退功能
go(n) {
window.history.go(n);
}
}
hash 代码如上;该类里面有hashchange事件监听hash值的变化,如果变化的话就会调用 handler 函数,在执行该函数中的render方法之前,会先调用 getState 方法,该方法目的是获取当前的hash。比如getState方法中使用location.hash 获取的hash会是 '#/x' 这样的,然后会返回 '/x'。
getUrl() 方法是获取完整的url,可以看如上代码理解下即可,其他的就是 push,replace,go方法。
js/history.js 代码如下:
import { BaseRouter } from './base.js';
export class HistoryRouter extends BaseRouter {
constructor(list) {
super(list);
this.handler();
// 监听历史栈变化,变化时候重新渲染页面
window.addEventListener('popstate', (e) => {
this.handler();
});
}
// 渲染
handler() {
const state = this.getState();
this.render(state);
}
// 获取路由路径
getState() {
const path = window.location.pathname;
return path ? path : '/';
}
/*
pushState方法实现压入功能,PushState不会触发popstate事件,
因此我们需要手动调用handler函数
*/
push(path) {
window.history.pushState(null, null, path);
this.handler();
}
/*
pushState方法实现替换功能,replaceState不会触发popstate事件,
因此我们需要手动调用handler函数
*/
replace(path) {
window.history.replaceState(null, null, path);
this.handler();
}
go(num) {
window.history.go(num);
}
};
代码和hash.js 代码类似。
js/routerList.js 代码如下:
export const ROUTERLIST = [
{
path: '/',
name: 'index',
component: '这是首页'
},
{
path: '/hash',
name: 'hash',
component: '这是hash页面'
},
{
path: '/history',
name: 'history',
component: '这是history页面'
},
{
path: '*',
component: '404页面'
}
];
js/index.js 代码如下:
import { HashRouter } from './hash';
import { HistoryRouter } from './history';
import { ROUTERLIST } from './routerList'; // 路由模式,默认为hash
const MODE = 'history'; class WebRouter {
constructor({ mode = 'hash', routerList }) {
this.router = mode === 'hash' ? new HashRouter(routerList) : new HistoryRouter(routerList);
}
push(path) {
// 返回 this.router 因此有 hash或history中的push方法
this.router.push(path);
}
replace(path) {
this.router.replace(path);
}
go(num) {
this.router.go(num);
}
} const webRouter = new WebRouter({
mode: MODE,
routerList: ROUTERLIST
}); document.querySelector('.list').addEventListener('click', e => {
const event = e || window.event;
event.preventDefault();
if (event.target.tagName === 'A') {
const url = event.target.getAttribute('href');
!url.indexOf('/') ? webRouter.push(url) : webRouter.go(url);
}
});
如上就是所有的代码了,仔细研究下看到这样编写代码的好处,就是hash.js,和 history.js 代码分离出来了,并且都有对应的方法,然后在index.js 初始化对应的代码。最后使用dom点击事件传递对应的路由进去。
查看效果
注意:github源码下载后会有一个dist文件夹,该文件是打包后的文件,也就是说我们改完里面的代码后,先 npm run build 后生成该文件,然后我们再运行下 npm run dev 后,使用 http://0.0.0.0:7799/dist/ 访问页面进行切换。
前端路由hash、history原理及简单的实践下的更多相关文章
- Hash算法原理的简单分析
哈希计算就是努力的把比较大的数据存放到相对较小的空间中.最常见的哈希算法是取模法.下面简单讲讲取模法的计算过程.比如:数组的长度是5.这时有一个数据是6.那么如何把这个6存放到长度只有5的数组中呢.按 ...
- 前端路由vue-router介绍
一.前端路由vue-router介绍 Vue-Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌.包含的功能有: 嵌套的路由/视图表 模 ...
- vue-learning:38 - router - 前端路由的发展
前端路由的发展 参考博客 前端路由是什么东西? 什么是路由 在jQuery时代,我们使用<a href="https://www.example.com/example/home.ht ...
- Vue(三)之前端路由
01-前端路由 1.前端路由的实现原理 vue+vue-router 主要来做单页面应用(Single Page Application) 为什么我们要做单页面应用? (1)传统的开发方式 url改变 ...
- 前端路由原理之 hash 模式和 history 模式
什么是路由? 个人理解路由就是浏览器 URL 和页面内容的一种映射关系. 比如你看到我这篇博客,博客的链接是一个 URL,而 URL 对应的就是我这篇博客的网页内容,这二者之间的映射关系就是路由. 其 ...
- 简单的基于hash和hashchange的前端路由
hash定义 hash这个玩意是地址栏上#及后面部分,代表网页中的一个位置,#后面部分为位置标识符.页面打开后,会自动滚动到指定位置处. 位置标识符 ,一是使用锚点,比如<a name=&quo ...
- 前端路由以及浏览器回退,hash & history & location
一.前言 其实不止一次想监听浏览器的回退方法,比如 在 list.html 页滚动加载了几页列表,点到 detail.html 看详情,反回来时又得重新加载几页 H5 有背景音乐的,跳页就得重新放,体 ...
- 原生JS实现一个简单的前端路由(原理)
说一下前端路由实现的简要原理,以 hash 形式(也可以使用 History API 来处理)为例, 当 url 的 hash 发生变化时,触发 hashchange 注册的回调,回调中去进行不同的操 ...
- 【前端路由】Vue-router 中hash模式和history模式的区别
咱们今天说说VUE路由的hash模式与history模式的区别,这个也是面试常问的问题,不要小看这道题其实问到这里的时候那个面试官应该是个大牛,开发经验丰富,这个题其实就是考验你的开发经验是否属实. ...
随机推荐
- 心动吗?正大光明的免费使用IntelliJ IDEA商业版
IntelliJ IDEA是广受Java开发者喜爱的工具,其商业版的价格十分昂贵,如下图: 现在有机会免费获取IntelliJ IDEA的正版License,您是否心动呢?我把自己成功申请Licens ...
- .NET之Hangfire快速入门和使用
前言: 定时任务调度问题,是一个老生常谈的问题.网上有许多定时任务调度的解决方案,对于我而言很早以前主要是使用Window计划和Window服务来做任务定时执行,然后就开始使用定时任务调度框架Quar ...
- GStreamer基础教程09 - Appsrc及Appsink
摘要 在我们前面的文章中,我们的Pipline都是使用GStreamer自带的插件去产生/消费数据.在实际的情况中,我们的数据源可能没有相应的gstreamer插件,但我们又需要将数据发送到GStre ...
- 4.7 if else-if
c#中的if else-if if else-if 中最后的"else":如果用户输入的不等于上面的else if(xxx)表达式,则输出这行代码. **不参与运算的数值不用转换 ...
- linux 设置查看文本行数
在一般模式下,即摁下esc按键下的模式: 设置行数为:set nu(此处的冒号需要带上) 取消行号为:set nonu(此处的冒号需要带上)
- Mysql的表级锁和行级锁
表级锁 MySQL表级锁分为读锁和写锁. 读锁 用法:LOCK TABLE table_name [ AS alias_name ] READ 释放锁使用UNLOCK tables.可以为表使用别名, ...
- Flask基础(01)-->Flask框架介绍
什么是Flask? 说白了,Flask就是一种web框架 在python中常用的框架有 flask django tornado 什么又是web框架呢? 为什么要使用web框架呢? 增强扩展性和稳定 ...
- Angular 元素拖拽
拖动元素到指定区域 拖放的同时传递数据 1. 安装 ng2-drag-drop npm install ng2-drag-drop --save 2. 模板中配置可拖拽元素 // drag.compo ...
- 十大排序算法JavaScript实现总结
花费了几周的时间断断续续的练习和模仿与使用JavaScript代码实现了十大排序算法. 里面有每种算法的动图和静态图片演示,看到图片可以自己先按照图片的思路实现一下. github中正文链接,点击查看 ...
- C++——指针
目录 一.地址和指针 1.1内存 1.2针和指针变量 二.指针变量 2.1始化 2.2赋值 2.3指针类型算术运算 2.4指针类型关系运算 2.5指向指针的指针 三.指针与数组 3.1指针运算 3.2 ...