Yönlendirme
Gezinme ve Rota Durumu
navigate ve navigateByName, canlı rota durumu, gezinme yönü ve kayıt durumu, kaydırma hafızası.
Programatik gezinme
Koddan gezinmek için bileşen içinde this.context (çalışan Application), bileşen dışında uygulama örneği ya da useNavigation() kullanılır:
Koddan gezinmek için fonksiyon bileşeninde useNavigation(), bileşen dışında uygulama örneği kullanılır:
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
const nav = useNavigation();
const note = notes.create();
nav.navigate(`/notes/${note.id}`);
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
const nav = useNavigation();
const note = notes.create();
nav.navigate(`/notes/${note.id}`);
Uygulama örneğinden, gezinme seçenekleriyle:
await app.navigate("/notes/5");
await app.navigateByName("notes-note", { id: 42 });
const first = app.provider.get(NotesService).list("notes")[0];
await app.navigate(`/notes/${first.id}`, { replace: true });
await app.router.navigate("/archive", { replace: true });
await app.navigate("/notes/5");
await app.navigateByName("notes-note", { id: 42 });
const first = app.provider.get(NotesService).list("notes")[0];
await app.navigate(`/notes/${first.id}`, { replace: true });
await app.router.navigate("/archive", { replace: true });
app.navigate(uri, seçenekler?) ile app.router.navigate(uri, seçenekler?) aynı NavigationOptions tipini alır; app.navigate seçenekleri router'a iletir.
| Seçenek | Etki |
|---|---|
replace |
Yeni geçmiş kaydı eklemek yerine mevcut kaydı değiştirir. |
state |
Geçmiş kaydına yazılan değer (bkz. Gezinme yönü ve kayıt durumu). |
force |
Ekrandaki adrese yapılan gezinme normalde yok sayılır; force: true ile middleware, guard'lar ve onRouterChanged yine çalışır. Ekrandaki sayfa yeniden oluşturulmaz. |
scroll |
Gezinme sonunda kaydırma: 'top', 'smooth', 'instant' ya da { top, left, behavior }. Verildiğinde scrollMemory'yi ezer. |
app.navigate gezinmenin sonucunu döndürür: tamamlanan gezinmede gösterilen rotanın ResolveResult'ı (bulunamayan adreste ve hata sayfasında ok: false), yok sayılan gezinmede { ok: true, skipped: true, uri }, guard ya da onLeave iptalinde { ok: false, cancelled: true, reason } (reason: 'guard', 'onLeave' ya da onLeave'in verdiği metin), middleware'in durdurduğu gezinmede { ok: false, uri, cancelled: true, reason: 'middleware' }, gezinme bitmeden router restartRouter() ile yeniden başlatıldıysa ya da app.dispose() ile kapatıldıysa { ok: false, uri, cancelled: true, reason: 'restart' | 'dispose' }; gezinme beklenmedik bir hatayla düşerse MJX317 raporlanır ve sonuç { ok: false, uri, cancelled: true, reason: 'error', error } olur. app.router.navigate ve app.router.navigateByName aynı sonucu döndürür.
useNavigation() app.router ile aynı nesneyi döndürür:
import { useNavigation } from "@motifx/core";
const nav = useNavigation();
await nav.navigate("/archive");
await nav.navigateByName("notes-note", { id: 7 });
nav.params; // mevcut yol parametreleri: { id: '7' }
nav.route; // mevcut rota
nav.uri; // mevcut URI
nav.fullPath; // tam yol
nav.meta; // rota meta verisi: { folder: 'notes' }
nav.chain; // zincirdeki her rotanın kendi yolu, parametreler doldurulmuş: ['/', '/notes', '/7']
import { useNavigation } from "@motifx/core";
const nav = useNavigation();
await nav.navigate("/archive");
await nav.navigateByName("notes-note", { id: 7 });
nav.params; // mevcut yol parametreleri: { id: '7' }
nav.route; // mevcut rota
nav.uri; // mevcut URI
nav.fullPath; // tam yol
nav.meta; // rota meta verisi: { folder: 'notes' }
nav.chain; // zincirdeki her rotanın kendi yolu, parametreler doldurulmuş: ['/', '/notes', '/7']
Canlı rota durumu
app.router uygulama boyunca tek nesnedir. Bir alana, useApplication().router'dan ya da useNavigation()'dan alınan router her okumada güncel rotayı verir. Rota alanları (params, route, uri, ok, meta, extend, fullPath, aliasOf, chain, direction, state, stack) reaktiftir: bir fonksiyon içinde okunduklarında gezinmede yeniden çalışırlar. Gezinme boyunca yerinde kalan bir layout, rota alanlarını getter içinde okuyarak her gezinmede güncellenir; yeniden kurulmaz. Örnekte iki öznitelik rotanın meta ve params değerlerinden hesaplanır:
import { Component, RouterView, useNavigation } from "@motifx/core";
export default class MainLayout extends Component {
nav = useNavigation();
override view() {
return (
<div
class="app"
data-layout={() => (this.nav.meta.folder ? "notes" : "page")}
data-pane={() => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail")}
>
<RouterView />
</div>
);
}
}
import { RouterView, useNavigation } from "@motifx/core";
export default function MainLayout() {
const nav = useNavigation();
return (
<div
class="app"
data-layout={() => (nav.meta.folder ? "notes" : "page")}
data-pane={() => (nav.meta.folder && !nav.params.id ? "list" : "detail")}
>
<RouterView />
</div>
);
}
import { motifOptions, RouterView, useNavigation } from "@motifx/core";
const MainLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
onConfigured() {
this.class.add("app");
this.attr.add({
"data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
"data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
});
},
view() {
return <RouterView />;
},
});
export default MainLayout;
import { Component, ComponentBase, motifComponent, RouterView, useNavigation } from "@motifx/core";
export default class MainLayout extends Component {
nav = useNavigation();
override view() {
return motifComponent("div", (app: ComponentBase) => {
app.class.add("app");
app.attr.add({
"data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
"data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
});
app.controls.add(motifComponent(RouterView));
});
}
}
import { ComponentBase, motifComponent, RouterView, useNavigation } from "@motifx/core";
export default function MainLayout() {
const nav = useNavigation();
return motifComponent("div", (app: ComponentBase) => {
app.class.add("app");
app.attr.add({
"data-layout": () => (nav.meta.folder ? "notes" : "page"),
"data-pane": () => (nav.meta.folder && !nav.params.id ? "list" : "detail"),
});
app.controls.add(motifComponent(RouterView));
});
}
import { motifComponent, motifOptions, RouterView, useNavigation } from "@motifx/core";
const MainLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
onConfigured() {
this.class.add("app");
this.attr.add({
"data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
"data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
});
},
view() {
return motifComponent(RouterView);
},
});
export default MainLayout;
import { Component, RouterView, useNavigation } from "@motifx/core";
export default class MainLayout extends Component {
nav = useNavigation();
view() {
return (
<div
class="app"
data-layout={() => (this.nav.meta.folder ? "notes" : "page")}
data-pane={() => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail")}
>
<RouterView />
</div>
);
}
}
import { RouterView, useNavigation } from "@motifx/core";
export default function MainLayout() {
const nav = useNavigation();
return (
<div
class="app"
data-layout={() => (nav.meta.folder ? "notes" : "page")}
data-pane={() => (nav.meta.folder && !nav.params.id ? "list" : "detail")}
>
<RouterView />
</div>
);
}
import { motifOptions, RouterView, useNavigation } from "@motifx/core";
const MainLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
onConfigured() {
this.class.add("app");
this.attr.add({
"data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
"data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
});
},
view() {
return <RouterView />;
},
});
export default MainLayout;
import { Component, motifComponent, RouterView, useNavigation } from "@motifx/core";
export default class MainLayout extends Component {
nav = useNavigation();
view() {
return motifComponent("div", (app) => {
app.class.add("app");
app.attr.add({
"data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
"data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
});
app.controls.add(motifComponent(RouterView));
});
}
}
import { motifComponent, RouterView, useNavigation } from "@motifx/core";
export default function MainLayout() {
const nav = useNavigation();
return motifComponent("div", (app) => {
app.class.add("app");
app.attr.add({
"data-layout": () => (nav.meta.folder ? "notes" : "page"),
"data-pane": () => (nav.meta.folder && !nav.params.id ? "list" : "detail"),
});
app.controls.add(motifComponent(RouterView));
});
}
import { motifComponent, motifOptions, RouterView, useNavigation } from "@motifx/core";
const MainLayout = () => motifOptions({
el: "div",
nav: useNavigation(),
onConfigured() {
this.class.add("app");
this.attr.add({
"data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
"data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
});
},
view() {
return motifComponent(RouterView);
},
});
export default MainLayout;
Öznitelikler /notes'ta notes / list, /notes/3'te notes / detail, /settings'te page / detail olur; layout yeniden kurulmaz, yalnız öznitelikler güncellenir.
- Canlı okuma nesne üzerinden yapılır.
const { params } = useNavigation()ya daconst id = nav.params.ido anın değerini alır ve sonradan değişmez; güncel değer için her seferindenav.params.idokuyun. - Rota, ayrılan sayfa ekrandan kaldırıldıktan sonra ve yeni sayfa bağlanmadan önce değişir (yığın gezinmesinde geçiş animasyonundan önce);
scopedservisler de aynı anda yeni gezinmeye geçer.keepAliveönbelleğine alınan sayfaonDeactivatediçinde kendi rotasını görür. Serbest bırakılan sayfanınonDisposingveonDisposedkancaları yeni sayfa ekrana geldikten sonra çalışır ve gidilen rotayı görür (bkz. Ayrılan sayfa). Router'ın kurduğu yeni sayfa, yapıcısında ve alan başlangıç değerlerinde gidilen rotayı görür. - Guard ve rota kancaları (
useGuard,onEntering,onUpdate) rota değişmeden önce çalışır; gidilen rota bağlam nesnesindedir (to,params). İptal edilen gezinmede rota değişmez. - Önbellekteki
keepAlivesayfalar gizliyken de güncel rotayı okur: bağları başka bir rotaya geçildiğinde o rotanın değerleriyle yeniden çalışır. Rotaya bağlı işi (veri yükleme gibi)onActivatediçinde yapın. useRouter()çağrılmadan önce rota alanları boştur (params{},uri'',routenull,chain[]) venavigateMJX309fırlatır.
Rota değişimini dinlemek
Ekrana yansıyan değerler için reaktif alanları bir fonksiyon içinde okumak yeter; yukarıdaki MainLayout bunu yapar. Bir gezinmeden sonra yan bir iş yapılacaksa (sekme başlığı, odak, ölçüm) onRouterChanged dinlenir. app.onRouterChanged(fn) aboneliği kaldıran bir fonksiyon döndürür. Bileşen içinde context.onRouterChanged(fn) yazıldığında (sınıfta ve Options'ta this.context, fonksiyon bileşeninde kök bileşenin context'i) abonelik bileşenin ömrüne bağlanır: bileşen serbest bırakılınca kendiliğinden kalkar, ayrıca kaldırmak gerekmez. Yük { uri, params, meta, route, ok, initial, redirectedFrom?, direction?, state? } biçimindedir; açılış gezinmesinde initial true olur (bkz. Gezinme guard'ları).
Gezinme yönü ve kayıt durumu — direction, state
Her gezinmenin yönü direction alanında gelir: 'initial' (açılış), 'push' (yeni geçmiş kaydı; adres çubuğuna elle yazılan adres ya da düz bir çapa bağlantısının açtığı kayıt da buna dahil), 'replace', 'back', 'forward' ya da sırası bilinemeyen geçmiş hareketi için 'traverse'. Yön guard'lara (to.direction), onRouterChanged yüküne ve app.router.direction'a yansır. Geçişleri yöne göre seçmek (ileride sola, geride sağa kaydırma) için kullanın.
navigate(uri, { state }) ile verilen değer o geçmiş kaydına yazılır ve kayda geri/ileri ile dönüldüğünde de geri gelir; sayfa yenilense bile korunur. Adrese yazılmadan taşınması gereken veriler (listeden hangi filtreyle gelindi, açık sekme gibi) için uygundur:
await app.navigate("/notes/7", { state: { from: "archive", query: "toplantı" } });
app.router.state; // { from: 'archive', query: 'toplantı' } — geri/ileri ile dönüldüğünde de
app.useGuard(({ to }, next) => { console.log(to.direction, to.state); next(); });
app.onRouterChanged(({ direction, state }) => console.log(direction, state));
await app.navigate("/notes/7", { state: { from: "archive", query: "toplantı" } });
app.router.state; // { from: 'archive', query: 'toplantı' } — geri/ileri ile dönüldüğünde de
app.useGuard(({ to }, next) => { console.log(to.direction, to.state); next(); });
app.onRouterChanged(({ direction, state }) => console.log(direction, state));
Düz nesneler olduğu gibi saklanır (router yalnızca kendi damgasını ekler, app.router.state onu çıkarır); Date, Map gibi düz olmayan değerlere hiç dokunulmaz. state verilmemiş kayıtta app.router.state undefined'dır.
Kaydırma hafızası — scrollMemory
Tek sayfalık uygulamada gezinme belgeyi değiştirir ama kaydırma konumunu değiştirmez; tarayıcı da bunu SPA için kendiliğinden yönetmez. scrollMemory açıldığında router her yolun son kaydırma konumunu tutar ve o yola dönüldüğünde (geri/ileri, bağlantı, yenileme) konumu yerine koyar:
app.useRouter({ routes, mode: "history", scrollMemory: true });
app.useRouter({ routes, mode: "history", scrollMemory: true });
Kurallar:
- Hiç görülmemiş yol sayfa başından başlar (
top: falseile kapatılır). - Adreste çıpa varsa (
/notes/5#history) çıpa hafızayı ezer; ilgiliid'ye kaydırılır. Sabit bir üst şerit varsa CSS'tescroll-margin-topverin. - Kaydırma anında yapılır, CSS'te
scroll-behavior: smootholsa da: kayıtlı konuma dönüş, yeni sayfanın başı, başka sayfadan gelinen çıpa ve geri/ileri, tarayıcının sayfa geçişlerindeki gibi beklemeden yerine oturur. Aynı sayfa içindeki çıpa bağlantısı (/notes/5→/notes/5#history,historymodunda düz<a href="#history">dahil) CSS'tekiscroll-behaviorkuralına uyar;smoothverildiyse yumuşak kayar. - Gezinmeye özel
scrollseçeneği hafızayı ezer:app.router.navigate('/x', { scroll: 'top' })her zaman başa alır;containerayarlıysa kabı, değilse pencereyi kaydırır. Gidilen sayfadan ayrılırken konum yine o sayfanın adına kaydedilir. - Konumlar bellekte tutulur:
restartRouter()ile yeniden başlatmada korunur, sayfa yenilenince silinir. Yenilemeden sonra da hatırlanacaksa kaydı siz yaparsınız; nereye yazılacağı (tarayıcı deposu, sunucu) size kalmıştır.
Kayıt için app.router üç metot verir: saveScrollPositions(fn) kayıt gereken her anda (sayfadan ayrılırken, pagehide, sayfa gizlenince ve app.dispose()) geri çağrıyı güncel konumlarla çağırır ve aboneliği kaldıran bir fonksiyon döndürür; abonelik yeniden başlatmada da sürer. restoreScrollPositions(positions) verilen konumları hafızaya ekler (limit uygulanır); açılışta, run()'dan önce de çağrılabilir. scrollPositions() o anki konumları, ekrandaki sayfanınki dahil, { "/notes": 1200 } biçiminde döndürür. scrollMemory kapalıyken scrollPositions() boş nesne döndürür, restoreScrollPositions etkisizdir ve geri çağrı hiç çağrılmaz.
app.useRouter({ routes, mode: "history", scrollMemory: true });
app.router.restoreScrollPositions(JSON.parse(localStorage.getItem("scroll") ?? "{}"));
app.router.saveScrollPositions((positions) => localStorage.setItem("scroll", JSON.stringify(positions)));
app.useRouter({ routes, mode: "history", scrollMemory: true });
app.router.restoreScrollPositions(JSON.parse(localStorage.getItem("scroll") ?? "{}"));
app.router.saveScrollPositions((positions) => localStorage.setItem("scroll", JSON.stringify(positions)));
Ayar nesnesiyle ince ayar:
app.useRouter({
routes,
scrollMemory: {
top: true, // görülmemiş yolda başa dön (varsayılan)
anchor: true, // çıpa kayıtlı konumu ezsin (varsayılan)
settleMs: 1200, // içeriğin oturmasını bekleme üst sınırı
limit: 60, // kaç yol hatırlansın
// Aynı sayfaya iki adresten geliniyorsa tek anahtara indirin:
key: (path) => path.replace(/^\/notlar/, "/notes"),
// Pencere yerine bir kabın kaydırmasını hatırla (seçici, öğe ya da öğe döndüren fonksiyon):
container: "main.detail",
},
});
app.useRouter({
routes,
scrollMemory: {
top: true, // görülmemiş yolda başa dön (varsayılan)
anchor: true, // çıpa kayıtlı konumu ezsin (varsayılan)
settleMs: 1200, // içeriğin oturmasını bekleme üst sınırı
limit: 60, // kaç yol hatırlansın
// Aynı sayfaya iki adresten geliniyorsa tek anahtara indirin:
key: (path) => path.replace(/^\/notlar/, "/notes"),
// Pencere yerine bir kabın kaydırmasını hatırla (seçici, öğe ya da öğe döndüren fonksiyon):
container: "main.detail",
},
});
Kaydırma bir kabın içindeyse (sabit başlık ve sekme çubuğu olan mobil kabuklar gibi) container verin. Kap her gezinmede yeniden aranır, yani ilk açılışta henüz DOM'da olmaması sorun değildir; bulunamazsa pencere kaydırması kullanılır.
İçerik asenkron çiziliyorsa (uzak veri, markdown, sanal liste, x-chunk ile dilimlenen satırlar) endişelenmeyin: geri koyma tek seferlik değil, hedefe ulaşılana ya da settleMs dolana kadar her karede yeniden denenir. Kullanıcı bu sırada tekerlek/dokunuş/tuş/fare tıklamasıyla araya girerse deneme bırakılır.
Maliyeti: durağan hâlde sıfır. scroll olayı dinlenmez; konum her gezinmede bir kez okunur: guard'lar geçtikten sonra, adres uygulanmadan ve içerik değiştirilmeden hemen önce. O an ayrılan sayfa hâlâ ekrandadır ve konumu gerçektir; iptal edilen gezinmede konum okunmaz. Açıkken history.scrollRestoration 'manual' yapılır; pagehide / sayfa gizlenince de ekrandaki sayfanın konumu hafızaya alınır ve kayıt geri çağrısı çağrılır. Kapalıyken (varsayılan) hiçbir dinleyici bağlanmaz ve history.scrollRestorationa dokunulmaz.
Çıpa kaynağı moda göre değişir. Adres çubuğundaki #… yalnızca history modunda çıpa olarak okunur. hash / file (ve shell) modunda yalnızca rotanın kendi çıpası (/sayfa#bolum) kullanılır; adresin # kısmı rota olduğu için çıpa sayılmaz. Hafıza bu modlarda varsayılan anchor ayarıyla çalışır.
Sonraki adım
Gezinmeyi denetlemek ve durdurmak için Rota Kancaları ve Guard'lar bölümüne geçin.