前端路由两种模式的实现

135 阅读8分钟

前言

什么是路由?

想象你开车去一个陌生城市,手里拿着地图导航:

  • 目的地 = 你要访问的网站(比如淘宝/微信)

  • 十字路口 = 网络中的路由器

  • 路牌指示 = 路由规则

路由就是网络世界的导航系统

  1. 你在浏览器输入 taobao.com(告诉导航要去"淘宝大厦")

  2. 路由器(路口交警)查看"地址簿"(路由表):

    • "淘宝大厦 → 走3号高速出口"
    • "微信大厦 → 走5号隧道"
  3. 数据包像快递车一样,经过多个路口(路由器),每个路口根据"地址"决定下一站方向

  4. 最终准确到达淘宝服务器(目的地)

核心作用:把网络请求精准送到目标服务器,就像快递分拣站根据地址分发包裹

那什么是前端路由呢?

想象一本魔法相册:

  • 相册本身 = 你的浏览器页面(始终不刷新)

  • 照片 = 不同页面内容(首页/购物车/个人中心)

  • 目录标签 = 浏览器地址栏的URL(如 site.com/#cart

传统网站(无前端路由)

  • 想看购物车?撕掉当前页 → 电话联系印刷厂(服务器)→ 等新的一页寄过来 → 贴到相册里

  • 每次都要重印整本相册(页面刷新)

前端路由的魔法相册

  1. 相册自带所有照片(首次加载全部页面资源)

  2. 点"购物车"标签时:

    • 悄悄改地址栏:site.com/#cart(但不相册翻页)
    • 瞬间从夹层抽出购物车照片覆盖当前页(内容切换)
  3. 点"个人中心"标签时:

    • 地址栏变 site.com/#profile
    • 快速抽出个人中心照片覆盖
  4. 关键魔法:相册本身始终没换!只是快速抽换内页照片

核心技术:
Hash模式:用 #cart 这类锚点(像书签页签)
History模式:用 /cart 伪装成新页面(地址栏更整洁)

通俗点来讲就是,一直保持一个html页面,当浏览器url地址发生改变时,更换对应的组件展示在页面上(不触发页面刷新),前端路由就是用来构建url地址与组件之间的映射关系的

在了解前端路由的两种模式之前,我们需要知道实现前端路由的两大问题:
1. 怎么知道url地址变更了?
2. 怎么实现修改url(切换组件),浏览器不能刷新?

Hash模式

什么是Hash模式?

想象一本 带便利贴的书

  1. 书本身 = 你的网站(index.html

  2. 便利贴位置 = URL 中 # 后面的部分(如 site.com/#page2

  3. 翻书规则

    • 你想看第二章 → 直接翻到贴了 #page2 便利贴的那页

    • 书不用重新买(页面不刷新)

    • 别人复制 site.com/#page2 给你 → 你也能精准翻到那页

  4. 秘密机制

    • 浏览器无视 # 后面的内容发给服务器 → 永远只拿到整本书(index.html

    • 前端路由偷偷看便利贴(监听 # 变化) → 展示对应章节(组件)

简单来说就是,浏览器不会将#后面的任何内容发送给后端,所以在url地址上加上#,后面再加任何内容都不会触发浏览器的刷新,那么这就解决了第二个问题,之后便通过hashchange事件监听url的变更,然后替换对应的组件即可。

如何实现呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>
            <!-- href属性使用hash模式,以#开头 -->
            <a href="#/home">首页</a>
        </li>
        <li>
            <a href="#/about">关于</a>
        </li>
    </ul>
    <!-- 用于展示路由对应的组件内容的容器 -->
    <div id="root"></div>
</body>
<script>
    // 定义路由配置数组
    const routes=[
        {
            path:'/home',  // 路由路径
            component:()=>{  // 路由对应的组件(函数形式返回HTML字符串)
                return '<h1>首页</h1>'
            }
        },
        {
            path:'/about',
            component:()=>{
                return '<h1>关于</h1>'
            }
        }
    ]
    
    // 监听hashchange事件,当URL中的hash部分变化时触发
    window.addEventListener('hashchange',()=>{
        // 调用renderView函数,传入当前hash值
        renderView(location.hash)
    })
    
    // 根据传入的URL hash值渲染对应的组件
    function renderView(url){
        // 在routes数组中查找匹配的路由
        // 注意:这里需要将item.path前面加上#进行比较,因为location.hash包含#
        const index=routes.findIndex(item=>{return '#'+item.path===url})
        
        // 如果找到匹配的路由
        if(index!==-1){
            // 将对应组件渲染到root容器中
            document.getElementById('root').innerHTML=routes[index].component()
        }
    }
    
    // 监听DOMContentLoaded事件,确保页面加载完成后执行
    window.addEventListener('DOMContentLoaded',()=>{
        // 页面加载完成后,根据当前hash值渲染初始视图
        renderView(location.hash)
    })
</script>
</html>

使用a标签,当用户点击a标签时便可将url地址更换,再监听hashchange事件,获取当前url地址中#后面的值(包括#),然后再将获取的hash值拿去与routes数组进行匹配,找到对应的组件并渲染,最后监听DOMContentLoaded事件,只有当页面 DOM 完全就绪后,才会根据当前 URL 的 hash 值来渲染对应的组件内容到 #root 容器中,确保页面加载完成后,立即根据当前 URL(或 hash)渲染对应组件,保证用户打开页面时能看到正确的初始内容。

History模式

什么是history模式?

想象一个 会魔法的智能写字板

  1. 写字板本体 = 你的网站(index.html

  2. 板上的字 = URL 中的路径(如 site.com/cart

  3. 魔法操作

    • 你想写 /cart → 用魔法笔一挥,板子瞬间变成购物车页面

    • 板子还是那块板子(页面不刷新)

    • 地址栏显示 site.com/cart像全新页面!

  4. 风险点

    • 如果直接访问 site.com/cart → 浏览器会真去服务器找 cart.html(但服务器没有!)

    • 需要提前告诉服务器: “所有路径都给我 index.html!” (否则报错 404)

history模式和hash模式的区别之一就是history模式没有了#,使得url地址变得更美观了但同时也带来了一个问题,history模式下的url地址和传统地址相像,当用户直接访问这种url地址时(如example.com/about ) ,浏览器会向后端请求该路径下的资源,但服务器并没有该路径的资源(没有about.html),服务器只有主页面html(index.html)的资源,所以会报错404,这时候就需要配置服务器,告诉服务器,无论什么地址的请求,都返回主页面html的资源,然后由前端路由根据不同的url地址加载对应组件。

如何实现呢?

hash模式可以改变#后面的url地址,达到页面不刷新的效果,但是history模式没有#号,而是和普通路径一样,所以只能通过浏览器提供的history对象,调用它的pushState方法将url地址添加到浏览器历史记录栈中,同时也不触发页面的更新,还需要绑定popstate事件,当用户点击浏览器的前进和后退按钮时,浏览器便被通知历史记录发生变更,需要重新根据url地址更换对应的组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>
            <!-- 导航链接,使用History模式(无#前缀) -->
            <a href="/home">首页</a>
        </li>
        <li>
            <a href="/about">关于</a>
        </li>
    </ul>
    <!-- 用于展示路由对应的组件内容的容器 -->
    <div id="root"></div>
</body>
<script>
    // 定义路由配置数组
    const routes=[
        {
            path:'/home',  // 路由路径
            component:()=>{  // 路由对应的组件(函数形式返回HTML字符串)
                return '<h1>首页</h1>'
            }
        },
        {
            path:'/about',
            component:()=>{
                return '<h1>关于</h1>'
            }
        }
    ]
    
    // 获取用于渲染组件的DOM容器
    const renderView=document.getElementById('root')
    
    // 页面加载完成后,初始化路由
    window.addEventListener('DOMContentLoaded',()=>{
        onLoad()
    })
    
    // 监听popstate事件,处理浏览器前进/后退操作
    window.addEventListener('popstate',()=>{
        // 根据当前URL路径切换组件
        routerView(location.pathname)
    })
    
    // 初始化函数,设置导航链接的点击事件
    function onLoad(){
        // 获取页面上所有带有href属性的a标签
        let linkList=document.querySelectorAll('a[href]')
        
        // 遍历所有a标签
        linkList.forEach(el=>{
            // 为每个a标签添加点击事件监听
            el.addEventListener('click',(e)=>{
                // 阻止a标签的默认跳转行为(防止页面刷新)
                e.preventDefault()
                
                // 使用pushState修改URL,添加新的历史记录
                // el.getAttribute('href')获取a标签的href属性值
                history.pushState(null,'',el.getAttribute('href'))
                
                // pushState会进入浏览器的历史栈中,使前进/后退按钮有效
                // 但是不会触发popstate事件,所以需要手动调用routerView函数
                
                // 根据新的URL路径切换组件
                routerView(location.pathname)
            })
        })
    }
    
    // 根据URL路径切换对应的组件
    function routerView(url){
        // 在routes数组中查找匹配的路由
        const index=routes.findIndex(item=>{return item.path===url})
        
        // 如果找到匹配的路由
        if(index!==-1){
            // 调用组件函数并将结果渲染到容器中
            renderView.innerHTML=routes[index].component()
        }
    }
</script>
</html>
  1. 在单页应用(SPA)中,虽然a标签可实现页面跳转,但会导致整页刷新。因此首先需阻止其默认跳转行为(通过 e.preventDefault() ),使其仅作为交互触发元素而非传统跳转标签。

  2. 获取a标签的 href 属性值(使用 el.getAttribute('href') ),该值即为目标跳转的URL路径。

  3. 使用 history.pushState(null, '', targetUrl) 方法更新浏览器URL地址,此操作不会触发页面刷新,但会将新URL添加到浏览器历史记录栈中,使前进/后退按钮可用。

  4. 将获取到的URL路径与预定义的 routes 数组进行匹配(通常根据 path 字段),找到对应的路由配置项,并获取其关联的组件函数。

  5. 调用组件函数生成HTML内容,并将其渲染到页面指定容器(如 root 元素)中,完成视图更新。

  6. 最后,需监听 popstate 事件以处理浏览器的前进/后退操作。当用户点击前进/后退按钮时,会触发该事件,此时需重新调用路由匹配渲染函数(如 routerView ),根据当前URL路径切换显示对应的组件。