前端路由是什么?(what)
这个问题要从页面路由的发展历程来看。
传统的后端路由:通过不同的 URL 向后端请求不同的 HTML 页面。
“新潮”的前端路由:看起来好像访问了不同的页面,但实际上没有向后端发起过多次请求,访问的是同一份 HTML,前端识别 URL 变换来动态切换页面内容。
为什么会产生前端路由?或者说有什么好处?(why)
- 前后端解耦:日渐复杂的前端应用对于前后端解耦提出了更高的要求。使用后端路由,前端切换页面需要后端配置相应的路由,也就意味着沟通成本、逻辑耦合;而使用前端路由,页面的切换成为纯粹的交互逻辑,无需后端关心。
-
更优的用户体验:切换页面无需刷新,用户体验更佳。
前端路由怎么实现的?(how)
Demo 简介
先来看一下 demo 动画:
Demo 实现了 hash 模式和 history 模式。每个模式下的按钮代表不同的路由路径,蓝色方框表示对应的路由页面,页面内容是显示当前激活的路由名称。
用这样一个 HTML 片段来实现这个简单的页面布局:
<body>
<h1>实现一个前端路由</h1>
<h2>hash 模式</h2>
<div id="hash-btn">
<button>page1</button>
<button>page2</button>
</div>
<div id="hash-view"></div>
<h2>history 模式</h2>
<div id="history-btn">
<button>history-page1</button>
<button>history-page2</button>
</div>
</body>
简单地定义一个加载页面的函数:它的功能就是将当前路由对应的页面加载到视图区域。
function loadPage(page, viewId) {
const view = document.querySelector(`#${viewId}`)
view.innerHTML = page
}
Hash 模式
然后我们来看 hash 模式的实现:
- 为每个切换路由的按钮,添加事件监听,以便在点击时,将 URL 中的 hash 修改为对应值。核心就是监听 click 事件,修改 location.hash。
-
为 hashchange 事件定义回调函数,以便在路由发生变化时,加载对应的页面。
页面事件流程:
- 点击按钮
- 触发 click 事件
- 修改 location.hash 为当前按钮对应的路由
- 因为 hash 修改,触发了 hashchange 事件
- 加载对应的路由页面
function initHash() {
// 为每个按钮添加事件监听
const buttons = Array.from(document.querySelector('#hash-btn').childNodes).filter(node => node.nodeName === 'BUTTON')
buttons.forEach(button => {
button.addEventListener('click', () => {
// 修改哈希值
location.hash = button.innerText
})
})
loadPage(location.hash, 'hash-view')
window.onhashchange = function() {
// 挂载对应的组件
const page = location.hash
loadPage(page, 'hash-view')
}
}
// 哈希模式
initHash()
History 模式
最后再来看下 history 模式的实现。与 hash 模式是一样的思路,只是其中3、4步骤的具体实现不同。
页面事件流程:
- 点击按钮
- 触发 click 事件
- 调用 pushState 在路由列表末尾新增一项
- 触发了 popstate 事件(划重点!)
-
加载对应的路由页面
注意,步骤 4 中,pushState 和 replaceState 方法原本并不触发 popstate 事件,所以这里我们要想办法让它触发事件。具体方法就是在原有方法上,增加 dispatch(new Event('popstate') ,再将对象方法指向这个新方法。具体实现如下:
// 劫持原方法并做发射事件处理
const wrappedFn = function (eventName, ) {
const originFn = window.history[eventName]
const event = new Event('popstate')
return function(...args) {
const ret = originFn.apply(this, args)
event.state = args[0]
window.dispatchEvent(event)
return ret
}
}
window.history.pushState = wrappedFn('pushState')
window.history.replaceState = wrappedFn('replaceState')
添加 popstate 的巧妙之处还在于,go、forward、back 方法本身会触发 popstate 事件。这样我们只需要监听 popstate 这一个事件就足够了。
// 历史模式
function initHistory() {
// 为每个按钮添加事件监听
const buttons = Array.from(document.querySelector('#history-btn').childNodes).filter(node => node.nodeName === 'BUTTON')
buttons.forEach(button => {
button.addEventListener('click', () => {
// 修改路由状态
window.history.pushState({state: button.innerText}, `${button.innerText}页面` ,`${button.innerText}.html`)
})
})
window.addEventListener('popstate', (e) => {
loadPage(e.state?.state, 'history-view')
})
// 初始化状态
window.history.replaceState({state: 'router'}, '', 'router.html')
}
initHistory()
*常见问题:history 模式刷新 404 问题
原因:都没能进入正确的页面,当然无法执行任何逻辑了
解决方案:借助 nginx 配置跳转到页面内;由于页面内有对 path 的监听,就可以直接加载正确的页面啦!
Hash VS history,选择哪一个更好?
| 模式 | hash | history |
|---|---|---|
| 优点 | 实现方式更简单 | 更主流的方式 |
| 缺点 | 不美观,路径中带有 # 符号 ;会导致描点功能失效; 相同 hash 值不会触发,不会记录到历史中 | 需要服务端协助解决刷新 404 问题 |
【附】完整代码链接
PS:在 vscode 中,安装 Live Server 插件后,右键选择 open with Live Server 就可以看到效果啦~