Skip to content

路由

PreffX 通过 url 工具提供轻量级、基于信号的客户端路由机制。无需导入路由组件 —— 您将获得一个响应式的 ReadonlySignal<URL>,它通过 Navigation API 与浏览器位置保持同步。

url 信号

每个 PreffX 组件在其第二个参数(utils)中都会收到 url

属性类型描述
urlReadonlySignal<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>
    );
};

工作原理

  1. 像往常一样渲染 <a href="/contacts"> 链接。
  2. 当用户点击链接时,浏览器发出 navigate 事件。
  3. PreffX 拦截该事件(调用 event.preventDefault())并更新内部 url 信号。
  4. 任何依赖 url.valuecomputed 都会重新求值,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 各部分

因为 urlReadonlySignal<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 APIglobalThis.navigation)。

自 2026 年 1 月起,此功能适用于最新的设备和浏览器版本。此功能可能在旧设备或浏览器上无法使用。

根据 Apache-2.0 许可证发布。