ZHE LIN

一切伟大的思想和行动都有一个微不足道的开始

从零写一个路由插件

因为工作的需要,可能有的时候需要用的路由,虽然在Github上有许多大大小小已成熟的路由插件,但人有时候就是这样,宁愿自己写一个,也不愿多看几遍文档。这个是已经完成的路由插件地址:miniRouter,插件大小不足3kb,也没有任何依赖。虽然简单,我也觉得有必要整理成文,分享给大家。因为我使用的代码风格规范是JavaScript Standard Style所以代码我不会以分号结尾。

那么现在写一个插件,很重要的是要兼容AMD/CMD等标准,自适应不同的引用。那么这个“起手式”就很重要。我记得我出来工作那会写插件大多以这种形式:

(function(){
  var MiniRouter = function(ele, opt) {
    // ...

    this.init()
  }

  MiniRouter.prototype = {
    init: function() {
      // ...
    }
  }
})()

原型模式在插件开发中有举足轻重的地位,但这并不是我们上述的起手式,那么插件开发的起手式是什么呢?我也就不卖关子了,就是下面这样:

;(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(function () {
      return factory(root)
    })
  } else if (typeof exports === 'object') {
    module.exports = factory
  } else {
    root.PluginName = factory(root)
  }
})(this, function (root) {

  'use strict'

  // 这里写实现方法,并将插件对象return出去
  var PluginName = {}

  return PluginName
})

上面这种写法,兼顾了多种引用形式,同样的,以一个自执行函数将实参this和插件实现方法传给了内部函数 。在浏览器中这里的实参this指向了全局window对象。在这里的路由插件,我依然选择原型模式来实现。

首先开始,路由插件最重要的是监听路由的变化,这个很好办。我们只要监听window对象下的hashchange事件即可,事件对象有下面两个属性:

  • newURL:当前页面新的URL
  • oldURL:当前页面旧的URL

而针对某些不支持该事件的浏览器,我们使用轮询的URL变化监听即可。那么路由变化监听的代码就是这样的:

// 监听路由变化

if (('onhashchange' in window) && ((typeof document.documentMode === 'undefined') || document.documentMode === 8)) {
  window.onhashchange = function () {
    this.router(this.hashHandle())
  }.bind(this)
} else {
  setInterval(function () {
    var ischanged = this.isHashChanged()
    if (ischanged) {
      this.router(this.hashHandle())
    }
  }.bind(this), 150)
}

// 如果不支持onhashchange,isHashChanged方法的实现

var hash = this.hashHandle()
if (hash !== this.prevRouterHash) {
  this.prevRouterHash = hash
  return true
}
this.prevRouterHash = hash

在这段代码中,我们实现了上述的监听路由变化功能,在不支持onhashchange事件的情况下,我们调用了isHashChanged方法来判断路由的变化。

那么路由监听完了,还有一个问题是第一次进入页面,并不会触发onhashchange事件,那我们就需要在页面加载之后执行,页面加载完成之后就执行,使用onload事件?这明显是不行的,该事件会等到页面所有元素都加载完成才会执行。那么如果我们使用了jQuery十分好实现,只需要:

$(function(){
  // ...
})

或者这样:

$.ready(function(){
  // ...
})

但前面我也说过,这个插件并没有依赖任何库,所以上述的方法是不行的,我们得自己封装一个DOM加载完成后的方法。

如果这个插件你准备只兼容现代浏览器,那么太棒了,我们可以用很少的代码完成这件事。这样,我们只需要监听document.readyState或者是监听DOMContentLoaded即可。不过需要注意的是,浏览器对document.readyState的支持也有着一些差异。我们需要通过判断浏览器是否支持document.attachEvent来确定。那么我们就该这样实现这个DOM加载完成方法:

function ready(fn) {
  if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading"){
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}

如果你遗憾的需要支持到IE8,像我这样,那么上述的方法就不能使用了,我们需要多一点的判断,在这里,我么多监听了一个onreadystatechange

function ready(fn) {
  if (document.readyState != 'loading'){
    fn();
  } else if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', fn);
  } else {
    document.attachEvent('onreadystatechange', function() {
      if (document.readyState != 'loading')
        fn();
    });
  }
}

那么这样,我们就完成了基本的工作。如果有兴趣,可以点开第一段miniRouter的链接,查看完整的代码。