前言
文章相较于之前做了删减,将稍微深入一点的知识点(例如源码,拓展)和一些表述比较乱的地方剔除,目的是为了能够用最小精力去记住,并且输出。
本文章保证能让你懂,并且教你如何输出!
什么是单页面
在过去的前后端未分离时代,页面的每次跳转都是请求对应的html文件到浏览器进行渲染,页面才能展示。
而现在前后端分离时代,客户端一次性加载大部分的前端文件,url变化时,不用向后台请求新的html文件,只需要将当前页面内的html内容进行更换,渲染,就可以实现页面内容的切换。这就是单页面,简称SPA。
Vue-router的主要作用就是实现如何根据url的变化去更新视图,实现单页面效果。
优点:
- 相较过去,页面内容切换的时候很快,用户体验更好。
- 服务器压力没以前大了,毕竟很多工作都放在了浏览器上做。
缺点:
- 第一次加载页面文件会比较多,如果是大型项目,可能会出现首页加载缓慢问题,需要手动优化。
- SEO优势削弱,没有像不分离时代或者SSR服务端渲染那样的优势。
路由模式
在浏览器环境中,由router配置里的mode属性设置路由模式而决定实现原理;
主要有三种模式:
- Hash模式(默认,如果浏览器不支持history模式,也会采用hash模式)
- Hisrtory模式(实现建立在h5新增的api,所以也叫H5 Hisrtory)
- Abstract模式(在node环境下,此次不涉及)
我们平时在参数 mode
上切换路由方式,其实是在创建VueRouter类里面对应的实例对象(这个不懂没关系,继续看):
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
Hash模式
hash符号#
的本来作用是加在URL中指示网页中的位置:
http://www.example.com/index.html#print
#
符号本身以及它后面的字符称之为hash,可通过 window.location.hash
属性读取。
特点或原理
第一:
hash虽然出现在URL中,但不会被包括在HTTP请求中。例如当地址后面加上了hash值为:https://xxx.com/home#111
时,是不会向服务端发送资源获取请求的。因此,改变hash不会去请求页面资源。
第二:
当哈希值改变了就会触发内置事件hashchange
,触发条件有:
- 直接更改浏览器地址,在最后面增加或改变#hash;
- 通过改变
location.href
或location.hash
的值; - 通过触发点击带锚点的链接;
- 浏览器前进后退可能导致hash的变化,前提是两个网页地址的hash值不同;
第三:
每一次改变hash(window.location.hash
),都会在浏览器的访问历史中增加一个记录。
利用hash的以上特点,就可以来实现前端路由更新视图但不重新请求页面的功能了。
简单实现
写个简单的实现例子:
<body>
<a class="a-class" href="#home">点我切换</a>
<div id="view">原视图</div>
<script>
// router-view组件
window.addEventListener("hashchange", (e) => {
console.log('hashchange', e);
// 这里就可以做视图的更新了
let viewDom = view // 想不到吧!还可以这样获取dom
viewDom.innerHTML = '视图切换' // 做视图的切换
});
// 页面初次加载,获取hash
document.addEventListener("DOMContentLoaded", () => {
console.log("最初始的hash:", location.hash);
// router-link组件
document.querySelectorAll(".a-class").forEach((item) => {
// 给每个a标签的点击事件加上监听
item.addEventListener(
"click",
(e) => {
// 阻止a标签的默认跳转事件
e.preventDefault();
// 获取a标签的链接内容
let link = item.getAttribute('href');
// 添加到锚点后,每次点击后url都能更新哈希值
location.hash = link;
},
false
);
});
});
</script>
</body>
所以流程大致为:
- 监听a标签的点击事件
- 阻止a标签的默认行为,并用
location.hash
更改哈希值 hashchange
监听事件中切换视图
优缺点
优点:
- 兼容性好,下到ie8
- 服务端不用配置啥东西
缺点:
- 地址有个
#
号,看起来不美观
History模式
解释
History interface是浏览器历史记录栈提供的接口,在过去时期,通过back()
,forward()
,go()
,打开本标签新地址等方法,我们可以改变浏览器历史记录栈的信息,进行各种地址跳转操作(其实就是控制浏览器的前进后退键),浏览器就会按照地址向服务端发送资源请求,服务器会返回对应的页面html文件,浏览器拿到后再进行渲染。
pushState与replaceState
从HTML5开始,History interface有了新增:pushState()
,replaceState()
window.history.pushState(stateObject, title, URL) // 往后新增一个路由记录
window.history.replaceState(stateObject, title, URL) // 把当前的路由记录替换
- stateObject: 一个对象类型的变量,可以带入一些信息;
- title: 所添加记录的标题;
- URL: 所添加记录的URL;
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器并没有前进后退,所以不会立即发送请求该URL,这就为单页应用前端路由更新视图但不重新请求页面提供了基础。
可以自己随便找个网址,然后控制台试试下面代码,你就会发现地址改变了,但页面什么都没有发生。
history.replaceState({}, null, '/b') // 替换路由
history.pushState({}, null, '/a') // 路由压栈
接下来我们只需要一个api,监听地址发生了改变,就去更新视图。这就是PopState
事件。
PopState
replaceState与pushState的调用并不会触发PopState,只有浏览器的前进后退键,back、forward、go等方法才会触发这个地址监听。
那咋整?咱可以直接手动调用。
触发了PopState后,也就能获取到replaceState与pushState他俩传过来的参数
window.addEventListener("popstate", (e) => {
// 这里就可以做视图的更新了 render()
console.log("onpopstate", e.state);
});
简单实现
<body>
<a class="a-class" href="/home">点我切换</a>
<div id="view">原视图</div>
<script>
// router-view组件
window.addEventListener("popstate", (e) => {
// 这里就可以做视图的更新了
console.log("popstate", e.state, location.pathname);
let viewDom = view // 想不到吧!还可以这样获取dom
viewDom.innerHTML = e.state.content // 做视图的切换
});
// 页面初次加载,获取 path
document.addEventListener("DOMContentLoaded", () => {
console.log("load", location.pathname);
// router-link组件
document.querySelectorAll(".a-class").forEach((item) => {
// 给每个a标签的点击事件加上监听
item.addEventListener(
"click",
(e) => {
e.preventDefault(); // 阻止a标签的默认跳转事件
let link = item.getAttribute('href'); // 获取a标签的链接内容
let data = { content: '视图切换' }
// 先检查是否有history对象,并且是否支持h5的新api
if (!!window.history && history.pushState) {
// 因为pushState的第一个参数可以传对象,所以能传的数据比hash模式多
window.history.pushState(data, link, link);
// 手动触发popstate
let popStateEvent = new PopStateEvent('popstate', { state: data });
dispatchEvent(popStateEvent);
} else {
// 安装polyfill补丁,不能就回到hash模式
}
},
false
);
});
});
</script>
</body>
所以流程大致为:
- 监听a标签的点击事件
- 阻止a标签的默认行为,并调用
pushState
改变路由地址,接着new PopStateEvent('popstate')
,再用dispatchEvent
手动触发popstate
事件 popstate
事件监听中切换视图
服务端配置
前端的都搞定了,还需要后端配置一下。因为浏览器每次改变地址后最终我们都是要触发资源请求的,所以需要让服务器知道,无论我们请求什么地址,都只给我们返回index.html的资源,这样浏览器收到后发现与现在访问的资源一样,就不会重新加载了。
在nginx中配置
server {
listen 80;
server_name www.xxx.com;
location / {
index /data/dist/index.html;
try_files $uri $uri/ /index.html; // 这个就是关键,只返回index.html
}
}
然后前端最后在自己的代码判断,如果地址不在路由表的范围内,跳去404页面。
优缺点
优点:
- 美观
缺点:
- 需要服务端支持
- 兼容性没到ie8