什么是路由?路由就是用来描述服务器上的资源的路径。本文我们来理解前端路由。
一、为什么要理解前端路由?
在传统多页面应用中,每次切换页面都要重新请求 HTML,这样如果用户网络波动较大,就会导致用户切换页面时浏览器上一片空白,需要等待几秒才能渲染出来。而单页应用不刷新页面,而是只切换组件。
单页应用怎么实现呢?我们这里用原生 JS 简单的实现一下:
html
<body>
<header>
<nav>首页</nav>
<nav>沸点</nav>
</header>
<div id="root"></div>
</body>
js
const content = [
'<h2 class="home">首页</h2>',
'<h3 class="hot">沸点</h3>',
]
let navs = document.getElementsByTagName('nav');
Array.from(navs).forEach((nav, index) => nav.addEventListener('click', () => {
document.getElementById('root').innerHTML = content[index];
}))
这样就实现了一个点击导航能够手动切换 root 中的内容的简单单页应用,但是并没有改变 URL,只能实现"组件切换"。
前端路由的核心作用是:
- 改变浏览器 URL
- 页面不刷新
- 根据 URL 找到对应组件并渲染
也就是说,前端路由是构建浏览器 URL 地址和组件之间的映射关系。
二、前端路由的核心流程
- 修改浏览器 URL,但不刷新页面
- 监听 URL 的变化
- 根据 URL 从映射表中找到对应的组件并渲染
我们有两种方法来实现:hash 路由和 history 路由
三、hash 原理
- hash 是什么?
例如:
其中:#/home 就是 hash。 也就是说,浏览器会将 URL 地址后面接"#" 的这串值会被认为是一串 hash 值。
- hash 的特点
- URL 中 # 后面的内容变化不会导致页面刷新
- 可以通过
location.hash获取当前的 hash - 可以监听
hashchange事件
- hash 路由实现步骤
- 定义路由映射表
- 点击导航改变 hash
- 监听 hashchange
- 根据 location.hash 找到对应的组件
- 渲染到页面
- 映射表
js
const routes = [
{
path: '/home',
component: () => {
return '<h2>首页页面</h2>'
}
},
{
path: '/hot',
component: () => {
return '<h3>沸点页面</h3>'
}
}
]
- 监听 hashchange
js
// 知道url变更了
window.addEventListener('hashchange', () => {
// 在routes中找到对应的path,并渲染对应的组件
renderView(location.hash)
})
- 渲染函数
js
function renderView(hashVal) {
hashVal = hashVal.replace('#', '')
let route = routes.find(item => item.path === hashVal)
if(route) {
document.getElementById('root').innerHTML = route.component()
}
}
- 处理首次加载
js
// 初次刷新页面
window.addEventListener('DOMContentLoaded', () => {
renderView(location.hash)
})
- hash 路由小结 优点:
- 实现简单
- 兼容性好
- 切换 URL 不会刷新页面
缺点:
- URL 中有
# - 不够美观
- 不利于一些 SED 场景
四、history 路由原理
- history 是什么?
history 是浏览器提供的一个对象,用来管理浏览器的历史记录。
- 提供的方法
- pushState() 向浏览器的历史记录栈中添加一条记录
- popState() 从浏览器的记录中出栈一条记录
- replaceState() 替换浏览器历史记录中的栈顶记录
pushState() 可以修改 URL 且不带来页面刷新,监听 popState() 来关联浏览器的前进后退事件
五、手写 history 路由
- 定义路由表
js
const routes = [
{
path: '/home',
component: () => {
return '<h2>history 首页页面</h2>'
}
},
{
path: '/hot',
component: () => {
return '<h3>history 沸点页面</h3>'
}
}
]
- 阻止默认跳转
js
e.preventDefault()
- 使用 pushState 修改 URL
js
history.pushState(null, '', el.getAttribute('href')) // 进入浏览器的缓存栈
这一步可以做到:
- URL 改变
- 页面不刷新
- 浏览器历史记录增加一条
- 手动渲染页面
js
renderView(location.pathname)
- 监听浏览器前进后退
js
window.addEventListener('popstate', () => {
renderView(location.pathname)
})
- 渲染函数
js
function onLoad() {
// 路径变更,渲染对应组件
let linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault() // 阻止 a 标签默认跳转行为
history.pushState(null, '', el.getAttribute('href')) // 进入浏览器的缓存栈
renderView(location.pathname)
})
})
}
function renderView(historyPath) {
routes.forEach(item => {
if (item.path === historyPath) {
document.getElementById('root').innerHTML = item.component()
}
})
}
六、总结
前端路由的本质,是在不刷新页面的情况下,根据浏览器 URL 的变化来切换页面内容。它通过维护一套路由映射表,将不同的路径和不同的组件对应起来。当 URL 发生变化时,前端会监听这个变化,找到对应的组件并渲染到页面中。
常见的前端路由实现方式有两种: hash 模式和 history 模式。 hash 模式通过监听 hashchange 事件实现路由切换,特点是实现简单、兼容性好,但 URL 中会带有 # 。 history 模式通过 history.pushState 修改 URL,并配合 popstate 监听浏览器前进后退,URL 更加美观,但项目上线时需要服务端配置兜底,否则刷新页面可能会出现 404。