route(root, defaultRoute, routes)
Opis
Umożliwia nawigację między "stronami" w aplikacji.
var Home = {
view: function () {
return 'Welcome';
},
};
m.route(document.body, '/home', {
'/home': Home, // definiuje `https://localhost/#!/home`
});
W aplikacji może istnieć tylko jedno wywołanie m.route
.
Sygnatura
m.route(root, defaultRoute, routes)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
root | Element | Tak | Element DOM, który będzie elementem nadrzędnym dla renderowanego poddrzewa. |
defaultRoute | String | Tak | Trasa, do której nastąpi przekierowanie, jeśli bieżący adres URL nie pasuje do żadnej zdefiniowanej trasy. Uwaga: Nie jest to trasa startowa aplikacji. Jako trasa startowa zostanie użyty adres URL z paska adresu przeglądarki. |
routes | Object<String,Component|RouteResolver> | Tak | Obiekt, którego klucze są łańcuchami znaków reprezentującymi trasy, a wartościami są komponenty lub RouteResolver. |
zwraca | Brak zwracanej wartości. |
Statyczne elementy członkowskie
m.route.set
Przekierowuje do pasującej trasy lub do trasy domyślnej, jeśli nie można znaleźć pasującej trasy. Wyzwala asynchroniczne ponowne renderowanie wszystkich zamontowanych komponentów.
m.route.set(path, params, options)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
path | String | Tak | Nazwa ścieżki, do której ma nastąpić przekierowanie, bez prefiksu. Ścieżka może zawierać parametry, które zostaną interpolowane z wartościami z params . |
params | Object | Nie | Parametry routingu. Jeśli path zawiera symbole zastępcze dla parametrów routingu, właściwości tego obiektu zostaną użyte do interpolacji łańcucha ścieżki. |
options.replace | Boolean | Nie | Określa, czy utworzyć nowy wpis w historii przeglądarki, czy zastąpić bieżący. Domyślnie: false . |
options.state | Object | Nie | Obiekt state przekazywany do bazowej metody history.pushState / history.replaceState . Ten obiekt staje się dostępny poprzez właściwość history.state i jest scalany z parametrami routingu. Opcja ta działa tylko w przypadku korzystania z API pushState . Jest ignorowana, gdy router przechodzi w tryb hashchange (tzn. gdy API pushState jest niedostępne). |
options.title | String | Nie | Tekst, który zostanie przekazany jako tytuł do bazowej metody history.pushState / history.replaceState . |
zwraca | Zwraca undefined . |
Pamiętaj, że używając .set
z params
, musisz również zdefiniować odpowiednią trasę:
var Article = {
view: function (vnode) {
return 'This is article ' + vnode.attrs.articleid;
},
};
m.route(document.body, {
'/article/:articleid': Article,
});
m.route.set('/article/:articleid', { articleid: 1 });
m.route.get
Zwraca ostatnią w pełni rozwiązaną ścieżkę routingu, bez prefiksu. Może się różnić od ścieżki widocznej w pasku adresu przeglądarki, gdy asynchroniczne ładowanie trasy jest w toku (code-splitting).
path = m.route.get()
Argument | Typ | Wymagany | Opis |
---|---|---|---|
zwraca | String | Zwraca ostatnią w pełni rozwiązaną ścieżkę. |
m.route.prefix
Definiuje prefiks routera. Prefiks routera to fragment adresu URL, który określa bazową strategię używaną przez router.
m.route.prefix = prefix
Argument | Typ | Wymagany | Opis |
---|---|---|---|
prefix | String | Tak | Prefiks, który kontroluje bazową strategię routingu używaną przez Mithril. |
Jest to właściwość, którą można zarówno odczytywać, jak i zapisywać.
m.route.Link
Ten komponent tworzy dynamiczne linki nawigacyjne. Jego głównym zadaniem jest generowanie elementów <a>
, których atrybut href
jest modyfikowany w oparciu o prefiks trasy.
m(m.route.Link, { href: '/foo' }, 'foo');
// O ile m.route.prefix nie zmienił się od domyślnej strategii, renderuje do:
// <a href="#!/foo">foo</a>
Linki akceptują następujące specjalne atrybuty:
selector
to to, co zostałoby przekazane jako pierwszy argument dom
: dowolny selektor jest ważny, w tym elementy inne niża
.params
&options
to argumenty o tych samych nazwach, co zdefiniowane wm.route.set
.disabled
, jeśli true, wyłącza zachowanie routingu i dowolny powiązany handleronclick
oraz dołącza atrybutdata-disabled="true"
dla wskazówek dotyczących dostępności; jeśli element jesta
,href
jest usuwany.
Domyślne zachowanie routingu nie może zostać anulowane za pomocą standardowych metod obsługi zdarzeń. Zamiast tego należy użyć atrybutu disabled
.
m(
m.route.Link,
{
href: '/foo',
selector: 'button.large',
disabled: true,
params: { key: 'value' },
options: { replace: true },
},
'link name'
);
// Renderuje do:
// <button disabled aria-disabled="true" class="large">link name</button>
vnode = m(m.route.Link, attributes, children)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
attributes.href | Object | Tak | Docelowa trasa do nawigacji. |
attributes.disabled | Boolean | Nie | Wyłącza element w sposób dostępny. |
attributes.selector | String|Object|Function | Nie | Selektor dla m , domyślnie "a" . |
attributes.options | Object | Nie | Ustawia options przekazywane do m.route.set . |
attributes.params | Object | Nie | Ustawia params przekazywane do m.route.set . |
attributes | Object | Nie | Wszelkie inne atrybuty, które mają być przekazane do m . |
children | Array<Vnode>|String|Number|Boolean | Nie | Dziecięce vnode dla tego linku. |
zwraca | Vnode | Vnode. |
m.route.param
Pobiera parametr trasy z ostatniej w pełni rozwiązanej trasy. Parametry trasy to pary klucz-wartość. Parametry trasy mogą pochodzić z kilku różnych źródeł:
- Interpolacje tras (np. jeśli trasa to
/users/:id
, a rozwiązuje się do/users/1
, parametr trasy ma kluczid
i wartość"1"
). - Łańcuchy zapytań routera (np. jeśli ścieżka to
/users?page=1
, parametr trasy ma kluczpage
i wartość"1"
). history.state
(np. jeślihistory.state
to{foo: "bar"}
, parametr trasy ma kluczfoo
i wartość"bar"
).
value = m.route.param(key)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
key | String | Nie | Nazwa parametru trasy (np. id w trasie /users/:id lub page w ścieżce /users/1?page=3 lub klucz w history.state ). |
zwraca | String|Object | Zwraca wartość dla określonego klucza. Jeśli klucz nie jest określony, zwraca obiekt, który zawiera wszystkie klucze interpolacji. |
Zauważ, że w funkcji onmatch
RouteResolvera nowa trasa nie została jeszcze w pełni rozwiązana, a m.route.param()
zwróci parametry poprzedniej trasy, jeśli takie istnieją. onmatch
otrzymuje parametry nowej trasy jako argument.
m.route.SKIP
Specjalna wartość, która może być zwrócona z funkcji onmatch
resolvera trasy, aby przejść do następnej trasy.
RouteResolver
RouteResolver to obiekt, który nie jest komponentem i zawiera metodę onmatch
i/lub metodę render
. Obie metody są opcjonalne, ale przynajmniej jedna musi być obecna.
Jeśli dany obiekt posiada metodę view
lub jest funkcją/klasą (co wskazuje na to, że jest komponentem), zostanie potraktowany jako komponent, nawet jeśli zdefiniowano w nim metody onmatch
lub render
. RouteResolver
nie jest komponentem, dlatego nie posiada metod cyklu życia.
Z reguły RouteResolvers powinny znajdować się w tym samym pliku co wywołanie m.route
, podczas gdy definicje komponentów powinny znajdować się w ich własnych modułach.
routeResolver = {onmatch, render}
Można traktować komponenty jako uproszczoną wersję resolvera trasy. Przykładowo, jeśli komponent nazywa się Home
…
var routeResolver = {
onmatch: function () {
return Home;
},
render: function (vnode) {
return [vnode];
},
};
routeResolver.onmatch
Metoda onmatch
jest wywoływana przez router w celu znalezienia komponentu, który ma zostać wyrenderowany. Wywoływana jest raz przy każdej zmianie ścieżki, ale nie podczas kolejnych aktualizacji (ponownych renderowań) tej samej ścieżki. Może być używana do wykonania logiki przed zainicjowaniem komponentu (np. uwierzytelnianie, wstępne ładowanie danych, śledzenie analityczne przekierowań).
Ta metoda pozwala również asynchronicznie zdefiniować, który komponent zostanie wyrenderowany, dzięki czemu nadaje się do dzielenia kodu i asynchronicznego ładowania modułów. Aby wyrenderować komponent asynchronicznie, zwróć obietnicę, która rozwiązuje się do komponentu.
Więcej informacji na temat onmatch
można znaleźć w sekcji zaawansowane rozwiązywanie komponentów.
routeResolver.onmatch(args, requestedPath, route)
Argument | Typ | Opis |
---|---|---|
args | Object | Parametry routingu. |
requestedPath | String | Ścieżka routera żądana przez ostatnią akcję routingu, w tym interpolowane wartości parametrów routingu, ale bez prefiksu. Gdy onmatch jest wywoływany, rozwiązywanie dla tej ścieżki nie jest zakończone, a m.route.get() nadal zwraca poprzednią ścieżkę. |
route | String | Ścieżka routera żądana przez ostatnią akcję routingu, z wyłączeniem interpolowanych wartości parametrów routingu. |
zwraca | Component|\Promise<Component>|undefined | Zwraca komponent lub obietnicę, która rozwiązuje się do komponentu. |
Jeśli onmatch
zwraca komponent lub obietnicę, która rozwiązuje się do komponentu, ten komponent jest używany jako vnode.tag
dla pierwszego argumentu w metodzie render
RouteResolvera. W przeciwnym razie vnode.tag
jest ustawiony na "div"
. Podobnie, jeśli metoda onmatch
zostanie pominięta, vnode.tag
również wynosi "div"
.
Jeśli onmatch
zwraca obietnicę, która zostaje odrzucona, router przekierowuje z powrotem do defaultRoute
. Możesz zastąpić to zachowanie, wywołując .catch
na łańcuchu obietnic przed jego zwróceniem.
routeResolver.render
Metoda render
jest wywoływana przy każdym ponownym renderowaniu dla pasującej trasy. Jest podobna do metody view
w komponentach i istnieje, aby uprościć kompozycję komponentów. Pozwala to również na uniknięcie domyślnego zachowania Mithril.js, które polega na zastępowaniu całego poddrzewa DOM.
vnode = routeResolver.render(vnode)
Argument | Typ | Opis |
---|---|---|
vnode | Object | Vnode, którego obiekt atrybutów zawiera parametry routingu. Jeśli onmatch nie zwraca komponentu lub obietnicy, która rozwiązuje się do komponentu, pole tag vnode domyślnie przyjmuje wartość "div" . |
vnode.attrs | Object | Mapa wartości parametrów URL. |
zwraca | Array<Vnode>|Vnode | Vnode do wyrenderowania. |
Parametr vnode
to po prostu m(Component, m.route.param())
, gdzie Component
to rozwiązany komponent dla trasy (po routeResolver.onmatch
), a m.route.param()
jest udokumentowany tutaj. Jeśli pominiesz tę metodę, domyślna wartość zwracana to [vnode]
, opakowana we fragment, dzięki czemu możesz użyć parametrów klucza. W połączeniu z parametrem :key
staje się fragmentem z kluczem pojedynczego elementu, ponieważ kończy się renderowaniem do czegoś takiego jak [m(Component, {key: m.route.param("key"), ...})]
.
Jak to działa
Routing to system, który pozwala na tworzenie aplikacji jednostronicowych (SPA), tj. aplikacji, które mogą przechodzić z jednej "strony" do drugiej bez powodowania pełnego odświeżenia przeglądarki.
Umożliwia płynną nawigację, zachowując jednocześnie możliwość dodawania zakładek do każdej strony indywidualnie oraz możliwość nawigacji po aplikacji za pomocą mechanizmu historii przeglądarki.
Routing bez odświeżania strony jest częściowo możliwy dzięki API history.pushState
. Korzystając z tego API, można programowo zmienić adres URL wyświetlany przez przeglądarkę po załadowaniu strony, ale to na deweloperze aplikacji spoczywa odpowiedzialność za zapewnienie, że nawigacja do dowolnego adresu URL ze stanu zimnego (np. nowa karta) spowoduje wyrenderowanie odpowiedniego znacznika.
Strategie routingu
Strategia routingu określa, w jaki sposób biblioteka implementuje routing. Istnieją trzy ogólne strategie, które można wykorzystać do implementacji systemu routingu SPA, a każda z nich ma inne ograniczenia:
m.route.prefix = '#!'
(domyślna) – Używanie identyfikatora fragmentu (znanego również jako hash) części adresu URL. Adres URL korzystający z tej strategii zazwyczaj wygląda tak:https://localhost/#!/page1
.m.route.prefix = '?'
– Używanie łańcucha zapytania. Adres URL korzystający z tej strategii zazwyczaj wygląda tak:https://localhost/?/page1
.m.route.prefix = ''
– Używanie nazwy ścieżki. Adres URL korzystający z tej strategii zazwyczaj wygląda tak:https://localhost/page1
.
Użycie strategii hash jest gwarantowane w przeglądarkach, które nie obsługują history.pushState
, ponieważ może ona powrócić do używania onhashchange
. Użyj tej strategii, jeśli chcesz zachować hashe wyłącznie lokalnie.
Strategia łańcucha zapytania umożliwia wykrywanie po stronie serwera, ale nie pojawia się jako normalna ścieżka. Użyj tej strategii, jeśli chcesz obsługiwać i potencjalnie wykrywać zakotwiczone linki po stronie serwera i nie możesz wprowadzić zmian niezbędnych do obsługi strategii nazwy ścieżki (tak jakbyś używał Apache i nie możesz modyfikować swojego .htaccess).
Strategia nazwy ścieżki tworzy najczystsze adresy URL, ale wymaga skonfigurowania serwera do obsługi kodu aplikacji jednostronicowej z każdego adresu URL, do którego aplikacja może kierować. Użyj tej strategii, jeśli chcesz uzyskać czystsze adresy URL.
Aplikacje jednostronicowe, które używają strategii hash, często używają konwencji umieszczania wykrzyknika po hash, aby wskazać, że używają hash jako mechanizmu routingu, a nie do celów łączenia z kotwicami. Łańcuch #!
jest znany jako hashbang.
Domyślna strategia używa hashbang.
Typowe użycie
Zwykle musisz utworzyć kilka komponentów, aby mapować trasy do:
var Home = {
view: function () {
return [m(Menu), m('h1', 'Home')];
},
};
var Page1 = {
view: function () {
return [m(Menu), m('h1', 'Page 1')];
},
};
W powyższym przykładzie istnieją dwa komponenty: Home
i Page1
. Każdy zawiera menu i trochę tekstu. Menu samo w sobie jest definiowane jako komponent, aby uniknąć powtórzeń:
var Menu = {
view: function () {
return m('nav', [
m(m.route.Link, { href: '/' }, 'Home'),
m(m.route.Link, { href: '/page1' }, 'Page 1'),
]);
},
};
Teraz możemy zdefiniować trasy i mapować nasze komponenty do nich:
m.route(document.body, '/', {
'/': Home,
'/page1': Page1,
});
Tutaj określamy dwie trasy: /
i /page1
, które renderują swoje odpowiednie komponenty, gdy użytkownik przechodzi do każdego adresu URL.
Nawigacja do różnych tras
W powyższym przykładzie komponent Menu
zawiera dwa elementy m.route.Link
. Powoduje to utworzenie elementu (domyślnie <a>
), który po kliknięciu przez użytkownika, przenosi go do innej trasy w aplikacji. Nawigacja odbywa się lokalnie, bez odwoływania się do serwera.
Możesz również nawigować programowo, za pomocą m.route.set(route)
. Na przykład m.route.set("/page1")
.
Podczas nawigacji między trasami, prefiks routera jest obsługiwany automatycznie. Innymi słowy, pomiń hashbang #!
(lub cokolwiek ustawisz m.route.prefix
na) podczas łączenia tras Mithril.js, w tym zarówno w m.route.set
, jak i w m.route.Link
.
Zauważ, że podczas nawigacji między komponentami, całe poddrzewo jest zastępowane. Jeśli chcesz jedynie zaktualizować fragment drzewa DOM, użyj resolver trasy z zaimplementowaną metodą render
.
Parametry routingu
Czasami chcemy, aby zmienne identyfikatory lub podobne dane pojawiały się w trasie, ale nie chcemy jawnie określać oddzielnej trasy dla każdego możliwego identyfikatora. Aby to osiągnąć, Mithril.js obsługuje parametryzowane trasy:
var Edit = {
view: function (vnode) {
return [m(Menu), m('h1', 'Editing ' + vnode.attrs.id)];
},
};
m.route(document.body, '/edit/1', {
'/edit/:id': Edit,
});
W powyższym przykładzie zdefiniowaliśmy trasę /edit/:id
. To tworzy dynamiczną trasę, która pasuje do dowolnego adresu URL, który zaczyna się od /edit/
i jest kontynuowany przez jakieś dane (np. /edit/1
, edit/234
itp.). Wartość id
jest następnie mapowana jako atrybut vnode komponentu (vnode.attrs.id
).
Możliwe jest posiadanie wielu argumentów w trasie, na przykład /edit/:projectID/:userID
dałoby właściwości projectID
i userID
w obiekcie atrybutów vnode komponentu.
Parametr klucza
Gdy użytkownik przechodzi z trasy sparametryzowanej do tej samej trasy z innym parametrem (np. przechodząc z /page/1
do /page/2
przy danej trasie /page/:id
), komponent nie zostałby odtworzony od zera, ponieważ obie trasy rozwiązują się do tego samego komponentu, a tym samym powodują różnicę wirtualnego dom w miejscu. Skutkuje to wywołaniem metody onupdate
zamiast oninit
/ oncreate
.
Aby to osiągnąć, można połączyć parametryzację trasy z kluczami dla bardzo wygodnego wzorca:
m.route(document.body, '/edit/1', {
'/edit/:key': Edit,
});
Oznacza to, że vnode, który jest tworzony dla głównego komponentu trasy, ma obiekt parametru trasy key
. Parametry trasy stają się attrs
w vnode. Tak więc, przeskakując z jednej strony na drugą, key
zmienia się i powoduje odtworzenie komponentu od zera (ponieważ klucz mówi silnikowi wirtualnego dom, że stare i nowe komponenty są różnymi bytami).
Można to wykorzystać do tworzenia komponentów, które odświeżają się przy każdej zmianie trasy:
m.route.set(m.route.get(), {key: Date.now()})
Lub nawet użyć funkcji history state
, aby osiągnąć komponenty z możliwością ponownego załadowania bez zanieczyszczania adresu URL:
m.route.set(m.route.get(), null, {state: {key: Date.now()}})
Zauważ, że parametr klucza działa tylko dla tras komponentów. Jeśli używasz resolvera trasy, musisz użyć fragmentu z kluczem pojedynczego elementu, przekazując key: m.route.param("key")
, aby osiągnąć to samo.
Trasy wariadyczne
Możliwe jest również posiadanie tras wariadycznych, tj. trasy z argumentem, który zawiera nazwy ścieżek URL, które zawierają ukośniki:
m.route(document.body, '/edit/pictures/image.jpg', {
'/edit/:file...': Edit,
});
Obsługa błędów 404
W przypadku aplikacji izomorficznych (uniwersalnych) pisanych w JavaScript, przekazywanie parametru URL w połączeniu z trasą wariadyczną jest przydatne do wyświetlania niestandardowej strony błędu 404.
W przypadku błędu 404 Not Found, serwer odsyła niestandardową stronę do klienta. Gdy Mithril.js jest załadowany, przekieruje klienta do domyślnej trasy, ponieważ nie może wiedzieć, że trasa.
m.route(document.body, '/', {
'/': homeComponent,
// [...]
'/:404...': errorPageComponent,
});
Stan historii
Możliwe jest pełne wykorzystanie bazowego API history.pushState
, aby poprawić komfort nawigacji użytkownika. Na przykład, aplikacja mogłaby "zapamiętać" stan dużego formularza, gdy użytkownik opuszcza stronę, oddalając się, tak że jeśli użytkownik naciśnie przycisk wstecz w przeglądarce, miałby wypełniony formularz, a nie pusty formularz.
Na przykład, możesz utworzyć formularz w ten sposób:
var state = {
term: '',
search: function () {
m.route.set(m.route.get(), null, {
replace: true,
state: { term: state.term },
});
location.href = 'https://google.com/?q=' + state.term;
},
};
var Form = {
oninit: function (vnode) {
state.term = vnode.attrs.term || ''; // populated from the `history.state` property if the user presses the back button
},
view: function () {
return m('form', [
m("input[placeholder='Search']", {
oninput: function (e) {
state.term = e.target.value;
},
value: state.term,
}),
m('button', { onclick: state.search }, 'Search'),
]);
},
};
m.route(document.body, '/', {
'/': Form,
});
Dzięki temu, jeśli użytkownik wprowadzi dane w formularzu, a następnie cofnie się do niego za pomocą przycisku „Wstecz” w przeglądarce, pole wejściowe nadal będzie zawierało wprowadzone wcześniej dane. Technika ta poprawia komfort użytkowania, szczególnie w przypadku rozbudowanych formularzy, gdzie ponowne wprowadzanie danych byłoby uciążliwe.
Zmiana prefiksu routera
Prefiks routera to fragment adresu URL, który określa bazową strategię używaną przez router.
m.route.prefix = ''; // strategia nazwy ścieżki dla URL niebędącego głównym
m.route.prefix = '?';
m.route.prefix = '#';
// strategia nazwy ścieżki dla URL niebędącego głównym
m.route.prefix = '/my-app';
Zaawansowane zarządzanie komponentami
Zamiast przypisywać komponent bezpośrednio do trasy, możesz zdefiniować obiekt RouteResolver
. Obiekt RouteResolver
zawiera metodę onmatch()
i/lub render()
. Obie metody są opcjonalne, ale przynajmniej jedna z nich musi być zdefiniowana.
m.route(document.body, '/', {
'/': {
onmatch: function (args, requestedPath, route) {
return Home;
},
render: function (vnode) {
return vnode; // odpowiednik m(Home)
},
},
});
RouteResolvers
są przydatne do implementacji zaawansowanych scenariuszy routingu.
Opakowywanie komponentu układu
Często istnieje potrzeba opakowania wszystkich lub większości routowanych komponentów w kontener wielokrotnego użytku (często nazywany układem). Aby to osiągnąć, najpierw stwórz komponent, który zawiera wspólny znacznik, który będzie opakowywał inne komponenty:
var Layout = {
view: function (vnode) {
return m('.layout', vnode.children);
},
};
W powyższym przykładzie układ składa się jedynie z <div class="layout">
, który zawiera elementy potomne przekazane do komponentu, ale w rzeczywistym scenariuszu może być on bardziej złożony.
Jednym ze sposobów opakowania układu jest zdefiniowanie anonimowego komponentu w mapie tras:
// przykład 1
m.route(document.body, '/', {
'/': {
view: function () {
return m(Layout, m(Home));
},
},
'/form': {
view: function () {
return m(Layout, m(Form));
},
},
});
Pamiętaj jednak, że komponent główny jest komponentem anonimowym. Przejście z trasy /
do trasy /form
(lub odwrotnie) spowoduje usunięcie anonimowego komponentu i ponowne utworzenie DOM od zera. Jeśli komponent Layout
miał zdefiniowane metody cyklu życia, haki oninit
i oncreate
uruchamiałyby się przy każdej zmianie trasy. W zależności od aplikacji, może to być pożądane lub nie.
Jeśli wolisz, aby komponent Layout
był różnicowany i utrzymywany w stanie nienaruszonym, zamiast odtwarzania go od zera, powinieneś użyć RouteResolver
jako obiektu głównego:
// przykład 2
m.route(document.body, '/', {
'/': {
render: function () {
return m(Layout, m(Home));
},
},
'/form': {
render: function () {
return m(Layout, m(Form));
},
},
});
Zauważ, że w tym przypadku, jeśli komponent Layout
ma metody cyklu życia oninit
i oncreate
, zostaną one uruchomione tylko przy pierwszej zmianie trasy (zakładając, że wszystkie trasy używają tego samego układu).
Aby wyjaśnić różnicę między dwoma przykładami, przykład 1 jest równoważny z tym kodem:
// funkcjonalnie równoważne z przykładem 1
var Anon1 = {
view: function () {
return m(Layout, m(Home));
},
};
var Anon2 = {
view: function () {
return m(Layout, m(Form));
},
};
m.route(document.body, '/', {
'/': {
render: function () {
return m(Anon1);
},
},
'/form': {
render: function () {
return m(Anon2);
},
},
});
Ponieważ Anon1
i Anon2
są różnymi komponentami, ich poddrzewa (w tym Layout
) są odtwarzane od zera. Dzieje się tak również, gdy komponenty są używane bezpośrednio bez RouteResolver
.
W przykładzie 2, ponieważ Layout
jest komponentem najwyższego poziomu w obu trasach, DOM dla komponentu Layout
jest aktualizowany (tj. pozostawiany bez zmian, jeśli nie ma różnic), a jedynie zmiana z Home
na Form
powoduje odtworzenie tej sekcji DOM.
Przekierowanie
Funkcja onmatch
w RouteResolver
może być używana do uruchamiania logiki przed zainicjowaniem komponentu najwyższego poziomu dla danej trasy. Możesz użyć albo m.route.set()
Mithrila, albo natywnego API history
HTML. Podczas przekierowywania za pomocą API history
, hak onmatch
musi zwrócić obietnicę, która nigdy nie zostanie rozwiązana, aby zapobiec rozwiązaniu dopasowanej trasy. m.route.set()
anuluje rozwiązywanie dopasowanej trasy wewnętrznie, więc nie jest to konieczne w jego przypadku.
Przykład: uwierzytelnianie
Poniższy przykład ilustruje, jak zaimplementować ekran logowania, który uniemożliwia użytkownikom oglądanie strony /secret
, chyba że się zalogują.
var isLoggedIn = false;
var Login = {
view: function () {
return m('form', [
m(
'button[type=button]',
{
onclick: function () {
isLoggedIn = true;
m.route.set('/secret');
},
},
'Login'
),
]);
},
};
m.route(document.body, '/secret', {
'/secret': {
onmatch: function () {
if (!isLoggedIn) m.route.set('/login');
else return Home;
},
},
'/login': Login,
});
Po załadowaniu aplikacji wywoływana jest funkcja onmatch
i ponieważ isLoggedIn
ma wartość false
, aplikacja przekierowuje do /login
. Po naciśnięciu przycisku logowania, wartość isLoggedIn
zostanie ustawiona na true
, a aplikacja przekieruje do /secret
. Hak onmatch
zostanie uruchomiony ponownie i ponieważ isLoggedIn
ma tym razem wartość true
, aplikacja wyrenderuje komponent Home
.
Dla uproszczenia, w powyższym przykładzie, status zalogowania użytkownika przechowywany jest w zmiennej globalnej, a flaga ta jest jedynie przełączana, gdy użytkownik kliknie przycisk logowania. W rzeczywistej aplikacji użytkownik oczywiście musiałby podać odpowiednie dane logowania, a kliknięcie przycisku logowania spowodowałoby wysłanie żądania do serwera w celu uwierzytelnienia użytkownika:
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
login: function () {
m.request({
url: '/api/v1/auth',
params: { username: Auth.username, password: Auth.password },
}).then(function (data) {
localStorage.setItem('auth-token', data.token);
m.route.set('/secret');
});
},
};
var Login = {
view: function () {
return m('form', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m('button[type=button]', { onclick: Auth.login }, 'Login'),
]);
},
};
m.route(document.body, '/secret', {
'/secret': {
onmatch: function () {
if (!localStorage.getItem('auth-token')) m.route.set('/login');
else return Home;
},
},
'/login': Login,
});
Wstępne ładowanie danych
Zazwyczaj komponent może ładować dane po jego inicjalizacji. Ładowanie danych w ten sposób powoduje dwukrotne renderowanie komponentu. Pierwsze przejście renderowania następuje po routingu, a drugie po zakończeniu żądania. Należy pamiętać, że loadUsers()
zwraca obietnicę, ale każda obietnica zwrócona przez oninit
jest obecnie ignorowana. Drugie przejście renderowania pochodzi z opcji background
dla m.request
.
var state = {
users: [],
loadUsers: function () {
return m.request('/api/v1/users').then(function (users) {
state.users = users;
});
},
};
m.route(document.body, '/user/list', {
'/user/list': {
oninit: state.loadUsers,
view: function () {
return state.users.length > 0
? state.users.map(function (user) {
return m('div', user.id);
})
: 'loading';
},
},
});
W powyższym przykładzie, podczas pierwszego renderowania, interfejs użytkownika wyświetla "loading"
(ładowanie), ponieważ state.users
jest pustą tablicą przed zakończeniem żądania. Następnie, gdy dane są dostępne, interfejs użytkownika jest ponownie renderowany i wyświetlana jest lista identyfikatorów użytkowników.
RouteResolvers
mogą być używane jako mechanizm wstępnego ładowania danych przed renderowaniem komponentu, co pozwala uniknąć migotania interfejsu użytkownika i eliminuje potrzebę wyświetlania wskaźnika ładowania:
var state = {
users: [],
loadUsers: function () {
return m.request('/api/v1/users').then(function (users) {
state.users = users;
});
},
};
m.route(document.body, '/user/list', {
'/user/list': {
onmatch: state.loadUsers,
render: function () {
return state.users.map(function (user) {
return m('div', user.id);
});
},
},
});
Powyżej, render
uruchamia się dopiero po zakończeniu żądania, co czyni operator trójargumentowy zbędnym.
Dzielenie kodu
W dużej aplikacji może być pożądane, aby pobierać kod dla każdej trasy na żądanie, zamiast z góry. Dzielenie bazy kodu w ten sposób jest znane jako dzielenie kodu (code splitting) lub ładowanie opóźnione (lazy loading). W Mithril.js można to osiągnąć, zwracając obietnicę z haka onmatch
:
W najprostszej formie można to zrobić następująco:
// Home.js
module.export = {
view: function () {
return [m(Menu), m('h1', 'Home')];
},
};
// index.js
function load(file) {
return m.request({
method: 'GET',
url: file,
extract: function (xhr) {
return new Function(
'var module = {};' + xhr.responseText + ';return module.exports;'
);
},
});
}
m.route(document.body, '/', {
'/': {
onmatch: function () {
return load('Home.js');
},
},
});
Jednak w rzeczywistości, aby to działało na skalę produkcyjną, konieczne byłoby spakowanie wszystkich zależności dla modułu Home.js
do pliku, który jest ostatecznie obsługiwany przez serwer.
Na szczęście istnieje wiele narzędzi, które ułatwiają zadanie pakowania modułów do leniwego ładowania. Oto przykład użycia natywnego dynamicznego import(...)
, obsługiwanego przez wiele bundlerów:
m.route(document.body, '/', {
'/': {
onmatch: function () {
return import('./Home.js');
},
},
});
Typowane trasy
W niektórych zaawansowanych przypadkach routingu możesz chcieć bardziej ograniczyć wartość niż tylko samą ścieżkę, dopasowując tylko coś takiego jak numeryczny identyfikator. Możesz to zrobić dość łatwo, zwracając m.route.SKIP
z trasy.
m.route(document.body, '/', {
'/view/:id': {
onmatch: function (args) {
if (!/^\d+$/.test(args.id)) return m.route.SKIP;
return ItemView;
},
},
'/view/:name': UserView,
});
Ukryte trasy
W rzadkich przypadkach możesz chcieć ukryć niektóre trasy przed niektórymi użytkownikami, ale nie dla wszystkich. Na przykład, użytkownik może nie mieć uprawnień do wyświetlania profilu innego użytkownika i zamiast wyświetlać błąd uprawnień, chcemy ukryć fakt jego istnienia i przekierować do widoku 404. W takim przypadku możesz użyć m.route.SKIP
, aby po prostu udawać, że trasa nie istnieje.
m.route(document.body, '/', {
'/user/:id': {
onmatch: function (args) {
return Model.checkViewable(args.id).then(function (viewable) {
return viewable ? UserView : m.route.SKIP;
});
},
},
'/:404...': PageNotFound,
});
Anulowanie lub blokowanie trasy
RouteResolver
onmatch
może zapobiec rozwiązaniu trasy, zwracając obietnicę, która nigdy się nie rozwiązuje. Można to wykorzystać do wykrywania prób ponownego, niepotrzebnego rozwiązywania tej samej trasy i ich anulowania:
m.route(document.body, '/', {
'/': {
onmatch: function (args, requestedPath) {
if (m.route.get() === requestedPath) return new Promise(function () {});
},
},
});
Integracja z zewnętrznymi bibliotekami
W niektórych sytuacjach możesz potrzebować współpracy z innym frameworkiem, takim jak React. Oto jak to zrobić:
- Zdefiniuj wszystkie swoje trasy za pomocą
m.route
jak zwykle, ale upewnij się, że używasz go tylko raz. Wiele punktów routingu nie jest obsługiwanych. - Aby usunąć subskrypcje routingu, użyj
m.mount(root, null)
, podając ten sam elementroot
, który został użyty wm.route(root, ...)
.m.route
używa wewnętrzniem.mount
do podłączenia wszystkiego, więc to nie magia.
Oto przykład z React:
class Child extends React.Component {
constructor(props) {
super(props);
this.root = React.createRef();
}
componentDidMount() {
m.route(this.root, '/', {
// ...
});
}
componentDidUnmount() {
m.mount(this.root, null);
}
render() {
return <div ref={this.root} />;
}
}
A oto z grubsza odpowiednik z Vue:
<div ref="root"></div>
Vue.component('my-child', {
template: `<div ref="root"></div>`,
mounted: function () {
m.route(this.$refs.root, '/', {
// ...
});
},
destroyed: function () {
m.mount(this.$refs.root, null);
},
});