从头开始实现一个简单的单页面应用路由

272 阅读2分钟

当我们在一个传统的服务端渲染的 web 应用中点击一个链接时,浏览器会从服务端获得全新的 HTML,然后重新加载整个页面。然而,在单页面应用,我们点击一个导航会跳转到一个新的路由地址展示新的页面。那么今天我们就来简单的手动实现一个吧。

前置知识

1.History API

History API

history是什么

History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。下面是在浏览器控制台打印window.history后的内容

history History.png

api

1.window.history.back() 浏览器后退
2.window.history.forword() 浏览器前进
3.window.history.go()跳转到 history 中指定的一个点

你可以用 go() 方法载入到会话历史中的某一特定页面,通过与当前页面相对位置来标志 (当前页面的相对位置标志为 0).

向后移动一个页面 (等同于调用 back()):

window.history.go(-1);

向前移动一个页面,等同于调用了 forward():

window.history.go(1)

4.添加和修改历史记录中的条目,会改变history的length
pushState() 需要三个参数:一个状态对象state;一个标题 (目前被忽略),可以传一个空字符串和 (可选的) 一个 URL,注意:新 URL 必须与当前 URL 同源,否则 pushState() 会抛出一个异常,类似

Uncaught DOMException Failed to execute 'pushState' on 'History' A history state object with URL 'httplocalhost5174' canno.png

在某种意义上,调用 pushState() 与 设置 window.location = "#foo" 类似,二者都会在当前页面创建并激活新的历史记录。pushState() 绝对不会触发 hashchange 事件

history.replaceState()

它的使用与 history.pushState() 非常相似,区别在于 replaceState() 是修改了当前的历史记录项而不是新建一个。举个例子吧

<body>
    <a href="" target="_blank" id="linkA">/home</a>
    <a href="" target="_blank" id="linkB">/about</a>
    <a href="" target="_blank" id="linkC">/test</a>
    <script>

        const linkA = document.getElementById('linkA')
        const linkB = document.getElementById('linkB')
        const linkC = document.getElementById('linkC')
        linkA.addEventListener('click', function (e) {
            e.preventDefault();
            window.history.pushState(null, null, '/home')
        })
        linkB.addEventListener('click', function (e) {
            e.preventDefault();
            window.history.pushState(null, null, '/about')
        })
        linkC.addEventListener('click', function (e) {
            e.preventDefault();
            window.history.pushState(null, null, '/test')
        })
    </script>

</body>

上面代码中有3个链接,依次点击,会对应跳转/home,/about,/test,当到了/test后,点击浏览器的回退按钮,会依次跳转/about,/home。但是如果将linkC中的代码换成 window.history.replaceState(null, null, '/test'),按上面顺序执行后再点击浏览器回退按钮,则会跳过/about,直接到/home。相当于上一个记录直接被修改成了/test,再回退时直接跳转到了/home。

window.onpopstate

备注:  调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。

2.hashChange

当浏览器地址栏中的hash值改变时会触发该事件 hashchange 事件

用法


window.addEventListener('hashchange', function() {
  console.log('The hash has changed!')
}, false);

代码实现

方案一:使用hash + vue3 + 动态组件模拟单页面路由

    <script setup>
    import { ref, computed } from "vue";
    import Home from "@/views/HomeView.vue";
    import About from "@/views/AboutView.vue";
    import NotFound from "@/views/NotFound.vue";

    const routes = {
      "/": Home,
      "/about": About,
    };
    //默认展示home组件
    const currentPath = ref("/");
    window.addEventListener("hashchange", () => {
      console.log("hash改变啦", window.location.hash);
      const path = window.location.hash.slice(1);
      currentPath.value = path;
    });
    const currentView = computed(() => {
      return routes[currentPath.value] || NotFound;
    });
    </script>
    
    <template>
         <div class="wrapper">
            <a href="#/">home</a>
            <a href="#/about">about</a>
            <a href="#/not-found">not-found</a> 
            <component :is="currentView"></component>
        </div>
    </template>

方案二:使用pushState+vue3+动态组件

    <script setup>
    import { ref, computed } from "vue";
    import Home from "@/views/HomeView.vue";
    import About from "@/views/AboutView.vue";
    import NotFound from "@/views/NotFound.vue";

    const routes = {
      "/": Home,
      "/about": About,
    };
    //默认展示home组件
   const currentPath = ref("/");
   const handlClick = (e, path) => {
      e.preventDefault();
      console.log("handlClick--path", path);
      window.history.pushState(null, null, path);
      currentPath.value = path;
   };

    // 浏览器前进后退按钮会触发这个事件
    window.onpopstate = function (event) {
      // TODO:这里可能会存在state为null的情况,可以详细测试下
      currentPath.value = event.state?.current;
      console.log("popState 触发啦", event);
    };
    const currentView = computed(() => {
      return routes[currentPath.value] || NotFound;
    });
    </script>
    
    <template>
      <div class="wrapper">
        <a href="" id="homeLink" @click="handlClick($event'/')">home</a>
        <a href="" id="aboutLink" @click="handlClick($event, '/about')">about</a>
        <a href="" id="NotFoundLink" @click="handlClick($event, '/notFound')">not-found</a >
        <component :is="currentView"></component>
     </div>
   </template>