路由
PreffX 通过 url 工具提供轻量级、基于信号的客户端路由机制。无需导入路由组件 —— 您将获得一个响应式的 ReadonlySignal<URL>,它通过 Navigation API 与浏览器位置保持同步。
url 信号
每个 PreffX 组件在其第二个参数(utils)中都会收到 url。
| 属性 | 类型 | 描述 |
|---|---|---|
url | ReadonlySignal<URL> | 响应式信号,其 .value 为当前 URL 对象。当用户导航到同源页面时发生变化。 |
类型签名
typescript
type PreffXUtils = {
url: ReadonlySignal<URL>;
// ...
};基本路由
使用 url.value 读取当前路径名、搜索参数、哈希或任何其他 URL 属性。结合 computed 在视图之间进行响应式切换。
tsx
import type { PC } from 'preffx';
const HomePage: PC = () => <div>首页</div>;
const ContactsPage: PC = () => <div>联系方式</div>;
const NotFound: PC = () => <div>页面未找到</div>;
export const App: PC = (_, { computed, url }) => {
const page = computed(() => {
switch (url.value.pathname) {
case '/':
return <HomePage />;
case '/contacts':
return <ContactsPage />;
default:
return <NotFound />;
}
});
return (
<div>
<nav>
<a href="/">首页</a>
<a href="/contacts">联系方式</a>
</nav>
<main>{page}</main>
</div>
);
};工作原理
- 像往常一样渲染
<a href="/contacts">链接。 - 当用户点击链接时,浏览器发出
navigate事件。 - PreffX 拦截该事件(调用
event.preventDefault())并更新内部url信号。 - 任何依赖
url.value的computed都会重新求值,DOM 也会响应式更新。
导航链接
使用标准的带 href 的 <a> 元素。PreffX 会自动处理拦截 —— 无需特殊的 <Link> 组件。
tsx
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
<a href="/contacts">联系方式</a>
<a href="/products?category=books&sort=asc">图书</a>
</nav>PreffX 的拦截器仅在至少一个组件正在使用 url 信号时激活(通过内部 URL_WATCHERS 计数器追踪)。这意味着:
- 如果没有组件读取
url,点击<a>链接的行为正常(完整页面导航)。 - 一旦组件访问
url.value,导航即被拦截并在客户端处理。
读取 URL 各部分
因为 url 是 ReadonlySignal<URL>,您可以读取原生 URL 对象的任何属性:
tsx
const CurrentRoute: PC = (_, { url }) => {
return (
<div>
<p>路径名: {url.value.pathname}</p>
<p>搜索: {url.value.search}</p>
<p>哈希: {url.value.hash}</p>
<p>主机: {url.value.host}</p>
<p>源: {url.value.origin}</p>
</div>
);
};这些都是响应式的 —— 其中任何属性的变化都会触发依赖信号的重新计算。
路由参数(查询字符串)
在 computed 内部通过 url.value.searchParams 解析查询参数:
tsx
import type { PC } from 'preffx';
const ProductList: PC = (_, { computed, url }) => {
const category = computed(() =>
url.value.searchParams.get('category') ?? '全部'
);
const sort = computed(() =>
url.value.searchParams.get('sort') ?? '名称'
);
return (
<div>
<p>分类: {category}</p>
<p>排序: {sort}</p>
<a href="/products?category=books&sort=asc">图书(升序)</a>
<a href="/products?category=books&sort=desc">图书(降序)</a>
</div>
);
};动态片段(路径参数)
由于 url 提供完整的 URL 对象,您可以手动解析路径片段:
tsx
import type { PC } from 'preffx';
const UserProfile: PC = (_, { computed, url }) => {
const segments = computed(() => url.value.pathname.split('/').filter(Boolean));
// pathname = "/users/42" → segments = ["users", "42"]
const userId = computed(() => segments.value[1]);
return <div>用户 ID: {userId}</div>;
};对于更结构化的方法,构建一个简单的路由匹配器:
tsx
import type { PC } from 'preffx';
// 简单路由模式 → 参数提取器
function match(pattern: string, pathname: string): Record<string, string> | null {
const patternParts = pattern.split('/').filter(Boolean);
const pathParts = pathname.split('/').filter(Boolean);
if (patternParts.length !== pathParts.length) return null;
const params: Record<string, string> = {};
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
params[patternParts[i].slice(1)] = pathParts[i];
} else if (patternParts[i] !== pathParts[i]) {
return null;
}
}
return params;
}
const RouteExample: PC = (_, { computed, url }) => {
const route = computed(() => {
const { pathname } = url.value;
const params = match('/users/:id', pathname);
if (params) return <div>用户 {params.id}</div>;
const postParams = match('/posts/:postId', pathname);
if (postParams) return <div>文章 {postParams.postId}</div>;
return <div>未找到</div>;
});
return (
<div>
<nav>
<a href="/users/42">用户 42</a>
<a href="/users/7">用户 7</a>
<a href="/posts/abc">文章 abc</a>
</nav>
<main>{route}</main>
</div>
);
};跨源导航
导航拦截器仅适用于同源导航。跨源链接(不同协议、主机或端口)保持不变,正常触发完整页面加载。
tsx
<a href="https://example.com">外部链接</a> <!-- 完整导航 -->
<a href="/internal">内部链接</a> <!-- 客户端导航 -->浏览器支持
路由依赖于 Navigation API(globalThis.navigation)。
自 2026 年 1 月起,此功能适用于最新的设备和浏览器版本。此功能可能在旧设备或浏览器上无法使用。