VueRouter 原理
手写前端路由背景
首先不局限在 Vue 框架里,扩大一点,前端为什么需要路由 router
通过不同的 URL 访问到不同的前端内容, 是前端在静态服务器中自带的功能(由静态服务器实现)
🤔 为什么现代前端应用还要手动
Js实现路由逻辑?
前端路由一般是在 SPA 单页应用里才需要手动实现的。
这是因为单页应用的概念就是希望页面不
reload而是局部渲染页面。这就限制了不能由静态服务器来通过URL加载前端页面(会reload)
👆 此时就产生了手动
Js实现单页应用的页面加载路由逻辑需求
手写前端路由实现思路
因此有2个大难点:
- 拦截浏览器原生的 切换
URL, 由自己Js实现的路由逻辑渲染目标页面内容 - 禁用浏览器原生的 切换
URL访问前端页面,防止reload整个单页
🤔 从这2个难点思考, 因为我们切换页面渲染逻辑只能通过 URL 变化, 并且 URL 能指出目标页面
👇 那就只能找 URL 变化但是不会触发浏览器 reload 的方法:
URL Hashhash是URL中#及后面的那部分,常用作锚点在页面内进行导航- 改变
URL中的Hash部分不会引起页面刷新 URl Query Params?参数的形式,会引起页面刷新 ❕
Window History API
URL Hash
👆 我们知道 URL Hash 是不会引起浏览器页面刷新的, 因此不需要 Js 手动实现禁止 页面reload
只需要 Js 实现拦截 URL 变化,并按照指定页面渲染即可
拦截 URL Hash 变化可以通过监听 hashchange 事件(由浏览器 window 事件监听器 EventListener 提供的 API 实现)
👇 原生 HTMl/JS 实现
<html>
<body>
<!-- 1. 通过a标签触发 URL 变化, 省去 Js 写跳转逻辑 -->
<ul>
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
</ul>
<!-- 2. 根据 URL Hash 显示的页面内容 placeholder -->
<!-- 当然可以不用 placeholder 直接往 body 下加 DOM -->
<div id="routeView"></div>
</body>
<script>
let routerView = routeView
// 3. 监听 Hash 切换
window.addEventListener('hashchange', ()=>{
let hash = location.hash; // 取出 URL 上的 Hash
routerView.innerHTML = hash // 根据 Hash 渲染相应的页面内容 这一步可引入 Vue 组件
})
</script>
</html>另外还需要处理边界问题,URL 首次加载页面 不会触发 hashchange
👇 因此需要 Js 手动实现监听首次加载 DOMContentLoaded 触发一次 Hash 逻辑
// 4. 监听 页面首次加载 load
window.addEventListener('DOMContentLoaded', ()=>{
if(!location.hash) {
// 如果不存在hash值,那么重定向到 #/ 并触发 hashchange 因此不用写渲染逻辑
location.hash = "/"
} else {
// 根据 Hash 渲染相应的页面内容 这一步可引入 Vue 组件
let hash = location.hash;
routerView.innerHTML = hash
}
})
History API
History 模式下,直接修改 URL 会触发页面 reload
通过 History API 修改 URL 则不会触发页面 reload
history.pushState()方法向当前浏览器会话的历史堆栈中添加一个状态(state) - MDNs
因此 a标签 的跳转, 需要禁用原逻辑, 而其他 Js 触发的跳转(如按钮逻辑)则需要限制只能用 History API
👇 页面首次加载 load 的时候, 遍历现有所有 a标签 绑定点击事件禁用原逻辑
window.addEventListener('DOMContentLoaded', onLoad)
function onLoad () {
var linkList = document.querySelectorAll('a[href]')
// 遍历现有所有 a标签 绑定点击事件禁用原逻辑
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault()
}))
}👆 除了要禁用 a标签 原逻辑, 还要做新的跳转逻辑
- 也就是使用
History API来修改URL为a标签上的href - 手动根据 URL 渲染对应页面
function onLoad () {
var linkList = document.querySelectorAll('a[href]')
// 遍历现有所有 a标签 绑定点击事件禁用原逻辑
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault()
// 使用 `History API` 来跳转 `a标签` 上的 `href` 指定页面
history.pushState(null, '', el.getAttribute('href')) // <-- this
// 手动根据 URL 渲染对应页面 监听 popstate 不会触发 下面会提到
routerView.innerHTML = location.pathname // <-- this
}))
}👇 同样的,首次加载也不会触发路由监听 因此需要 手动渲染首次加载时的 URL 对应的页面内容
function onLoad () {
// 根据首次加载时的 URL 渲染对应的页面内容
routerView.innerHTML = location.pathname // <-- this
var linkList = document.querySelectorAll('a[href]')
// 遍历现有所有 a标签 绑定点击事件禁用原逻辑
linkList.forEach(el => el.addEventListener('click', ()=>{}))
}👆 至此, 我们实现了跳转, URL 变化不触发页面 reload
浏览器测试时会发现 pushState 不允许在本地文件系统的源中使用 
👇 我们用一个静态服务器打开这个 HTML 文件 
👆 可以看出跳转不会触发 reload 但是浏览器的前进后退没有根据 URL 渲染相应的页面内容,而且在跳转后的路径刷新时, 这个静态服务器会报 404
每当激活同一文档中不同的历史记录条目时,
popstate事件就会在对应的window对象上触发。如果当前处于激活状态的历史记录条目是由history.pushState()方法创建的或者是由history.replaceState()方法修改的,则popstate事件的state属性包含了这个历史记录条目的state对象的一个拷贝。 - popstate - MDN
调用
history.pushState()或者history.replaceState()不会触发popstate事件。popstate事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在JavaScript中调用history.back()方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
👇 监听 popstate 浏览器后退前进触发根据 URL 渲染相应的页面内容
window.addEventListener('popstate', ()=>{
routerView.innerHTML = location.pathname
})至此就实现了 history 模式的路由机制了
但是刷新的 静态服务器 404 问题,需要静态服务的 nginx 配置一下如:符合某个目录下的路径(当然也可以所有静态服务器路径)都重定向到 单页应用根html 资源, 由 Js 路由机制渲染正确的页面
👇 原生 HTMl/JS 实现
<html>
<body>
<!-- 1. 通过a标签触发 URL 变化, 省去 Js 写跳转逻辑 -->
<ul>
<li><a href='/home'>home</a></li>
<li><a href='/about'>about</a></li>
</ul>
<!-- 2. 根据 URL Hash 显示的页面内容 placeholder -->
<!-- 当然可以不用 placeholder 直接往 body 下加 DOM -->
<div id="routeView"></div>
</body>
<script>
let routerView = routeView
// 3. 监听页面首次加载
window.addEventListener('DOMContentLoaded', onLoad)
function onLoad () {
// 6. 根据首次加载时的 URL 渲染对应的页面内容
routerView.innerHTML = location.pathname
// 4. 遍历现有所有 a标签 绑定点击事件禁用原逻辑
var linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault()
// 5. 使用 `History API` 来跳转 `a标签` 上的 `href` 指定页面
history.pushState(null, '', el.getAttribute('href'))
routerView.innerHTML = location.pathname
}))
}
// 7. 监听 `popstate` 浏览器后退前进触发根据 `URL` 渲染相应的页面内容
window.addEventListener('popstate', ()=>{
routerView.innerHTML = location.pathname
})
</script>
</html>补充
- 上面都是通过
a标签跳转的, 但是2种模式的跳转是截然不同的!hash时就是a标签默认事件直接修改URL, 但是history时是手动改写成我们封装的跳转事件 History API除了 pushState 还有 replaceState- 本文只是讲解
Vue路由(前端路由)的实现所基于的底层逻辑 VueRouter还把这些路由模式封装成一个Class类以及提供渲染组件的功能, 并通过Vue plugin的形式抛出 - 这部分为 VueRouter源码分析- 上面原生
HTML/JS实现的源码 html代码