Router
مسیریابی سمت کلاینت برای برنامههای تکصفحهای. کامپوننتهای HTML اعلانی و یک API جاوااسکریپتی با نگهبانهای ناوبری، View Transitions و گذار میانسندی (MPA) در اختیار میگذارد.
کجا به کار میآید: وقتی به مسیریابی SPA سمت کلاینت با نگهبانهای ناوبری، View Transitions و گذار میانسندی (MPA) نیاز دارید.
createRouterبههمراه<r-router>/<r-route>/<r-link>ناوبری درونبرنامهای را سیمکشی میکند.
شروع سریع
یک برنامه کوچک اما کامل، با نگهبان احراز هویت و گذار SPA:
import { createRouter } from 'ranui';
// ۱. ساخت مسیریاب با مسیرهای محافظتشده و گذارهای SPA
const router = createRouter({
mode: 'history',
viewTransition: 'spa',
routes: [
{ path: '/', exact: true, meta: { title: 'Home' } },
{ path: '/about', meta: { title: 'About' } },
{ path: '/dashboard', meta: { title: 'Dashboard', requiresAuth: true } },
{ path: '/login', meta: { title: 'Login' } },
],
});
// ۲. نگهبان احراز هویت — کاربر واردنشده را هدایت کن
router.beforeEach((to, from, next) => {
if (to.meta?.requiresAuth && !sessionStorage.getItem('token')) {
next('/login');
} else {
next();
}
});
// ۳. پس از هر ناوبری عنوان صفحه را بهروز کن و آمار بفرست
router.afterEach((to) => {
document.title = to.meta?.title ?? 'App';
});
router.onRouteChange((to) => {
analytics.track(to.fullPath);
});<!-- مسیریاب را سوار کن، پیوندها را بگذار، مسیرها را اعلام کن -->
<r-router>
<nav>
<r-link href="/">خانه</r-link>
<r-link href="/about">درباره</r-link>
<r-link href="/dashboard">داشبورد</r-link>
</nav>
<r-route path="/" exact><h2>خانه</h2></r-route>
<r-route path="/about"><h2>درباره</h2></r-route>
<r-route path="/dashboard"><h2>داشبورد</h2></r-route>
<r-route path="/login"><h2>ورود</h2></r-route>
</r-router>/* گذار SPA — محو متقابل میان مسیرها */
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
::view-transition-old(root) {
animation: 200ms ease-out fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-in fade-in;
}کامپوننتها
r-router
کامپوننت ظرف. به popstate گوش میدهد و در هر ناوبری همه r-routeهای فرزند را هماهنگ میکند.
اتریبیوتها
| اتریبیوت | نوع | پیشفرض | توضیح |
|---|---|---|---|
mode | 'history' | 'hash' | 'history' | حالت History API |
base | string | '' | پیشوند نشانی پایه که از همه مسیرها برداشته میشود |
sheet | string | '' | CSSی که به Shadow DOM تزریق میشود |
رویدادها
| رویداد | detail | توضیح |
|---|---|---|
routechange | { path: string } | پس از هر بهروزرسانی مسیر فرستاده میشود |
r-route
اگر مسیر جاری با path بخواند، محتوای اسلاتش را نشان میدهد؛ وگرنه پنهانش میکند.
اتریبیوتها
| اتریبیوت | نوع | پیشفرض | توضیح |
|---|---|---|---|
path | string | '/' | الگوی تطبیق. از بخشهای :param و جانشین * پشتیبانی میکند |
exact | boolean | false | تطبیق دقیق را لازم میکند (بدون تطبیق پیشوندی) |
src | string | '' | شناسه ماژول برای سوار و پیاده کردن صفحه بهصورت تنبل و با جداسازی کد |
sheet | string | '' | CSSی که به Shadow DOM تزریق میشود |
رویدادها
| رویداد | detail | توضیح |
|---|---|---|
routematch | { path, params } | وقتی این مسیر فعال میشود فرستاده میشود |
نمونههای الگوی مسیر
/users با /users، /users/42، /users/42/profile میخواند
/users (exact) فقط با /users میخواند
/users/:id مقدار :id را میگیرد → params.id
/* با همهچیز میخواندسوار و پیاده کردن تنبل src
در برنامهای بزرگتر و چندصفحهای، r-route میتواند بهجای اینکه محتوای اسلاتش را همیشه از پیش بفرستد، کد هر صفحه را جدا کند. src را روی یک شناسه ماژول بگذارید؛ هنگام تطبیق، r-route آن را پویا import() میکند و خروجی پیشفرضش را — تابعی از نوع (host: HTMLElement) => void | (() => void) — درون یک دامنه واکنشی صدا میزند و عنصر میزبانی برای رسم به آن میدهد. ترک مسیر، آن دامنه را یکجا دور میریزد (هر اثر، هر پیوند و هر onCleanupی که صفحه ثبت کرده) و سپس محتوای رسمشده را برمیدارد؛ بازگشت به همان مسیر، از ماژول کششده دوباره سوار میشود بدون آنکه دوباره واکشی شود.
<r-route path="/settings" src="/pages/settings.js"></r-route>// pages/settings.js
export default function renderSettings(host) {
host.textContent = 'Settings page';
return () => {
/* پاکسازی اختیاری، هنگام ترک مسیر اجرا میشود */
};
}این حالت فقط سمت کلاینت است: در SSR/SSG، یک مسیر تنبل تنها وضعیت نمایش/پنهانش را حل میکند، نه خودِ ماژول صفحه را.
r-link
یک پیوند ناوبری. برای مسیرهای هممبدأ از بارگذاری دوباره کل صفحه جلوگیری میکند، اگر مسیریابی فعال باشد RouterCore.push/replace را صدا میزند، و در غیر این صورت رویداد ran-navigate را در درخت DOM به بالا میفرستد.
نشانیهای بیرونی (http://، //، mailto:، tel:) مثل پیوند <a> معمولی رد میشوند.
اتریبیوتها
| اتریبیوت | نوع | پیشفرض | توضیح |
|---|---|---|---|
href | string | '' | مسیر مقصد |
replace | boolean | false | بهجای افزودن، ورودی جاری تاریخچه را جایگزین میکند |
sheet | string | '' | CSSی که به Shadow DOM تزریق میشود |
<r-link href="/about">درباره</r-link>
<r-link href="/settings" replace>تنظیمات</r-link>
<r-link href="https://github.com">GitHub ↗</r-link>اسلاتها
هیچیک از r-router، r-route و r-link اسلات نامدار ندارند. هرکدام تنها <slot> پیشفرض (بینام) را رسم میکنند: r-router و r-route مسیرهای فرزند یا محتوای مسیر را همانگونه نمایش میدهند و r-link هرچه درونش بگذارید را بهعنوان محتوای دیدنیِ پیوند نشان میدهد. هیچکدام ::part() هم تعریف نمیکنند، پس این گروه کامپوننت بخش Partهای CSS ندارد.
API جاوااسکریپت
createRouter(config?)
یک نمونه سراسری RouterCore میسازد و ثبت میکند. در آغاز برنامه یک بار و پیش از سوار کردن هر عنصر r-router صدایش بزنید.
import { createRouter } from 'ranui';
const router = createRouter({
mode: 'history', // 'history' (پیشفرض) | 'hash'
base: '/app', // پیشوند '/app' را از همه مسیرهای درونی بردار
routes: [
{ path: '/', exact: true, meta: { title: 'Home' } },
{ path: '/users/:id', meta: { requiresAuth: true } },
],
viewTransition: 'spa', // 'spa' | 'mpa' | 'both' | false
});گزینهها
| گزینه | نوع | پیشفرض | توضیح |
|---|---|---|---|
mode | 'history' | 'hash' | 'history' | راهبرد نشانی |
base | string | '' | پیشوند مسیر پایه |
routes | RouteConfig[] | [] | تعریف مسیرها با path و exact و meta |
viewTransition | boolean | ViewTransitionMode | false | View Transitions را روشن میکند (true برابر 'spa') |
RouterCore
همه متدهای قلاب یک تابع لغو اشتراک برمیگردانند.
| نام | امضا / نوع | توضیح |
|---|---|---|
push(path) | (path: string) => Promise<void> | ناوبری میکند و ورودی تازهای به تاریخچه میافزاید |
replace(path) | (path: string) => Promise<void> | ناوبری میکند و ورودی جاری را جایگزین میکند |
back() | () => void | history.back() |
forward() | () => void | history.forward() |
go(delta) | (delta: number) => void | history.go(delta) |
beforeEach(guard) | (guard: NavigationGuard) => () => void | نگهبان ناوبری را ثبت میکند؛ پیش از قطعیشدن ناوبری اجرا میشود |
afterEach(handler) | (handler: RouteChangeHandler) => () => void | قلاب پس از ناوبری؛ پس از بهروزشدن DOM اجرا میشود |
onRouteChange(handler) | (handler: RouteChangeHandler) => () => void | هر تغییر مسیر را مشترک میشود |
onPageSwap(handler) | (handler: (e: PageSwapEvent) => void) => () => void | رویداد میانسندی pageswap (فقط در حالت MPA) |
onPageReveal(handler) | (handler: (e: PageRevealEvent) => void) => () => void | رویداد میانسندی pagereveal (فقط در حالت MPA) |
destroy() | () => void | همه شنوندهها و CSS تزریقشده را برمیدارد |
currentRoute | RouteLocation | null | شیء موقعیت مسیر جاری |
mode | 'history' | 'hash' | حالت تاریخچه |
base | string | پیشوند نشانی پایه |
routes | RouteConfig[] | پیکربندیهای مسیرِ ثبتشده |
router.push('/users/42');
router.replace('/login');
router.back();
router.go(-2);useRouter()
نمونه فعال RouterCore را برمیگرداند، یا null اگر createRouter هنوز صدا زده نشده باشد.
import { useRouter } from 'ranui';
const router = useRouter();
router?.push('/about');نگهبانهای ناوبری
نگهبانها به ترتیب ثبت و پیش از قطعیشدن ناوبری اجرا میشوند. برای اجازه next()، برای لغو next(false) و برای هدایت next('/path') را صدا بزنید.
const unsubscribe = router.beforeEach((to, from, next) => {
if (to.meta?.requiresAuth && !isLoggedIn()) {
next('/login');
} else {
next();
}
});
// برداشتن نگهبان در آینده:
unsubscribe();قلابهای پس از ناوبری
afterEach و onRouteChange هر دو پس از بهروزشدن DOM فرستاده میشوند. برای اثرهای جانبیای که به ناوبریِ کاملشده وابستهاند از afterEach و برای اشتراکهای سبک از onRouteChange استفاده کنید.
router.afterEach((to, from) => {
document.title = to.meta?.title ?? 'App';
});
router.onRouteChange((to, from) => {
analytics.track(to.fullPath);
});View Transitions
با View Transitions API مرورگر، گذار میان مسیرها را متحرک کنید.
مقایسه
پیش از نوشتن هر CSSی، حالت را انتخاب کنید:
| حالت | Chrome | چه چیزی آن را راه میاندازد | JS لازم است |
|---|---|---|---|
'spa' | 111+ | router.push() یا کلیک روی r-link | بله |
'mpa' | 126+ | هر پیوند <a>، ارسال فرم، location.href | خیر |
'both' | 111+ / 126+ | همه موارد بالا | اختیاری |
SPA — گذار درون یک سند
const router = createRouter({ viewTransition: 'spa' }); // یا trueهر فراخوانی router.push() / router.replace() بهروزرسانی DOM را در document.startViewTransition() میپیچد. جایی که این API نباشد، با متانت به یک بهروزرسانی همگام افت میکند (Chrome 111 به بالا).
CSSی برای تعریف انیمیشن بیفزایید:
/* محو متقابل پیشفرض */
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
::view-transition-old(root) {
animation: 200ms ease-out fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-in fade-in;
}MPA — گذار میان سندها
const router = createRouter({ viewTransition: 'mpa' });قاعده @view-transition { navigation: auto } را در <head> تزریق میکند و گذار خودکار را در هر ناوبری تمامصفحه هممبدأ روشن میکند (Chrome 126 به بالا). در هر صفحه به جاوااسکریپت نیازی نیست.
برای برنامههایی که اصلاً از مسیریاب استفاده نمیکنند:
import { enableMpaViewTransitions } from 'ranui';
const cleanup = enableMpaViewTransitions();
// در صورت نیاز cleanup() تگ <style> تزریقشده را برمیداردرویدادهای چرخه عمر MPA:
// pageswap روی سندی که دارد میرود، پیش از unload فرستاده میشود
router.onPageSwap((e) => {
const type = e.activation?.navigationType; // 'push' | 'replace' | 'traverse'
if (type === 'traverse') e.viewTransition?.skipTransition();
});
// pagereveal روی سندی که وارد میشود، پیش از نخستین رسم فرستاده میشود
router.onPageReveal((e) => {
console.log('new page ready');
});SPA و MPA با هم
const router = createRouter({ viewTransition: 'both' });ناوبریهای SPA از startViewTransition() استفاده میکنند و ناوبریهای تمامصفحه از قاعده CSSی @view-transition. هرجا شد گذار را با JS برانید، وگرنه CSS جایگزین میشود.
view-transition-name — گذار عنصر مشترک
view-transition-name بهجای کل قاب دید، یک عنصر مشخص را میان دو صفحه متحرک میکند. مرورگر جا و اندازه آن عنصر را در هر دو سو میگیرد و میانشان انیمیشن میسازد. این همان جلوه بازشدن کارت در نمونه پروفایلهای Chrome است.
کاربرد پایه
به عنصر «یکسان» در صفحه مبدأ و مقصد نام یکسانی بدهید:
<!-- صفحه فهرست -->
<div class="card" style="view-transition-name: profile-42">
<img src="avatar.jpg" />
<span>Jane Doe</span>
</div><!-- صفحه جزئیات -->
<div class="profile-header" style="view-transition-name: profile-42">
<img src="avatar.jpg" />
<h1>Jane Doe</h1>
</div>مرورگر خودش کارت را از جای فهرستیاش تا جای جزئیاتش، با تغییر شکل، متحرک میکند.
نامهای پویا در یک فهرست
view-transition-name باید در هر صفحه یکتا باشد. شناسه آیتم را بخشی از نام کنید:
/* رویکرد CSS — یک قاعده برای هر کارت */
.card[data-id='1'] {
view-transition-name: card-1;
}
.card[data-id='42'] {
view-transition-name: card-42;
}// رویکرد JS — نام را درست پیش از ناوبری تعیین کن
function navigateToProfile(id) {
const card = document.querySelector(`.card[data-id="${id}"]`);
card.style.viewTransitionName = `profile-${id}`;
router.push(`/profiles/${id}`);
}در صفحه مقصد، نام متناظر را پیش از نخستین رسم تعیین کنید:
// بیدرنگ (و همگام) تعیین کنید تا مرورگر آن را بگیرد
const id = router.currentRoute?.params.id;
document.querySelector('.profile-header').style.viewTransitionName = `profile-${id}`;گذارهای لغزشیِ جهتدار
یک نگهبان beforeEach را با یک ویژگی سفارشی CSS ترکیب کنید تا برای هر جهت ناوبری انیمیشن متفاوتی بسازید:
const pages = ['/', '/step-1', '/step-2', '/step-3'];
router.beforeEach((to, from, next) => {
const toIdx = pages.indexOf(to.path);
const fromIdx = pages.indexOf(from?.path ?? '');
document.documentElement.dataset.navDir = toIdx >= fromIdx ? 'forward' : 'back';
next();
});@keyframes slide-from-right {
from {
translate: 100% 0;
}
}
@keyframes slide-from-left {
from {
translate: -100% 0;
}
}
@keyframes slide-to-right {
to {
translate: 100% 0;
}
}
@keyframes slide-to-left {
to {
translate: -100% 0;
}
}
[data-nav-dir='forward']::view-transition-old(root) {
animation: 300ms ease slide-to-left;
}
[data-nav-dir='forward']::view-transition-new(root) {
animation: 300ms ease slide-from-right;
}
[data-nav-dir='back']::view-transition-old(root) {
animation: 300ms ease slide-to-right;
}
[data-nav-dir='back']::view-transition-new(root) {
animation: 300ms ease slide-from-left;
}برای بیرون گذاشتن یک عنصر از گذار، view-transition-name: none را به کار ببرید. برای متحرککردن مستقلِ چند بخش، به هرکدام نامی یکتا بدهید؛ هرچه نام نداشته باشد با گذار ریشه محو میشود.
SSR / SSG
همه APIهای مرورگر (window، history، document) با بررسی typeof محافظت شدهاند، پس صدا زدن createRouter در محیط SSR روی Node یا Deno امن است. در بستر SSR، push و replace نگهبانها را اجرا میکنند و currentRoute را بهروز میکنند اما history.pushState / history.replaceState را رد میکنند. شنوندههای popstate هرگز سمت سرور ثبت نمیشوند. روی کلاینت مثل همیشه هیدریت کنید: createRouter را دوباره با همان پیکربندی صدا بزنید.
مرجع تایپها
interface RouteLocation {
path: string; // مثلاً '/users/42'
params: Record<string, string>; // مثلاً { id: '42' }
query: Record<string, string>; // مثلاً { tab: 'profile' }
fullPath: string; // مثلاً '/users/42?tab=profile'
}
type ViewTransitionMode = 'spa' | 'mpa' | 'both';
interface RouterConfig {
mode?: 'history' | 'hash';
base?: string;
routes?: RouteConfig[];
viewTransition?: boolean | ViewTransitionMode;
}
interface RouteConfig {
path: string;
exact?: boolean;
meta?: Record<string, unknown>;
children?: RouteConfig[];
}
type NavigationGuard = (
to: RouteLocation,
from: RouteLocation | null,
next: (redirect?: string | false) => void,
) => void;
type RouteChangeHandler = (to: RouteLocation, from: RouteLocation | null) => void;