Роутер на JavaScript

15 сентября 2009 г.

Цель

Для чего понадобилось создавать этот роутер:

  1. понадобилось API для переходов между страницами JavaScript-приложения
  2. переходы должны сохраняться в истории браузера. Т.е. пользователь может использовать стандартные кнопки навигации

Страницы в JavaScript-приложении довольно условные, потому что URL не меняется, а изменяется только его хэш. Т.е. мы переходим со страницы http://localhost#product/list на страницу http://localhost#product/details/1234 при клике на продукт с Id равной 1234.

API для работы с роутером

Регистрация роутера

   1:  ApplicationRouter.initRouter();

Регистрация путей

   1:  router.registerRouter('product/list', function() {
   2:      // действия, которые можно сделать при переходе на список продуктов
   3:  }, 'Список продуктов');
   4:   
   5:  router.registerRouter('product/details/{id}', function(id) {
   6:      // id - выбирается из конкретного URL и передается в функцию
   7:  }, 'Детали продукта');

Пути можно регистрировать по конкретной строке и по маске с Id. Ограничением является присутствие в маске только {id}, причем в конце URL. В любом случае, этих двух сценариев использования нам полностью хватает для реализации проекта. При желании роутер можно расширить, чтобы он понимал любое количество заданных масок и передавал их в функцию, которая вызывается при переходе по заданному пути.

Редирект

   1:  Router.redirect('product/list');
   2:   
   3:  Router.redirect('product/details/1234');

Реализация

В основе реализации лежит компонент Ext.History. Мы наследуем наш роутер от него, чтобы использовать перехват события change. Живой пример обработки события компонентом Ext.History можно посмотреть на сайте ExtJS в примерах.

Сам код:

   1:  /**
   2:   * Handle URL navigation functionality
   3:   * 
   4:   * @class ApplicationRouter
   5:   * @extends Ext.History
   6:   * @singletone
   7:   */
   8:  ApplicationRouter = (function() {
   9:      var redirectedTo = '';
  10:      var routerList = new Array();
  11:   
  12:      var setLink = function(link, title) {
  13:          if (!Ext.isEmpty(title))
  14:          {
  15:              document.title = title.replace('→', '»');
  16:          }
  17:   
  18:          top.location.hash = "#" + link;
  19:      };
  20:   
  21:      var saveUrl = function() {
  22:          redirectedTo = document.location.hash.substring(1);
  23:      };
  24:   
  25:      /**
  26:       * @param {String}
  27:       *            path
  28:       */
  29:      var getPageLink = function(path) {
  30:          var result = "";
  31:          if (path.indexOf('?') != -1) {
  32:              result = path.substr(0, path.indexOf('?'));
  33:          } else {
  34:              result = path;
  35:          }
  36:          return result;
  37:      };
  38:   
  39:      /**
  40:       * @param {String}
  41:       *            route Путь, который содержит ID
  42:       * @param {String}
  43:       *            pathMask Маска, по которой выбирается ID
  44:       * @return {Int} Возвращает найденную ID или 0
  45:       */
  46:      var getIdFromRoute = function(route, pathMask) {
  47:          if (pathMask.indexOf('{id}') == -1 || route == null)
  48:              return 0;
  49:   
  50:          var pathRegex = pathMask.replace('{id}', '(\\d*)');
  51:          var match = route.match(pathRegex);
  52:   
  53:          if (match == null || match.length < 2 || match[1] == null)
  54:              return 0;
  55:   
  56:          return parseInt(match[1]);
  57:      };
  58:   
  59:      /**
  60:       * @param {String}
  61:       *            route Путь, который проверяем
  62:       * @param {String}
  63:       *            pathMask Маска, которая проверяет путь
  64:       * @return {Boolean} True, если путь подходит маске; иначе, false
  65:       */
  66:      var isRouteAcceptPath = function(url, pathMask) {
  67:          var page = getPageLink(url);
  68:          if (page == pathMask)
  69:              return true;
  70:          return getIdFromRoute(page, pathMask) > 0;
  71:      }
  72:   
  73:      return {
  74:          /**
  75:           * Call init before user router
  76:           * 
  77:           * @public
  78:           */
  79:          initRouter : function(onReady, scope) {
  80:              ApplicationRouter.init();
  81:   
  82:              var me = this;
  83:              this.on('change', function(token) {
  84:                          if (!Ext.isEmpty(token) && token != redirectedTo) {
  85:                              saveUrl();
  86:                              me.redirect(token);
  87:                          }
  88:                      });
  89:          },
  90:   
  91:          /**
  92:           * Register router for specifiered path mask.
  93:           * 
  94:           * 
  95:           * @public
  96:           * @param {String}
  97:           *            pathMask - Mask for redirec path
  98:           * @param {Function}
  99:           *            responseAction - If user redirect to page for pathMask, router should execute specifiered action
 100:           * @param {String}
 101:           *            pageTitle - Title for page
 102:           */
 103:          registerRouter : function(pathMask, responseAction, pageTitle) {
 104:              routerList.push({
 105:                          pathMask : pathMask.toLowerCase(),
 106:                          title : pageTitle,
 107:                          responseAction : responseAction
 108:                      });
 109:          },
 110:   
 111:          /**
 112:           * Redirect application to path
 113:           * 
 114:           * @public
 115:           * @param {String}
 116:           *            path - Path to redirect
 117:           *            @example
 118:           *            ApplicationRouter.redirect("my-url");
 119:           */
 120:          redirect : function(url) {
 121:              var router = null;
 122:              for (var i = 0; i < routerList.length; i++) {
 123:                  router = routerList[i];
 124:                  if (isRouteAcceptPath(url, router.pathMask)) {
 125:                      this.doRedirect(url, router);
 126:                      return;
 127:                  }
 128:              }
 129:              router = routerList[0];
 130:              this.doRedirect(router.pathMask, router);
 131:          },
 132:   
 133:          /**
 134:           * @private
 135:           */
 136:          doRedirect : function(path, router) {
 137:              redirectedTo = path;
 138:              setLink(path, router.title);
 139:   
 140:              var id = getIdFromRoute(path, router.pathMask);
 141:              router.responseAction(id > 0 ? id : null);
 142:   
 143:              this.fireEvent('redirect', path, router.title);
 144:          }
 145:      };
 146:  })();
 147:   
 148:  Ext.apply(ApplicationRouter, Ext.History);

9 комментариев:

  1. Заголовок неточный. Может сложиться впечатление что "роутер" - аппаратное сетевое средство.

    ОтветитьУдалить
  2. @Сергей Звездин

    Все, кто дочитали до этого комментария, знайте, что речь идет о роутере для URL в веб-страницах ;)

    ОтветитьУдалить
  3. Уф, а я тоже подумал, что какой-то гик сэмулировал Cisco на JavaScript-е :)))

    Кстати, а почему внутри класса везде var myVar вместо this.myVar ? По религиозным соображением, или просто чтоб помедленнее работало?

    ОтветитьУдалить
  4. "почему внутри класса везде var myVar вместо this.myVar"

    Даже не знаю как ответить =). Почитай про области видимости переменных в JavaScript и попробуй вместо var написать this. На самом деле вопрос из серии "я познаю мир".

    ОтветитьУдалить
  5. А я в свою очередь рекомендую тебе почитать о разнице в накладных расходах между созданием свойств и методов в прототипе и в конструкторе. А вопрос был из серии "надо ли писать на джаваскрипте как на шарпе".

    ОтветитьУдалить
  6. Ха-ха-ха
    Чувак, не позорься.

    obj = (function(){
    var internalVar;

    return {
    publicVar: 'i am public',
    publicFunc: function(){
    this.publicVar;
    internalVar;
    }
    }
    })();

    obj.publicFunc();
    obj.publicVar();

    Вот это стандартный прием в работе с JavaScript. Так что расслабься.

    ОтветитьУдалить
  7. Да, да, да... Александр, приношу свои извинения! Мне следовало внимательнее задачу читать и код смотреть. Посыпаю голову пеплом, что не увидел синглтон в постановке задачи и замыкание в коде. Действительно, опозорился.

    ОтветитьУдалить
  8. Ой, пристыдил тебя, мне аж неловко стало =)

    JavaScript вообще удивительный язык. Когда я начал им плотно пользоваться год назад, то плевался во все стороны. Как сказал его создатель: "JavaScript не имеет отношения к Java и скриптовым языком не является", так что с названием ему изначально не свезло =) А как лодку назовешь, так она и поплывет ;)

    Соглашусь, что хаки, типа приведенного синглтона распознаются на глаз уж очень не просто.

    ОтветитьУдалить
  9. Ай, да так мне и надо! Глянул код с середины по диагонали. Что имеется return внутренней функции - не заметил. Хоть бы увидел синглтон в шапке, он бы точно на мысль о замыкании навел - так нет же, полез холиварить, что свойства объекта экономнее юзать... Ээх!

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...