路由源码简单实践

路由

hash

history

概念

mp.weixin.qq.com/s/2o7VvwyPI...

zhuanlan.zhihu.com/p/37730038

www.jianshu.com/p/b6684ef7c...

在 Web 开发过程中,经常遇到路由 的概念。那么到底什么是路由呢?简单来说,路由就是 URL 到函数的映射。

路由这个概念本来是由后端提出来的,在以前用模板引擎开发页面的时候,是使用路由返回不同的页面,大致流程是这样的:

  1. 浏览器发出请求;
  2. 服务器监听到 80 或者 443 端口有请求过来,并解析 UR L路径;
  3. 服务端根据路由设置,查询相应的资源,可能是 html 文件,也可能是图片资源......,然后将这些资源处理并返回给浏览器;
  4. 浏览器接收到数据,通过content-type决定如何解析数据

简单来说,路由就是用来跟后端服务器交互的一种方式,通过不同的路径来请求不同的资源,请求HTML页面只是路由的其中一项功能。

  • 服务端路由

    当服务端接收到客户端发来的 HTTP 请求时,会根据请求的 URL,找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。

    • 对于最简单的静态资源服务器,可以认为,所有 URL 的映射函数就是一个文件读取操作。
    • 对于动态资源,映射函数可能是一个数据库读取操作,也可能进行一些数据处理,
  • 客户端路由

    服务端路由会造成服务器压力比较大,而且用户访问速度也比较慢。在这种情况下,出现了单页应用

    单页应用,就是只有一个页面,用户访问网址,服务器返回的页面始终只有一个,不管用户改变了浏览器地址栏的内容或者在页面发生了跳转,服务器不会重新返回新的页面,而是通过相应的js操作来实现页面的更改。

前端路由的优点:

  • 前端路由可以让前端自己维护路由与页面展示的逻辑,每次页面改动不需要通知服务端。
  • 更好的交互体验:不用每次从服务端拉取资源。

区别

模式的区别

hash 模式来说, 它虽然看着是改变了 url ,但不会被包括在 http 请求中,所以这种模式不利于 SEO 优化。即所有页面的跳转都是在客户端进行操作。所以,它算是被用来指导浏览器的动作,并不影响服务器端。

使用 history 模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。

history 模式需要服务端的支持,服务端在接受到所有页面请求后,都指向index.html文件,或设置404页面为index.html。不然刷新时页面会出现404。

router 和 route 的区别

  • router 可以理解为一个容器,或者说一种机制,它管理了一组 route。
  • route 就是一条路由,它将一个 URL 路径和一个函数进行映射。

概括为:route 只是进行了 URL 和函数的映射,在当接收到一个 URL 后,需要去路由映射表中查找相应的函数(组件函数),这个过程是由 router 来处理的。

实现原理

实现前端路由,需要解决两个核心:

  • 如何改变 URL 却不引起页面刷新?
  • 如何检测 URL 变化了?

hash 实现

hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新

改变 URL 会触发 hashchange 事件:

  • 浏览器前进后退改变URL
  • 通过a标签锚点方式改变URL。
  • 通过window.location.hash改变URL
  • 调用history的back、go、forward方法

不能触发事件的方式

  • pushState和replaceState
javascript 复制代码
 window.addEventListener(
     'hashchange',
     function (event) {
         const oldURL = event.oldURL; // 上一个URL
         const newURL = event.newURL; // 当前的URL
         console.log(newURL, oldURL);
     },
     false
 );

简单

xml 复制代码
  <ul>
    <!-- 定义路由 -->
    <li><a href="#/home">home</a></li>
    <li><a href="#/about">about</a></li>
 ​
    <!-- 渲染路由对应的 UI -->
    <div id="routeView"></div>
  </ul>
 ​
 ​
 <script>
    // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
    window.addEventListener('DOMContentLoaded', onLoad)
    // 监听路由变化
    window.addEventListener('hashchange', onHashChange)
    
    // 路由视图
    var routerView = null
    
    function onLoad () {
      routerView = document.querySelector('#routeView')
      onHashChange()
    }
    
    // 路由变化时,根据路由渲染对应 UI
    function onHashChange () {
      switch (location.hash) {
        case '#/home':
          routerView.innerHTML = 'Home'
          return
        case '#/about':
          routerView.innerHTML = 'About'
          return
        default:
          return
      }
    }
 </script> 

详细

对 oldURL 和 newURL 进行拆分后,就能获取到更详细的 hash 值。我们这里从创建一个 HashRouter 的 class 开始一步步写起:

ini 复制代码
 class HashRouter {
     currentUrl = ''; // 当前的URL
     handlers = {};
 ​
     getHashPath(url) {
         const index = url.indexOf('#');
         if (index >= 0) {
             return url.slice(index + 1);
         }
         return '/';
     }
 }

事件hashchange只会在 hash 发生变化时才能触发,而第一次进入到页面时并不会触发这个事件 ,因此我们还需要监听load事件。这里要注意的是,两个事件的 event 是不一样的:hashchange 事件中的 event 对象有 oldURL 和 newURL 两个属性,但 load 事件中的 event 没有这两个属性,不过我们可以通过 location.hash 来获取到当前的 hash 路由

javascript 复制代码
 class HashRouter {
   currentUrl = ""; // 当前的URL
   handlers = {};
 ​
   constructor() {
     this.refresh = this.refresh.bind(this);
     window.addEventListener(
       "load",
       (curURL, oldURL) => {
         console.log("load");
         this.emit("change", curURL, oldURL);
       },
       false
     );
     window.addEventListener(
       "hashchange",
       (curURL, oldURL) => {
         console.log("hashchange");
         this.emit("change", curURL, oldURL);
       },
       false
     );
   }
 ​
   getHashPath(url) {
     const index = url.indexOf("#");
     if (index >= 0) {
       return url.slice(index + 1);
     }
     return "/";
   }
 ​
   on(evName, listener) {
     this.handlers[evName] = listener;
   }
 ​
   emit(evName, ...args) {
     const handler = this.handlers[evName];
     if (handler) {
       handler(...args);
     }
   }
 ​
   refresh(event) {
     let curURL = "",
       oldURL = null;
     if (event.newURL) {
       oldURL = this.getHashPath(event.oldURL || "");
       curURL = this.getHashPath(event.newURL || "");
     } else {
       curURL = this.getHashPath(window.location.hash);
     }
     this.currentUrl = curURL;
   }
 }
 ​

调用

ini 复制代码
 // 先定义几个路由
 const routes = [
   {
     path: "/",
     name: "home",
     component: "<h1>home</h1>",
     // component: <Home />
   },
   {
     path: "/about",
     name: "about",
     // component: <About />
     component: "<h1>1</h1>",
   },
   {
     path: "*",
     name: "404",
     component: "<h1>404</h1>",
     // component: <NotFound404 />
   },
 ];
 const router = new HashRouter();
 // 监听change事件
 router.on("change", (currentUrl, lastUrl) => {
   let route = null;
   // 匹配路由
   for (let i = 0, len = routes.length; i < len; i++) {
     const item = routes[i];
     if (router.getHashPath(currentUrl.target.location.hash) === item.path) {
       route = item;
       break;
     }
   }
   // 若没有匹配到,则使用最后一个路由
   if (!route) {
     route = routes[routes.length - 1];
   }
   // 渲染当前的组件
   document.getElementById("app").innerHTML = route.component;
 });
 ​

history实现

history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。

history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:a标签改变 URL 不会触发 popstate 事件

history提供了popstate监听事件,但是只有以下两种情况会触发该事件

  • 点击浏览器前进后退的按钮
  • 显示调用history的back、go、forward方法

不能触发事件的方式

  • pushState和replaceState (可以使用window.dispatchEvent重写事件,实现对popstate监听)

    ini 复制代码
     const listener = function (type) {
         var orig = history[type];
         return function () {
             var rv = orig.apply(this, arguments);
             var e = new Event(type);
             e.arguments = arguments;
             window.dispatchEvent(e);
             return rv;
         };
     };
     window.history.pushState = listener('pushState');
     window.history.replaceState = listener('replaceState');
  • a标签不能触发,因为非锚点模式直接跳转了页面。

详细

xml 复制代码
 <!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>
     <div id="app"></div>
     <a
       href="http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/"
       >home</a
     >
     <a
       href="http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/about"
       >about</a
     >
     <a
       href="http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/404"
       >404</a
     >
     <button>location</button>
     <button>pushState</button>
   </body>
   <script>
     document.getElementsByTagName("button")[0].addEventListener("click", () => {
       location.href =
         "http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/404";
     });
     document
       .getElementsByTagName("button")[1]
       .addEventListener("click", (e) => {
         window.history.pushState("a", "", "about");
       });
     // 先定义几个路由
     const routes = [
       {
         path: "/",
         name: "home",
         component: "<h1>home</h1>",
         // component: <Home />
       },
       {
         path: "/about",
         name: "about",
         // component: <About />
         component: "<h1>about</h1>",
       },
       {
         path: "*",
         name: "404",
         component: "<h1>404</h1>",
         // component: <NotFound404 />
       },
     ];
     class HistoryRouter {
       currentUrl = "";
       handlers = {};
 ​
       constructor() {
         this.refresh = this.refresh.bind(this);
         this.addStateListener();
         window.addEventListener("load", this.refresh, false);
         window.addEventListener("popstate", this.refresh, false);
         window.addEventListener("pushState", this.refresh, false);
         window.addEventListener("replaceState", this.refresh, false);
       }
       addStateListener() {
         const listener = function (type) {
           var orig = history[type];
           return function () {
             console.log(orig, type, arguments);
             var rv = orig.apply(this, arguments);
             var e = new Event(type);
             e.arguments = arguments;
             window.dispatchEvent(e);//使用window.dispatchEvent添加事件
             return rv;
           };
         };
         window.history.pushState = listener("pushState");
         window.history.replaceState = listener("replaceState");
       }
       refresh(event) {
         this.currentUrl = location.pathname;
         console.log(location);
         this.emit("change", location.pathname);
         // document.querySelector("#app span").innerHTML = location.pathname;
       }
       on(evName, listener) {
         this.handlers[evName] = listener;
       }
       emit(evName, ...args) {
         const handler = this.handlers[evName];
         if (handler) {
           handler(...args);
         }
       }
     }
 ​
     const router = new HistoryRouter();
     // 监听change事件
     router.on("change", (currentUrl) => {
       let route = null;
       // 匹配路由
       for (let i = 0, len = routes.length; i < len; i++) {
         const item = routes[i];
         console.log("/" + currentUrl.split("/").pop(), item.path);
         if ("/" + currentUrl.split("/").pop() === item.path) {
           route = item;
           break;
         }
       }
       if (currentUrl.split("/").pop().includes("html")) {
         route = routes[0];
       } else if (!route) {
         // 若没有匹配到,则使用最后一个路由
         route = routes[routes.length - 1];
       }
       // 渲染当前的组件
       document.getElementById("app").innerHTML = route.component;
     });
   </script>
 </html>
 ​
相关推荐
无我Code9 小时前
前端-2025年末个人总结
前端·年终总结
文刀竹肃9 小时前
DVWA -SQL Injection-通关教程-完结
前端·数据库·sql·安全·网络安全·oracle
LYFlied9 小时前
【每日算法】LeetCode 84. 柱状图中最大的矩形
前端·算法·leetcode·面试·职场和发展
Bigger9 小时前
Tauri(21)——窗口缩放后的”失焦惊魂”,游戏控制权丢失了
前端·macos·app
Bigger10 小时前
Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?
前端·macos·app
bug总结10 小时前
前端开发中为什么要使用 URL().origin 提取接口根地址
开发语言·前端·javascript·vue.js·html
一招定胜负11 小时前
网络爬虫(第三部)
前端·javascript·爬虫
Shaneyxs11 小时前
从 0 到 1 实现CloudBase云开发 + 低代码全栈开发活动管理小程序(13)
前端
半山烟雨半山青11 小时前
微信内容emoji表情包编辑器 + vue3 + ts + WrchatEmogi Editor
前端·javascript·vue.js
码途潇潇11 小时前
Vue 事件机制全面解析:原生事件、自定义事件与 DOM 冒泡完全讲透
前端·javascript