Цель
Для чего понадобилось создавать этот роутер:
- понадобилось API для переходов между страницами JavaScript-приложения
- переходы должны сохраняться в истории браузера. Т.е. пользователь может использовать стандартные кнопки навигации
Страницы в 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);
 
Заголовок неточный. Может сложиться впечатление что "роутер" - аппаратное сетевое средство.
ОтветитьУдалить@Сергей Звездин
ОтветитьУдалитьВсе, кто дочитали до этого комментария, знайте, что речь идет о роутере для URL в веб-страницах ;)
Уф, а я тоже подумал, что какой-то гик сэмулировал Cisco на JavaScript-е :)))
ОтветитьУдалитьКстати, а почему внутри класса везде var myVar вместо this.myVar ? По религиозным соображением, или просто чтоб помедленнее работало?
"почему внутри класса везде var myVar вместо this.myVar"
ОтветитьУдалитьДаже не знаю как ответить =). Почитай про области видимости переменных в JavaScript и попробуй вместо var написать this. На самом деле вопрос из серии "я познаю мир".
А я в свою очередь рекомендую тебе почитать о разнице в накладных расходах между созданием свойств и методов в прототипе и в конструкторе. А вопрос был из серии "надо ли писать на джаваскрипте как на шарпе".
ОтветитьУдалитьХа-ха-ха
ОтветитьУдалитьЧувак, не позорься.
obj = (function(){
var internalVar;
return {
publicVar: 'i am public',
publicFunc: function(){
this.publicVar;
internalVar;
}
}
})();
obj.publicFunc();
obj.publicVar();
Вот это стандартный прием в работе с JavaScript. Так что расслабься.
Да, да, да... Александр, приношу свои извинения! Мне следовало внимательнее задачу читать и код смотреть. Посыпаю голову пеплом, что не увидел синглтон в постановке задачи и замыкание в коде. Действительно, опозорился.
ОтветитьУдалитьОй, пристыдил тебя, мне аж неловко стало =)
ОтветитьУдалитьJavaScript вообще удивительный язык. Когда я начал им плотно пользоваться год назад, то плевался во все стороны. Как сказал его создатель: "JavaScript не имеет отношения к Java и скриптовым языком не является", так что с названием ему изначально не свезло =) А как лодку назовешь, так она и поплывет ;)
Соглашусь, что хаки, типа приведенного синглтона распознаются на глаз уж очень не просто.
Ай, да так мне и надо! Глянул код с середины по диагонали. Что имеется return внутренней функции - не заметил. Хоть бы увидел синглтон в шапке, он бы точно на мысль о замыкании навел - так нет же, полез холиварить, что свойства объекта экономнее юзать... Ээх!
ОтветитьУдалить