前端路由,是指前端应用中管理页面导航和url的机制。前端路由使得单页面应用能够在用户交互时动态的加载不同的视图,而不需要每次都重新加载整个页面,即刷新页面。
单页面和多页面的区别:
1. 单页面就是只有一个主页面的应用,只需要在第一次时请求一次js,css,html等相关资源,页面的切换时不需要重新加载整个页面,而是通过ajax和javascript动态的更新DOM
2. 多页面就是有多个独立页面的应用,每个页面都是独立的html文件,每个页面都需要重复请求js,css,html等相关资源,页面切换时需要重新加载整个页面,需要再次请求相关资源
vue-router,React Router等都属于前端路由库,这些库提供了一组api和组件,用于定义路由规则、处理导航事件和渲染相应的视图。
前端路由实现原理
路由这个词最早是来自服务器,路由是服务端用来描述路径的,换句话说就是URL和文件的映射关系。
后来前端借鉴了路由的概念,用于描述URL和组件的映射关系。浏览器的URL变成了需要映射到页面的某个组件,URL变成了需要展示某个组件。/Home和Home.vue,/about和About.vue就是一一映射关系。
区别
hash
- 原理
在 url 中的 # 之后对应的是 hash 值, 其原理是通过hashChange() 事件监听hash值的变化, 根据路由表对应的hash值来判断加载对应的路由加载对应的组件
- 优点
- 只需要前端配置路由表, 不需要后端的参与
- 兼容性好, 浏览器都能支持
- hash值改变不会向后端发送请求, 完全属于前端路由
- 缺点
- hash值前面需要加#, 不符合url规范,也不美观
- 分析
当 URL 改变时,页面不会重新加载。
hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;
同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据
history
- 优点
- 符合url地址规范, 不需要#, 使用起来比较美观
- 缺点
- 在用户手动输入地址或刷新页面时会发起url请求, 后端需要配置index.html页面用户匹配不到静态资源的情况, 否则会出现404错误
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面
但是这样又会有一个问题:这样做了以后,服务器就不再返回404错误页面了,因为对于所有路径都会返回index.html文件。为了避免这种情况,应该在Vue引用里面覆盖所有的路由情况,然后再给出一个404页面:
```js
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})
解释一下,*这个通配符用于匹配所有未匹配到的路径,即表示没有与之匹配的路径。因此,当用户访问的路径没有在路由表中找到匹配项时,会匹配到这个路由通配符路由。可以使得在应用中处理所有未知的路径,并为他们提供自定义的处理方式,例如显示一个404页面。
- 兼容性比较差, 是利用了 HTML5 History对象中新增的 pushState() 和 replaceState() 方法,需要特定浏览器的支持.
- 分析
利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
pushState()方法可以改变URL地址且不会发送请求;
replaceState()方法可以读取历史记录栈,还可以对浏览器记录进行修改。
这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。
只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
上面有些问题是与URL有关的:
一般URI有一个通用的结构描述:
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
主要资源是由URI进行标识,URI中的fragment用来标识次级资源。换言之,fragment就是用来标识uri所标识资源里的某个资源。
在URI的末尾通过hash mark(#)作为fragment的开头,其中#不属于fragment的值。
fragment的特点:
- #有别于?,?后面的查询字符串会被网络请求带上服务器,而fragment不会被发送的服务器
- fragment的改变不会触发浏览器刷新页面,但是会生成浏览记录
- frament会被浏览器根据文件媒体类型进行对应处理。
具体来说,浏览器在处理HTML文档时,会根据文档的媒体类型(通常是**`text/html`**)和fragment的内容来决定如何处理它。主要有以下几种情况:
1. **HTML文档**:
- 如果浏览器加载的文档是HTML文档,并且fragment指定了一个存在于文档中的元素的ID,则浏览器会自动滚动到该ID所对应的元素处。这被称为页面内链接(page anchor),浏览器会自动将视图滚动到指定ID的元素位置,使用户能够方便地查看页面的特定部分。
2. **其他媒体类型**:
- 如果浏览器加载的文档不是HTML文档,而是其他类型的文档(例如图片、视频、音频等),或者是未知类型的文档,则fragment通常被忽略,浏览器会简单地加载整个文档。在这种情况下,fragment不会影响文档的加载或显示。
- Google的搜索引擎会忽略#及器后面的字符串
frament的应用:
- 单页面路由。JavaScript提供了
location.hash
来操作当前URI的fragment/读取当前URI的fragment,同时提供了HashChange
事件监听fragment的变化。结合这两个api在结合上述特点1,2就可以实现一个简单前端路由:
修改location.hash
的值,触发HashChange
事件,js处理对应的逻辑,改变页面ui实现页面的跳转,并在浏览器中产生历史记录
- HTML锚点。在HTML中比较常见的一个应用-页面内定位。在页面中通过设置标签的id属性来定义锚点,从而实现锚点定位。实际上锚点定位的实现依赖了fragment的特点3。例如这个URI:
https://domain/index.html#L18
,假设返回的文件类型是text/html,则浏览器会读取URI的fragment,然后在页面中寻找#L8这个锚点,并将页面滚动到该锚点的位置。
因此我们当点击 <a href="#top">top</a>时,实际上处理过程是 URI 的 hash 发生变化,然后浏览器读取新的 fragment,并寻找 DOM 中是否存在对应的锚点,将该锚点显示到页面中。在 MIME Type 为 HTML 或 XML 时,如https://domain/index.html#这个 URI 中是空的 fragment,则浏览器默认显示页面的最顶端。
- 特点4 其实是针对 hash 模式前端路由来说的一个缺点。因为 fragment 会被 Google 搜索引擎忽略掉,因此对于用 hash 模式前端路由的应用的 SEO 来说是很不友好的。不过 Google 给了一个方案,就是在
#
紧跟一个!
,这样Google 搜索引擎就会将这个 URI 进行转换,如https://domain/index.html#!L18
转换后就成为了https://domain/index.html?_escaped_fragment_=L18
。这样搜索引擎就会携带上 URI’s fragment 直接去访问这个 URI,开发者可以利用这个 trick 优化网站的 SEO。
实现路由
实现路由需要实现:
- 如何修改URL还不引起页面的刷新
- 如何知道URL变化了
哈希Hash
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<ul>
<li><a href="#/home">首页</a></li>
<li><a href="#/about">关于</a></li>
</ul>
<div id="routeView"></div>
</body>
<script>
const routes = [
{
path: '#/home',
component: '首页内容',
},
{
path: '#/about',
component: '关于页面内容',
},
];
// hashchange事件监听fragment的变化
window.addEventListener('hashchange', onHashChange);
// 想要hashchangge在页面初次加载时就触发一次,就监听一次dom结构的事件,dom一出来就执行一次
window.addEventListener('DOMContentLoaded', onHashChange);
function onHashChange() {
// 获取当前URL中的fragment
const hash = location.hash;
routes.forEach((item, index) => {
if (hash === item.path) {
document.querySelector('#routeView').innerHTML = item.component;
}
});
}
</script>
</html>
History
浏览器中有个会话历史栈
,它可以维护你的访问路径,有了这个你返回就可以按照栈的顺序进行前进回退。
pushState
提到了popState
,他是靠popState
监听url的改变的,并且仅当浏览器前进后退时生效
pushState
可以修改URL并且不引起页面的刷新。
pushState()
和 popstate
之间存在着一种关系,它们共同组成了 HTML5 History API 的一部分,用于在浏览器历史记录中进行导航和管理。
- pushState():
pushState()
方法用于向浏览器的历史记录栈中添加一个新的状态。它接受三个参数:状态对象(state object)、标题(title)和 URL。当调用pushState()
方法时,浏览器不会立即加载新的页面,而是将当前的状态推入历史记录栈,并更新当前的 URL。这意味着你可以使用pushState()
方法来改变 URL,而不会导致页面的重新加载。这个方法通常与前端路由配合使用,用于在单页面应用中切换视图而不刷新页面。 - popstate 事件:
popstate
事件是在用户点击浏览器的前进或后退按钮时触发的事件。当历史记录发生改变时(例如通过调用pushState()
方法添加新的状态或通过浏览器导航按钮进行导航),浏览器会触发popstate
事件。通过监听popstate
事件,你可以在历史记录发生改变时执行相应的操作,例如更新页面内容以反映当前的状态。
因此,pushState()
用于向历史记录栈中添加新的状态,而 popstate
事件则用于在历史记录发生改变时通知页面,并且在这种情况下可以执行相应的操作。这两者结合使用可以实现在单页面应用中进行导航、路由和状态管理的功能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<ul>
<li><a href="/home">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
<div id="routeView"></div>
<script>
const routes = [
{
path: '/home',
component: '首页内容',
},
{
path: '/about',
component: '关于页面内容',
},
];
const routeView = document.getElementById('routeView');
window.addEventListener('DOMContentLoaded', onLoad);
window.addEventListener('popstate', onPopState);
function onLoad() {
const links = document.querySelectorAll('li a'); // 获取所有的li下的a标签
// console.log(links)
links.forEach((a) => {
// 禁用a标签的默认跳转行为
a.addEventListener('click', (e) => {
console.log(e);
e.preventDefault(); // 阻止a的跳转行为
// pushState可以修改URL,但不引起页面的刷新
// pushState有三个参数:
// 1. null
// 2. 空字符串
// 3. 新的URL
// 新的URL肯定是点了什么就放什么URL,所以需要读取到a标签的href值
history.pushState(null, '', a.getAttribute('href')); // 核心方法 a.getAttribute('href')获取a标签下的href属性
// 映射对应的dom
onPopState();
});
});
}
function onPopState() {
routes.forEach((item) => {
if (item.path === location.pathname) {
routeView.innerHTML = item.component;
}
});
}
</script>
</body>
</html>