路由源码简单实践

路由

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>
 ​
相关推荐
前端小小王23 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发32 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6413 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js
dz88i84 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr4 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook