Плагин для Матрешки: реактивный роутинг

Демо
Репозиторий
Плагин включает синхронизацию свойств объекта и куска урла.

this.initRouter('/a/b/c/');
this.a = 'foo';
this.b = 'bar';
this.c = 'baz'

// location.hash теперь #!/foo/bar/baz/


Для использования History API вместо location.hash, нужно передать строку "history" вторым аргументом.

this.initRouter('/a/b/c/', 'history');


Справедливо предположить, что то, что называется фронт-енд фреймворком должно включать в себя возможность роутинга. Вопрос «есть ли в Матрешке роутер» звучит достаточно часто, на что всегда был один ответ: есть много замечательных библиотек, которые имплементируют роутинг и они будут работать луше, чем непопулярная реализация «местячкового» роутера. Этот вопрос пришлось добавить в скромный FAQ со ссылкой на одну из самых популярных библиотек для роутинга Director. Но время пришло.

Как работает «традиционный» роутинг? Разработчик указывает правило (роут) и описывает то, как будет себя вести приложение при соответствии урла с указанным правилом.

route('books/:id', id => {
    // делать что-то
});


Matreshka Router работает совсем по-другому. Он следует традициям Матрешки, синхронизируя часть пути со свойством объекта.

Дисклеймер: такой способ роутинга совершенно нов и беспрецедентен. Он может не покрывать все те задачи, которые обычно ставят перед обычным роутером. Поэтому, воспользуйтесь альтернативным решением, если в этом есть необходимость.

Принцип работы плагина в следующем: вы указываете какая часть урла (поддерживается и hash, и HTML5 History) синхронизируется со свойством.

Скажем, вы хотите синхронизировать свойство "x" с первой частью location.hash, свойство "y" — со второй.

this.initRouter("/x/y/");


Теперь когда вы пишете…

this.x = 'foo';
this.y = 'bar';


… Хеш автоматически изменится на #!/foo/bar/

И наоборот, если вы вручную, программно или с помощью стрелок «вперед» и «назад» меняете адрес, свойства изменятся автоматически.

location.hash = '#!/baz/qux/';

console.log(this.x, this.y); // ‘bar’, ‘qux’


Как и всегда, свойства можно прослушивать с помощью метода on.

this.on('change:x', handler);


В метод initRouter можно передать строку со «звездочками», если нет необходимости в синхронизации какой-нибудь части урла.

this.initRouter('/x/*/y');


Если хеш выглядит, как #!/foo/bar/baz/, то this.x становится равным "foo", this.y — "baz".

Такая возможность полезна тогда, когда один класс контролирует одну часть адреса, другой — другую.

class1.js

this.initRouter('/x/*/');


class2.js

this.initRouter('/*/y/');


Важно помнить две вещи:

1. Если при инициализации роутера свойство имеет правдивое значение, адрес изменится сразу после вызова initRouter. Иначе — наоборот, свойство получит значение части пути.

this.x = 'foo';

this.initRouter('/x/y/');


2. Если свойство, указанное в роуте получит лживое значение, все последующие свойства, указанные в роуте, получат значение null.

this.initRouter('/x/y/z/u/');

this.y = null; // уствновит this.z и this.u тоже в null


Пускай вас не пугает такое решение. Идея в том, чтоб поддерживать состояния урла и свойств актуалными. Было бы странно иметь свойство "z" со значением "foo" при условии отсутствующей части адреса, связанной с этим свойством.

HTML5 History API


Кроме хеш роутинга, плагин поддерживает возможность работы с HTML5 History. Для инициализации нужно передать в метод initRoute дополнительный параметр type равный "history" (по умолчанию, type = "hash").

this.initRouter('/x/y/z/', 'history');


Поддержка произвольных объектов


Напомню, что почти все методы Матрешки (для связывания, реактивности, событий и пр.) имеют свой статичный аналог, в качестве примера приведу метод on.

// если this - инстанс Матрешки
this.on('something', handler);

// для любого другого объекта
var obj = {};
MK.on(obj, 'something', handler);


Подобные статичные методы скрыты под флажком «продвинутый режим» в документации.

Такое разделение не обошло стороной и этот плагин: кроме метода прототипа, в наличии есть и статичный аналог метода initRouter, который принимает произвольный объект в качестве первого аргумента и позволяет синхронизировать части адреса со свойствами обычного объекта.

var obj = {};
MK.initRouter(obj, '/a/b/c/');

obj.a = 'foo';
obj.b = 'bar';
obj.c = 'baz';


Дополнительная информация


Ядро роутера реализовано в виде класса Matreshka.Router. Его конструктор принимает один аргумент: тип роутера ("hash", "history" или произвольную строку).

При подключении модуля создаётся два экземпляра класса Matreshka.Router с типами hash и history, которые хранятся, соответственно, в Matreshka.Router.hash и Matreshka.Router.history (используется ленивая инициализация, поэтому подключение модуля без его использования ничего не делает). Для этих двух экземпляров реализуется паттерн синглтон, т. е, при создании роутера с типом hash возвращается Matreshka.Router.hash вместо создания нового экземпляра. Такая логика централизует обработку урла, положительно влияя на производительность и не вызывает коллизии. Объекты, в свою очередь, просто подписываются на изменения и не занимаются парсингом.

«Кастомные» экземпляры Matreshka.Router могут быть созданы вручную, на случай, если есть наобходимость работать вне браузерного окружения, или нужно сгенерировать ссылку для дальнейшего произвольного использования. В этом случае, изменение свойств не повлияет ни на hash и не вызовет pushState.

Экземпляры класса Router имеют три свойства.

path — свойство, которое содержит актуальный урл, например, /foo/bar/baz/.

hashPath — свойство, которое сожеджит path, с дописанным к ней hashbang, например #!/foo/bar/baz/

parts — свойство, представленное в виде массива, содержащее актуальные кусочки пути, например, [‘foo’, ‘bar’, ‘baz’].

Все три свойства объявлены с помощью метода linkProps, так что при изменении одного свойства, меняются и другие:

Matreshka.Router.hash.path = '/yo/man/';


… И два метода

subscribe(object, route) — подписывает объект на изменения в свойствах.

init() — используется для ленивой инициализации при вызове subscribe (вызывать вручную нет нужды).

var customRouter = new Matreshka.Router(),
    object = {
        a: 'foo',
        b: 'bar'
    };

customRouter.subscribe(object, '/a/b/');

console.log(customRouter.path); // /foo/bar/


Всем добра.

© Habrahabr.ru