Vue3基础:带你手搓简易vue-router理解实现原理

550 阅读6分钟

路由

在传统的多页面应用(MPA) 中,服务端路由是非常常见的。当用户在浏览器中输入 URL 或点击链接时,浏览器会向服务器发送请求,服务器端的路由系统会解析请求的 URL,并确定应该返回哪个HTML页面。每个URL对应服务器上的一个独立的资源或页面。

单页面应用(SPA) (vue就是单页面应用)中,情况有所不同。SPA是一种Web应用程序架构,其核心思想是在加载应用程序时只加载一个HTML页面,并通过JavaScript动态地更新该页面,而不需要每次交互都向服务器请求新的HTML页面。

单页应用只有一个html页面,页面切换url不变,想保持原来服务端路由的特点,来维持url和组件的映射关系,这就是前端路由要解决的问题。

实现前端路由需要解决的问题

单页应用在一般页面变化时url并不会直接改变,但是我们想模拟传统多页应用中路由显示特点,希望页面切换url对应也变化,于是我们需要考虑两个核心问题

  1. 如何修改url,还不引起页面的刷新(跳转)
  2. 如何知道url变化了

我们可以通过js实现路由的hash模式和history模式来解决

hash

浏览器中url后面拼接 #xxx 会被认为是hash值,而hash值的变更,是不会引起浏览器页面的刷新。我们就能实现页面切换同步url变化,但是页面不跳转。

特点

在网址后加'/'回车后网页刷新 网址后面加任意内容跳转示例.gif

在网址后加其它内容回车后网页刷新

网址后面加任意内容跳转示例1.gif

在网址后面加'#'后接任意内容回车不刷新

网址后加#号不跳转.gif

我们编写一个页面来实现hash模式下的前端路由

代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        ul {
            display: flex;
        }

        li {
            display: inline-block;
        }
    </style>
</head>

<body>
    <ul>
        <li><a href="#/home">首页</a></li>
        <li><a href="#/about">关于</a></li>
    </ul>
    <div id="routerView">
        内容显示区
    </div>

    <script>
        const routes = [
            {
                path: '#/home',
                component: '首页页面内容'
            },
            {
                path: '#/about',
                component: '关于页面内容'
            }
        ]
        const routerView = document.getElementById('routerView')
        window.addEventListener('hashchange', onHashChange);
        function onHashChange(event) {
            // 处理URL变化的逻辑
            //console.log(window.location.hash);//直接打印location也是一样的效果
            routes.forEach((item, index) => {
                if (item.path === location.hash) {
                    routerView.innerHTML =
                        item.component
                }
            });
        }
        //解决只能点击才会触发onHashChange事件
        window.addEventListener('DOMContentLoaded',onHashChange)
    </script>
</body>

</html>

代码解释

  • 一般修改url的主要方式:

    1. a标签
    2. 浏览器的前进后退
    3. window.location的前进后退
  • 这里我们通过a标签中跳转的路径开头设置为'#',这样点击a标签后,页面url后面添加上a标签中设置的路径,因为是变更了url的哈希部分,所以页面并没有刷新

  • 在script标签中定义一个对象数组,每个对象有跳转路径属性path,以及跳转之后的要展示的内容component

  • 设置一个监听事件,监听hashchange(更改当前页面url中的哈希部分),当url改变就会执行函数onHashChange

  • onHashChange会读取当前的window对象的location.hash属性也就是url中的哈希部分,再遍历对象数组,如果该对象路径path和当前url的哈希部分匹配就将其component拿到页面中展示

效果

hash模式路由的实现效果.gif

history

因为前端路由的hash模式增加'#'号,有点不好看,当然也有去掉'#'更简洁的history模式

原理:

  • 解决页面刷新问题: history模式下,更改url之后页面会被刷新,但是我们想更改url又不想刷新页面,js中提供了一个pushState方法,可以修改url且不会引起页面刷新;

  • 解决组件和url的映射问题: 那我们也要完成url与组件的映射关系可以通过location.pathname属性获取URL中的路径部分,之后通过匹配对应的url展示相应的页面内容,通过js提供的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>
    <style>
        ul {
            display: flex;
        }

        li {
            display: inline-block;
        }
    </style>
</head>

<body>
    <ul>
        <li><a href="/home">首页</a></li>
        <li><a href="/about">关于</a></li>
    </ul>
    <div id="routerView">
        放一个代码片段
    </div>

    <script>
        const routerView = document.getElementById("routerView");
        const routes = [
            {
                path: '/home',
                component: '首页页面内容'
            },
            {
                path: '/about',
                component: '关于页面内容'
            }
        ]
        const links = document.querySelectorAll('li a')
        // console.log(links);
        links.forEach(a => {
            a.addEventListener('click', (e) => {
                // console.log(e);
                //阻止了a标签的默认跳转行为
                e.preventDefault();
                //添加一种可以修改url又不造成页面刷新
                history.pushState(null, '', a.getAttribute('href'));
                //映射对应的DOM
                onPopState()
            })
        })
        function onPopState() {
            // console.log(location.pathname);
            routes.forEach((item) => {
                if (item.path == location.pathname) {
                    routerView.innerHTML = item.component
                }
            });
        }
        function onLoad() {
            onPopState()
            const links = document.querySelectorAll('li a')
            links.forEach(a => {
                //为每个a标签添加点击事件
                a.addEventListener('click', (e) => {
                    e.preventDefault()  // 阻止了a标签的默认跳转行为
                    // 添加一个可以修改url又不造成页面刷新
                    history.pushState(null, '', a.getAttribute('href'))
                    // 映射对应的dom
                    onPopState()
                })
            })
        }

        window.addEventListener('DOMContentLoaded', onLoad)
        //监听页面因为浏览器前进后退导致的url变化事件
        window.addEventListener('popstate', onPopState)
    </script>
</body>

</html>

代码解释

  • 因为点击a标签会更改url,而history模式下,更改url会导致页面刷新,这不是我们想要的效果,所以我们使用a标签的自带函数preventDefault(),阻止了a标签的默认跳转行为
  • 那么此时a标签就不能修改url,当然js中有我们需要的修改url但是不引起页面刷新的方法pushState,我们给a标签绑定点击事件,点击事件执行自定义函数onPopState,在onPopState中使用pushState方法来更改url
  • 自定义函数onPopState,函数通过读取当前页面的url的路径部分location.pathname,再遍历路径对象,如果路径参数path匹配就将该对象的component展示到页面上
  • 在浏览器ui界面进行前进后退,改变url的话并没有触发点击事件,所以我们需要为这个情景补充一个监听事件window.addEventListener('popstate', onPopState),popstate事件,仅在浏览器ui界面进行前进后退,改变url触发

效果:

history模式下的路由效果.gif

总结

1. 背景

在传统多页面应用(MPA)中,服务端路由常见,而单页面应用(SPA)通过前端路由实现页面切换和导航,从而提升用户体验。本文讨论了前端路由的简易实现,重点介绍了 hash 模式和 history 模式。

2. hash 模式

  • 特点: 在 URL 后面加 #,不会引起页面刷新。
  • 解决问题: 实现页面切换同步 URL 变化,但不进行页面跳转。
  • 实现原理: 利用 hashchange 事件监听 URL 变化,通过对象数组映射 URL 和组件关系,实现页面内容的动态切换。

3. history 模式

  • 特点: 去除 #,更简洁。
  • 解决问题: 使用 pushState 方法修改 URL 且不引起页面刷新,解决页面刷新问题;通过 popState 事件,和a标签的点击事件实现 URL 与组件的映射关系。
  • 实现原理: 利用 popstate 事件监听浏览器前进后退,通过 location.pathname 获取 URL 路径部分,映射到相应组件。