@expressive/router

中文文档
0.10.4发布于 2026/11/11MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1988

Host-agnostic router built on Expressive MVC components.

原始使用文档
文档目录9 节
  1. Declaring routes
  2. Reading the match
  3. Navigation state, query & fragments
  4. Suspense and navigation settlement
  5. Links
  6. Redirect
  7. Generated navigation
  8. Runtime boundaries
  9. License

@expressive/router

Class-based declarative router built on Expressive MVC.

NPM


A host-agnostic router for Expressive MVC. Routes are declared as nested JSX, matching is computed lexically from that tree (not a separate config), and navigation state lives on a reactive Router any component can read or drive.

npm install @expressive/router @expressive/react react

Declaring routes

Routes nest to mirror the URL. to is the pattern segment, as is the page (or layout) component. A layout receives its matched children via children.

import '@expressive/react'; // registers the host adapter
import { BrowserRouter, Route } from '@expressive/router';

<BrowserRouter>
  <Route as={RootLayout}>
    <Route as={HomePage} /> {/* index - matches the parent path */}
    <Route to="blog" as={BlogLayout}>
      <Route as={BlogIndex} /> {/* /blog */}
      <Route to=":slug" as={BlogPost} /> {/* /blog/:slug */}
    </Route>
    <Route to="login" redirect="/" /> {/* matched -> redirects */}
    <Route none as={NotFound} /> {/* no sibling matched */}
  </Route>
</BrowserRouter>;
PropMeaning
toURL segment. :name captures a param, * is a catch-all. Omit for an index route.
asComponent rendered when matched (as a layout, receives children).
noneMatches when no sibling did - scoped to its parent (root = app 404, nested = section 404).
redirectWhen matched, redirect here instead of rendering.
label / metaDisplay name / free-form metadata for nav and breadcrumbs (ignored by matching).

Siblings competing for the same slot arbitrate first-match by declaration order. Without an ancestor Router, a <Route> spins up a headless in-memory one; BrowserRouter binds to the address bar.

Reading the match

A page reads the nearest Route from context with get() and uses its reactive getters:

import { Route } from '@expressive/router';

const BlogPost = () => {
  const { match } = Route.get(); // nearest Route in context; subscribes
  return <article>post: {match!.slug}</article>;
};

Read matched (boolean) in render so same-pattern navigations (/blog/a → /blog/b) reconcile in place instead of remounting.

Navigation state, query & fragments

Router exposes location as reactive path, query, hash, and derived url surfaces. Query and fragment state round-trip through navigation and history.

router.goto('/posts?page=2#comments'); // push
router.goto('/posts', true); // replace
router.url = '/posts?page=2#comments'; // assigning url navigates

router.query.get('page'); // read - subscribes to just this param
router.query.set('page', '2'); // write - pushes a new entry, like goto
router.query.delete('sort'); // delete - also navigates

router.hash; // '' or a leading-# string
router.hash = '#comments'; // pushes through the same settlement path

router.back(); // one entry back
router.go(-2); // move by an arbitrary history delta

The map is single-valued (string keys and values); repeated URL keys collapse to the last value. URL-driven changes reconcile the same map in place. The hash is an opaque, percent-encoded string. Fragment navigation does not scroll or focus an element automatically.

go(delta) truncates finite fractional deltas to integer steps. Zero, non-finite, and out-of-range deltas do nothing; it never uses go(0) as a reload signal.

Suspense and navigation settlement

Routes are Suspense boundaries. Pass fallback for cold load and use a lazy page normally:

const Post = lazy(() => import('./Post'));

<Route to="posts/:id" fallback={<Spinner />} as={Post} />;

In-app navigation runs through protected Router.navigate(work), whose default uses the host transition scheduler. If the next page or entry guard suspends, the current screen remains visible until the next one is ready. Cold load still renders fallback.

router.navigating remains true through presentation. Read it beside or around the routed content; overlapping navigation is latest-wins, so superseded work cannot later change the page, history, or status. For app-driven browser navigation, the address is written after the screen settles. Browser Back/Forward and external History API calls necessarily change it first.

Override navigate(work) on a Router subclass to stage presentation differently. Status and ordering wrap the seam, so the override need only run and settle work.

Link renders an <a> that navigates internal targets on plain left-click. Modifier/middle clicks, non-_self targets, downloads, and scheme-bearing or protocol-relative targets remain browser-owned. It exposes its own match state, so active styling needs no separate component:

import { Link } from '@expressive/router';

<Link to="blog">Blog</Link>
<Link to="/posts?page=2" replace>Page 2</Link>
<Link to="https://example.com/docs">External docs</Link>
// active links by subclassing - read `active` / `match`
class NavLink extends Link {
  render() {
    return (
      <a
        href={this.href}
        onClick={this.go}
        className={this.active ? 'active' : undefined}
        aria-current={this.active ? 'page' : undefined}>
        {this.props.children}
      </a>
    );
  }
}

Redirect

Redirect navigates to to when mounted, gated on when (default true), and renders nothing. The Route redirect prop is an always-replace shorthand for it.

import { Redirect } from '@expressive/router';

<Redirect to="/login" when={!user} />     {/* navigates on mount when `when` is true */}
<Redirect to="/home" replace />           {/* overwrite the current entry */}

Route.redirect also accepts a synchronous or async entry guard. A non-empty string redirects with replacement, a falsy value allows the route, and null cedes the path to the nearest scoped none Route:

<Route
  to="admin"
  fallback={<Spinner />}
  redirect={async () => (session.authorize() ? undefined : '/login')}
  as={Admin}
/>

The router coordinates navigation, not application data. Keep fetching, mutations, caches, and request cancellation in the route/page control; async fields suspend through the Route boundary.

Generated navigation

NavLinks renders a navigation tree from the route hierarchy - it walks the declared routes and emits links automatically. Its rendering is built from PascalCase subcomponents you override to shape the output:

MemberRenders
ItemA single link (defaults to a Link using the route's label).
ListThe container wrapping a level of items.
GroupA nested section; transparent by default - override to turn route nesting into headed sections.
import { NavLinks } from '@expressive/router';

class SideNav extends NavLinks {
  List = (props) => <ul className="side">{props.children}</ul>;
  Group = (props) => (
    <section>
      <h3>{props.route.label}</h3>
      {props.children}
    </section>
  );
}

These members are overridable reactive subcomponents bound to the live instance - the same mechanism Component provides, via subcomponents.

Runtime boundaries

  • Router is headless and keeps in-memory history - use it for tests and non-browser hosts.
  • BrowserRouter binds the browser address and History API.
  • Server rendering does not share a global router between requests, but the package is not an SSR routing/data/hydration system.
  • React Native can use Router; BrowserRouter, Link, and NavLinks are browser/DOM-facing.

The supported location model is pathname, single-valued query parameters, and an opaque fragment. Basename mounting, arbitrary history state, automatic scroll-to-anchor, scroll restoration, navigation blocking, and data-router APIs are not currently built in.


Routes are plain @expressive/mvc Components, so they render under any Expressive host adapter.

Full guide and examples → expressive.dev/docs/guides/router

License

MIT

版本与发布记录

@expressive/router 共发布 26 个版本,首次发布于 2026/06/13;过去 12 个月发布了 26 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.10.4latest2026/11/113 天前
0.10.32026/11/104 天前
0.10.22026/11/059 天前
0.10.12026/11/0212 天前
0.10.02026/11/0212 天前
0.9.62026/10/3114 天前

关于 @expressive/router 的常见问题

@expressive/router 是什么?

Host-agnostic router built on Expressive MVC components.

如何安装 @expressive/router?

在项目目录运行 npm install @expressive/router,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@expressive/mvc(^0.94.3)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@expressive/router 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@expressive/router 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@expressive/router 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@expressive/router 可以商用吗?

@expressive/router 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@expressive/router 的下载量有多少?

根据 npm 官方下载统计,@expressive/router 在 2026/10/14 – 2026/11/12 这 30 天内被下载 1,988 次,日均约 66 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@expressive/router 还在维护吗?

最新版本 0.10.4 发布于 2026/11/11(3 天前);至今共发布 26 个版本,其中过去 12 个月发布了 26 个;首次发布于 2026/06/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。