前端单页应用的路由系统 – 实现篇

上篇文章我介绍了单页应用路由系统的相关常识,文章请点击这里,这篇文章就来教大家实现一个简单的路由系统。篇幅中会有一些代码块,为避免布局错乱,请选择使用合适的设备浏览。

我们的需求

我们的需求很简单,配置一个路由,调用一个方法跳页即可。大致步骤就下面这两步,

// 1、配置路由
var router = new SpaRouter(config);

// 2、使用路由跳页,并自动渲染模板
router.to(path);

理清了需求,我们就来看看需要哪些数据结构。

数据结构分析

路由管理类

根据我们之前分析的简单需求,首先需要的类就是路由管理类。路由管理类是我们路由配置的入口,它的配置能直接影响我们的路由模式,路径以及渲染区等内容。所以它的属性至少应该包括路由模式、路由的根路径、路由项配置、渲染区域等。比如,

/**
 * 路由管理器
 * @param config
 * @constructor
 */
win.SpaRouter = function(config) {

   /**
    * 路由模式。包括hash、history
    * @type {*|number|string}
    */
   this.mode = config.mode||"hash";
   
   /**
    * 根路由的基础路由,会加在所有路径之前
    * @type {string}
    */
   this.base = config.base || "";
   
   /**
    * 路径配置项。每项都是SpaRoute对象
    * @type {*|Array}
    */
   this.routes = config.routes||[];
   
   /**
    * 渲染区域的根节点。默认为document.body
    * @type {HTMLElement}
    */
   this.root = config.root || document.body;
};

它的方法至少得包括一个跳转方法。比如,

/**
 * 路由跳转。
 * @param path {String}      需要跳转的绝对路径
 * @returns {null|SpaRoute}
 */
win.SpaRouter.prototype.to = function (path) {
    // TODO
};

路由类

路由类是路由的配置项,当某个路径匹配到相对应的路由实例时,会获取路由实例相对应的模板字符串,还会获取相对应的参数。所以它的属性至少需要包括路径、子路由、模板或者模板控制器、路由参数等。比如,

/**
 * 路由对象的数据结构
 * @param route
 * @constructor
 */
function SpaRoute(route) {
   var self = this;
   
   /**
    * 路由相对父级的路径
    * @type {string}
    */
   this.path = route.path;
   
   /**
    * 路由模板的控制器。返回路由的模板。
    * 控制器内部可以访问路由参数,可以在控制器内新增模板引擎系统来渲染页面。
    * @type {Function}
    * @return {template|String}
    */
   this.controller = route.controller || function(){
      return self.template;
   };
   
   /**
    * 路由的模板。路由控制器的默认返回。
    * @type {string}
    */
   this.template = route.template || "";
   
   /**
    * 当前路由的子路由
    * @type {SpaRoute[]|Array}
    */
   this.children = route.children;

   /**
    * 当前路由携带的参数。默认为空对象{}
    * @type {{}|*}
    */
   this.params = route.params || {};
}

有这两个类,我们的路由系统其实就已经够用了,下面就是对应的实现一些方法来处理路由的逻辑。

逻辑关键点实现

路由配置项的递归转换

配置路由的时候,我们可以无限级的配置多个子路由。为了配置方便,不可能每个子路由都要求配置绝对路径。所以,在配置的时候就有一个路由转换的过程。它的目的,是将所有的路由的相对路径转换为各个子路由的绝对路径,并将他们的层级关系剥离开,方便使用。

为了实现这个逻辑,我们在路由管理类SpaRouter中新增字段,用来存储解析后的路径对象:

/**
 * 存储解析routes之后的路径对象
 * @type {Array}
 * @private
 */
this._parsedRoutes = [];

另外还需要在路由类SpaRoute中新增字段,用来存在路由转换后的绝对路径:

this.absolutePath = "";

之后的话,就是一个递归函数来处理路径了。如下,

/**
 * 解析路由,计算它的绝对路径
 * @param routes   {Array}       SpaRoute对象数组
 * @param basePath {String}   基础路径
 * @returns {Array}             SpaRoute对象数组
 */
function parseRoutes(routes,basePath){
   var res = [];
   basePath = basePath==="/"?"":basePath;
   
   if(!routes || routes.length===0) return res;
   
   for(var i =0;i<routes.length;i++){
      var route = routes[i];
      route.absolutePath = basePath+"/"+(route.path==="/"?"":route.path);
      var children = route.children;
      res = res.concat(parseRoutes(children,route.absolutePath));
      route.absolutePath = route.absolutePath+(route.path==="/"?"":"/");
      res.push(new SpaRoute(route));
   }
   return res;
}

上面的转换函数还考虑到了路由的根路径,即转换后的绝对路径父级路径。由于所有的分隔符都是用左斜线`/`来表示,所以在实现的时候,还判断了左斜线是否重复。并且,在转换完成之后,为了方便拆分,还自动在绝对路径末尾加上了一个左斜线。

正则匹配路由

路由的匹配也不是很复杂,关键点是由于路由中可能含有冒号`:`开头的参数。所以,首先应该将参数先替换为任意字符匹配。匹配函数如下,

/**
 * 匹配路径。
 * @param pathPattern        路由模式路径
 * @param path             需要匹配的路径
 * @returns {boolean}        返回boolean
 */
function matchPath(pathPattern,path) {
   // 替换冒号开头的参数
   var regStr = pathPattern.replace(/:.+?\//g,"([^\/])+\/");
   // 结尾标识符
   regStr+="$";
   var reg = new RegExp(regStr);
   return reg.test(path);
}

路由跳转

为了简单,路由跳转只支持绝对路径跳转。比如,

router.to("/absolute/path/name/xiaohei");

所以,这个实现起来并不是很复杂。大致逻辑如下,

win.SpaRouter.prototype.to = function (path) {

   // 保存浏览器中的真实路径
   this.turePath = path;
   
   // 自动给路径末尾加上破折号
   path = path[path.length-1]==="/"?path:path+"/";
   
   // 获取当前路由
   var route = this.getRouteByPath(path);
   if(!route) return null;
   
   
   // 根据路由模式跳转页面
   if(this.mode==="hash")
      location.hash = path;
   else if(this.mode==="history"){
      if(!history.state || history.state.path!==path)
         history.pushState({path:path},null,path);
      
   }
   // 自动渲染root节点
   this.root.innerHTML = route.controller();
   return route;
};

步骤大概是,先获取路由,再根据路由模式修改URL,最后使用配置的模板渲染配置的根节点。这里只是多了一些路径处理和一些安全判断而已。

使用示例

只要实现了以上关键点,还有一些杂七杂八的细枝末节,整个路由系统也差不多完成了。下面是一个简单的使用示例:

<script src="../../src/SpaRouter.class.js"></script>
<script>
   var router = new SpaRouter({
      mode:"hash",
      base:"/",
      root:document.getElementById("app"),
      routes:[
         {
            path:"/",
            template:"首页模板"
         },
         {
            path:"base",
            template:"基本模板",
            children:[
               {
                  path:"bb",
                  template:"模板"
               }
            ]

         },
         {
            path:"params",
            template:"给路由传递参数的示例,可以从controller中获取参数,再填入至模板内",
            children:[
               {
                  path:":id/:name",
                  controller:function () {
                     return "带参数传递模板。从controller中获取参数,再填入至模板内。<br>参数为:"+JSON.stringify(this.params);
                  }
               }
            ]
         },
         {
            path:"*",
            template:"404模板"
         }

      ]
   });
</script>

你也可以访问我网站上的在线示例,http://lizhiqianduan.com/products/spa-router-hash-mode

不足之处

虽说这个路由系统可用,但是还有很多不足的地方,我这里将我认为不足的地方列出来,有兴趣的朋友可以根据源码进行相关的修改。

1、路由跳转方法router.to()不支持相对路径。需要进行一番计算。

2、router.to()不支持根据路由名称进行跳转。需要给Route类加上name字段进行匹配。

3、路由参数中不能包括特殊字符,比如`\`,`/`,`:`等。需要将参数编码。

4、模板控制器不支持双向绑定。需要配套一个mvvm模板引擎或者状态管理器。

5、没有路由的hook函数。需要新增逻辑。

完整的路由系统代码库请查看GitHub地址:https://github.com/lizhiqianduan/single-page-app-router

感兴趣的朋友可以将我之前的不足之处进行完善。

本文完



打赏作者

发表评论

电子邮件地址不会被公开。 必填项已用*标注