教你50行代码实现前端路由小轮子

在SPA应用这么流行的当下,我们看到每个MV**框架都会有自己的路由插件用于实现单页应用的路由设置与监控,并且提供了一系列的生命周期来方便用户,那么到底它们都是怎么做到的呢,今天我会放上自己写的一个小轮子的思路,因为是轮子所以大家就别吐槽这么多啦~更多的是记录这个思路

当我第一次自学vue并用于实际工作中时,还不知道element-ui、mint-ui等组件库,于是选择了直接用于移动端的weui作为组件库,当我看到微信提供的文档时,我发现他们的demo页面是单页的,这就激起了我的兴趣了,于是看了下他们的路由源码,是基于zepto封装的一个基于location.hash的一个前端路由监控,所以在这里我的轮子也是这个原理,废话不多说,回到主题,最近又去完整的看了下,并自己写了一个轮子,weui中它实现了基于hashchange事件的监控及完整的配置构造及改变,但是只做了一级路由,于是经过我的改造,我简单实现了:

  • 路由信息的灵活自主配置与入口的绑定
  • 支持嵌套路由
  • 支持组件化思想

    下面我们按照思路一步步给出代码:

1、首先,我们知道轮子的原理是基于 location对象 的 hashchange 事件来实现的,所以我们要围绕着它来写,其次我们需要一个路由对应一个组件来实现页面的重绘(没组件轮子就没意义啦对吧),所以我们先随意给一个路由配置并写出对应组件中的html。

// 路由配置及各组件片段
var routeConfig = [
        {
          url: '',
          templateId: 'home'
        },
        {
          url: 'home',
          templateId: 'home',
          children:[
            {
              url: 'page1',
              templateId: 'page4',
              children: [
                {
                  url: 'page2',
                  templateId: 'page2'
                }
              ]
            },
            {
              url: 'page2',
              templateId: 'page5'
            },
            {
              url: 'page3',
              templateId: 'page6'
            }
          ]
        },
        {
          url: 'page1',
          templateId: 'page1'
        },
        {
          url: 'page2',
          templateId: 'page2'
        },
        {
          url: 'page3',
          templateId: 'page3'
        }
      ];
// html 片段
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<style media="screen">
  .home a{
    text-decoration: none;
    color: blue;
  }
</style>
<body>
  <h1>H5前端路由hash变化demo</h1>
  <div class="container" id=container>

  </div>
  <script type="text/html" id="home">
    <div class="home">
      <a href="#/page1">go page1</a>
      <a href="#/page2">go page2</a>
      <a href="#/page3">go page3</a>
      <a href="#/home/page1">go child-page1</a>
      <a href="#/home/page2">go child-page2</a>
      <a href="#/home/page3">go child-page3</a>
      <a href="#/home/page1/page2">go child-page1-page2</a>
    </div>
  </script>

  <script type="text/html" id="page1">
    <div>
      <h2>页面1</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page2">
    <div>
      <h2>页面2</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page3">
    <div>
      <h2>页面3</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page4">
    <div>
      <h2>嵌套页面1</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page5">
    <div>
      <h2>嵌套页面2</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page6">
    <div>
      <h2>嵌套页面3</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>
</body>
</html>

由上述配置中可看出,我们这个demo是有二重与三重嵌套路由的,并且每个路由我们都给它一个指定组件去对应展示的html

2、配置信息大家看起来一定很熟悉,没错,就跟vue-route的配置一样简单明了,由此配置我们看出我们只要实现由对应 url 的 templateId 去匹配对应组件然后构造dom到页面上就可以完成了,所以下面我们都围绕这个来写,首先写轮子的构造函数

    // 构建路由函数
    function Route(selector, routeConfig) {
      // 过滤 location.hash
      String.prototype.filterUrl = function () {
        return this.replace(/#\//, '');
      }

      // 读取路由配置文件
      this.config = routeConfig;

      // 读取需要绑定的根选择器
      this.root = document.querySelector(selector);

      // 初始化最初的路由以及监控路由的 hash 值变化
      this.init = function () {
        var that = this;
        that._init()
        window.addEventListener('hashchange', function () {
          that._init()
        });
      }
    }

为了代码的清晰明了,我们将公用方法写到构造函数内,将实现过程需要的函数写入原型内,令其实现完整的面向对象及函数式编程的思路,我们定义2个参数用于配置,一是根选择器,二就是需要读取的路由配置,由于是利用 location.hash 来实现,所以我们还需在其中写一个过滤#的方法,再监控 hashchange 事件即可,构造函数中的是主函数的运行,具体实现过程我们将其放到了原型中,因为这些方法只是用于内部实现,所以我们将其与主函数分离。

3、具体实现过程及思路

Route.prototype = {
      // 内部的初始化,用于重构各个组件的dom结构,最终利用文档树绘制到绑定的选择器中
      _init: function () {
        var that = this,
            root = this.root,
            urlArr = location.hash.filterUrl().split('/'),  // 获取路由并抽象为数组,数组长度为后续节点树的总深度,对应下标为深度层数
            fragTree = document.createDocumentFragment();

        // 利用闭包保存每次遍历的深度为 1 的树,并每次遍历找到结果后,将下一次的深度为 1 的树保存
        function appendTreeNode (configArr, rootNode) {
          var tree = configArr;
          var currTreeNode = rootNode;

          return function (url) {
            for (var i = 0, len = tree.length; i < len; i++) {
              if (tree[i].url === url) {
                // 添加节点到文档碎片中
                var node = that._parseDom(tree[i].templateId)
                currTreeNode.appendChild(node);

                // 将下一次需要寻找的深度为1的树重新赋值
                tree = tree[i].children || null ;
                currTreeNode = node;
                break;
              }
            }
          }
        }

        // 初始化页面元素结构,再重新构建文档碎片
        root.innerHTML = '';
        var multiWayTree = appendTreeNode(that.config, fragTree);
        for (var i = 0, len = urlArr.length; i < len; i++) {
          multiWayTree(urlArr[i]);
        }
        root.appendChild(fragTree);

      },
      // 用于寻找对应组件模块
      _find: function (id) {
        return document.querySelector('script[type="text/html"][id=' + id + ']')
      },

      // 将模板的 innerHTML 抽象为 dom
      _parseDom: function (templateId) {
        var node = document.createElement('div');
        node.innerHTML = this._find(templateId).innerHTML;
        if (node.childElementCount !== 1) {
          throw new Error('template component must have a node and only one node as the root node.')
        }
        return node.firstElementChild;
      }
    }

虽然我已经注释的比较详细,但是有几点需要说明

  1. _init方法用于初始化页面需要重绘的组件,它会利用hashchange事件读取当前页面的url,并将其抽象为数组的结构,那么这个数组的长度即为路由树(或者说后续的DOM树)的深度,下标对应着相应的深度层数,eg: #/home/page1/page2 => ['home', 'page1', 'page2']home 即为根组件,page1为home的嵌套组件,page2为page1的嵌套组件;
  2. 这里由于是小轮子,所以我没做模块的缓存处理机制,只是简单的每次url的hash变化后将页面innerHTML置空及重绘,大家如有兴趣可以自己去做;
  3. 我们看到路由配置及dom树实际上为一个多叉树,那么难点显而易见,就是要将抽象的url深度数组精确遍历并找到对应的组件匹配,最后组成文档碎片丢到根选择器下,按照正常的遍历思路这里是行不通的,为什么呢?因为每次遍历的路由配置数组是不一样的,你很难确定这棵多叉树下的位移轨迹究竟是哪条,所以我这里通过闭包保存每次寻找到的树的轨迹,并保存下一次需要遍历的深度为1的多叉树,这样我就不用去考虑要多少次嵌套循环了,而是每次只用遍历深度为1的多叉树,后面我会放简图来解释;
  4. text/html 这种方式是拿不到其中html的伪标签的,如果大家需要拿到dom元素可以换成template标签实现,所以我们这里会创建一个div将其添加至其下再返回第一个子节点,这样我们就完整的保证每个组件绘制的纯净度,我们不会为你添加任何其他标签,每个组件需要一个根标签来包裹的原因也很简单,方便你自己控制,最重要的是为嵌套路由的便利铺路,否则如果你没有根标签,那么你在深度为3的dom树中,你这个组件的自我深度为第二层,那么第三层的组件我就没办法帮你嵌套了,因为我不知道嵌套到哪,这是最重要的原因,我想这也是为什么vue、react都会这么要求的原因。
  5. parseDom 这个简单方法实现组件的纯净度,要感谢 @liuqing_1 同学的想法,一开始我自己都陷进了自己的思路,还去用template标签拿到伪元素去appendChild了,后面换成了它这种更简单的思路。

下面放上多叉树解释图
这里写图片描述
上图红线为url为 home/page1/page2 的情况,由这个多叉树我们就可看出,将整个过程精确渲染需要至少经过3次遍历寻址,并要动态改变渲染的节点,由此可看出这里复杂点就是,寻址的次数与遍历次数有关,但是我们无法得知会有多复杂的路由,甚至5层嵌套都不是不可能?所以我们不能进行简单的嵌套循环与递归解决,简单来说就是原本每次深度像下一层,都要多一次嵌套循环,那么我们假设深度为 x,那么时间复杂度即为 O(n^x) ,但由于我这里引进了闭包来保存每层遍历的结果,所以就将算法的时间复杂度变为了 O(n*x),利用闭包的效率和理由在此就显而易见了,就是将算法的时间复杂度由 O(n^x) => O(n*x)

轮子非常简单,所以很多功能并没有去考虑,主要是提供一个思路给大家,其中写的不到位的地方,还请大家谅解,欢迎探讨其他前端路由的解决方案,最后放上完成轮子及demo实现↓

// 完整示例
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<style media="screen">
  .home a{
    text-decoration: none;
    color: blue;
  }
</style>
<body>
  <h1>H5前端路由hash变化demo</h1>
  <div class="container" id=container>

  </div>
  <script type="text/html" id="home">
    <div class="home">
      <a href="#/page1">go page1</a>
      <a href="#/page2">go page2</a>
      <a href="#/page3">go page3</a>
      <a href="#/home/page1">go child-page1</a>
      <a href="#/home/page2">go child-page2</a>
      <a href="#/home/page3">go child-page3</a>
      <a href="#/home/page1/page2">go child-page1-page2</a>
    </div>
  </script>

  <script type="text/html" id="page1">
    <div>
      <h2>页面1</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page2">
    <div>
      <h2>页面2</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page3">
    <div>
      <h2>页面3</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page4">
    <div>
      <h2>嵌套页面1</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page5">
    <div>
      <h2>嵌套页面2</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/html" id="page6">
    <div>
      <h2>嵌套页面3</h2>
      <a href="#/home">Go Home!</a>
    </div>
  </script>

  <script type="text/javascript">
    // 构建路由函数
    function Route(selector, routeConfig) {
      // 过滤 location.hash
      String.prototype.filterUrl = function () {
        return this.replace(/#\//, '');
      }

      // 读取路由配置文件
      this.config = routeConfig;

      // 读取需要绑定的根选择器
      this.root = document.querySelector(selector);

      // 初始化最初的路由以及监控路由的 hash 值变化
      this.init = function () {
        var that = this;
        that._init()
        window.addEventListener('hashchange', function () {
          that._init()
        });
      }
    }

    Route.prototype = {
      // 内部的初始化,用于重构各个组件的dom结构,最终利用文档树绘制到绑定的选择器中
      _init: function () {
        var that = this,
            root = this.root,
            urlArr = location.hash.filterUrl().split('/'),  // 获取路由并抽象为数组,数组长度为后续节点树的总深度,对应下标为深度层数
            fragTree = document.createDocumentFragment();

        // 利用闭包保存每次遍历的深度为 1 的树,并每次遍历找到结果后,将下一次的深度为 1 的树保存
        function appendTreeNode (configArr, rootNode) {
          var tree = configArr;
          var currTreeNode = rootNode;

          return function (url) {
            for (var i = 0, len = tree.length; i < len; i++) {
              if (tree[i].url === url) {
                // 添加节点到文档碎片中
                var node = that._parseDom(tree[i].templateId)
                currTreeNode.appendChild(node);

                // 将下一次需要寻找的深度为1的树重新赋值
                tree = tree[i].children || null ;
                currTreeNode = node;
                break;
              }
            }
          }
        }

        // 初始化页面元素结构,再重新构建文档碎片
        root.innerHTML = '';
        var multiWayTree = appendTreeNode(that.config, fragTree);
        for (var i = 0, len = urlArr.length; i < len; i++) {
          multiWayTree(urlArr[i]);
        }
        root.appendChild(fragTree);

      },
      // 用于寻找对应组件模块
      _find: function (id) {
        return document.querySelector('script[type="text/html"][id=' + id + ']')
      },

      // 将模板的 innerHTML 抽象为 dom
      _parseDom: function (templateId) {
        var node = document.createElement('div');
        node.innerHTML = this._find(templateId).innerHTML;
        if (node.childElementCount !== 1) {
          throw new Error('template component must have a node and only one node as the root node.')
        }
        return node.firstElementChild;
      }
    }

    // 程序主体
    window.onload = function () {
      var routeConfig = [
        {
          url: '',
          templateId: 'home'
        },
        {
          url: 'home',
          templateId: 'home',
          children:[
            {
              url: 'page1',
              templateId: 'page4',
              children: [
                {
                  url: 'page2',
                  templateId: 'page2'
                }
              ]
            },
            {
              url: 'page2',
              templateId: 'page5'
            },
            {
              url: 'page3',
              templateId: 'page6'
            }
          ]
        },
        {
          url: 'page1',
          templateId: 'page1'
        },
        {
          url: 'page2',
          templateId: 'page2'
        },
        {
          url: 'page3',
          templateId: 'page3'
        }
      ];
      var router = new Route('#container', routeConfig);  // arg1: 绑定的元素选择器, arg2: 路由配置
      router.init();
    }
  </script>
</body>
</html>

猜你喜欢

转载自blog.csdn.net/yolo0927/article/details/78076473
今日推荐