node 第二十天 手写SPA前端路由,vue-router实现原理

  1. 前言
    本章和node的关系不大,不过的确是服务端开发的必备前端知识。
    路由经历了不同的发展阶段
    后端路由又可称之为服务器端路由,因为对于服务器来说,当接收到客户端发来的HTTP请求,就会根据所请求的相应URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。对于最简单的静态资源服务器,可以认为,所有URL的映射函数就是一个文件读取操作。对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。这种方式在早期的前端开发中非常普遍,它的好处与坏处都很明显:

    • 好处:安全性好,SEO好。
    • 缺点:加大服务器的压力,不利于用户体验,代码冗合。

    也正是由于后端路由还存在着自己的不足,前端路由才有了属于自己的一片天地与发展的空间。对于前端路由来说,路由的映射函数通常是进行一些DOM的显示和隐藏操作。这样,当访问不同的路径的时候,会显示不同的页面组件。前端路由主要有以下两种实现方案:

    • hash
    • history API
    • hash 和 history具备不同的特点

    当然,前端路由也存在缺陷:使用浏览器的前进,后退键时会重新发送请求,来获取数据,没有合理地利用缓存。但总的来说,现在前端路由已经是实现路由的主要方式了,我们常用的诸如vue-router等前端框架的路由控制都是基于前端路由进行开发的(vue-router 4.x 又多了一种 Memory 模式),因此将前端路由进行一个了解还是很有必要的。

  2. 代码实现 重原理 真正的框架路由库肯定还有诸多细节

    DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>client routertitle>
      head>
      <body>
        <p>路由控制由后端移交给前端是前后端分离的重要标志p>
        <p>SPA能够模拟多页面应用的效果, 归功于其前端路由机制。p>
        <ul>
          <li style="color: red">hash routerli>
          <li><a href="#/index">indexa>li>
          <li><a href="#/about">abouta>li>
          <li><a href="#/me">mea>li>
        ul>
        <ul id="history">
          <li style="color: red">history routerli>
          <li><a href="/index">indexa>li>
          <li><a href="/about">abouta>li>
          <li><a href="/me">mea>li>
        ul>
        <div id="router-view">
          
        div>
      body>
      <script>
        class HashRouter {
          constructor(params) {
            this.routes = params.routes;
            this.routePath = '';
    
            // this 丢失,相当于把this.renderView的指向赋值给load事件的回调,导致renderView执行时this指向错误
            // window.addEventListener('load', this.renderView, false);
            // window.addEventListener('hashchange', this.renderView, false);
            window.addEventListener('load', () => this.renderView(), false);
            window.addEventListener('hashchange', () => this.renderView(), false);
          }
          renderView() {
            const curPath = location.hash.slice(1);
            const routes = this.routes.map(cur => cur.path);
    
            if (!routes.includes(curPath)) {
              this.routePath = '/index';
              // 触发hashchange
              location.hash = '#' + this.routePath;
              return;
            }
    
            this.routePath = curPath;
            document.querySelector('#router-view').innerHTML = this.routes.find(cur => cur.path === this.routePath).view;
          }
        }
    
        new HashRouter({
          routes: [
            {
              path: '/index',
              view: '主页'
            },
            {
              path: '/about',
              view: '关于'
            },
            {
              path: '/me',
              view: '我的'
            }
          ]
        });
    
        // HistoryRouter 需要服务器做配置才能实现真正的前端路由,比如点击刷新页面的行为
        // see https://router.vuejs.org/zh/guide/essentials/history-mode.html
        class HistoryRouter {
          constructor(params) {
            this.routes = params.routes;
            this.routePath = '';
    
            document.querySelector('#history').addEventListener('click', e => {
              e.preventDefault();
              try {
                let routePath = new URL(e.target.href).pathname;
                this.renderView(routePath);
                // 第三个参数代表新历史条目的 URL 如果该参数没有指定,则将其设置为当前文档的 URL。
                history.pushState({ path: routePath }, '', routePath);
              } catch (err) {
                throw new Error(err);
              }
            });
    
            window.addEventListener('load', () => this.renderView('/'), false);
            window.addEventListener('popstate', e => this.renderView(e), false);
          }
          renderView(routePath) {
            this.routePath = routePath;
            if (typeof routePath === 'object') {
              this.routePath = routePath.state?.path ?? '/';
            }
            const routes = this.routes.map(cur => cur.path);
            if (!routes.includes(this.routePath)) {
              this.routePath = '/index';
            }
            document.querySelector('#router-view').innerHTML = this.routes.find(cur => cur.path === this.routePath)?.view;
          }
        }
    
        new HistoryRouter({
          routes: [
            {
              path: '/index',
              view: '主页'
            },
            {
              path: '/about',
              view: '关于'
            },
            {
              path: '/me',
              view: '我的'
            }
          ]
        });
    
        // 从理论上来说hash和history路由并不矛盾
        // 你的SPA既可以使用hash也可以使用history 就像这个例子 同时new两个实例也能实现预期的导航 (only you hold ok ...)
      script>
    html>
    

你可能感兴趣的:(前端,vue.js,javascript,路由,vue-router)