@barefootjs/router

中文文档
0.40.2发布于 2026/11/02MITTypeScriptES Module构建来源已验证近 30 天下载 3719

Backend-agnostic partial-navigation client router for BarefootJS — swaps only the page region and re-hydrates the islands inside it

原始使用文档
文档目录8 节
  1. Public API
  2. Options
  3. Correct by default
  4. Behaviour
  5. <head> : metadata is reconciled, resources are not
  6. Page metadata — reconciled on every swap, always
  7. Head resources — untouched
  8. Scope

@barefootjs/router

Backend-agnostic, progressively-enhanced client router for BarefootJS. It intercepts same-origin navigations, fetches the ordinary full-page HTML any backend already returns (no protocol header, no JSON endpoint), swaps only the page region, and disposes/re-hydrates just the islands inside it. The shell stays mounted; everything outside the region keeps its DOM, scroll, and state.

import { startRouter } from '@barefootjs/router'

startRouter() // install once on the client

Mark the swappable region in your layout with <Region> (compiled to [bf-region], see spec/router.md); the router swaps the first match.

Public API

  • startRouter(options?): Router — install once; SSR no-op. Returns { stop, navigate, prefetch }.
  • navigate(href, { history? }): Promise<void> — programmatic navigation.
  • BF_REGION — re-export of the bf-region marker for server-side helpers.

Options

optiondefaultpurpose
region[bf-region]selector for the swap point
rehydrate / disposeruntime fallback chainoverride island lifecycle
loadModuleimport(src)how island modules are loaded
shouldInterceptsame-origin, plain clickper-anchor opt-out (data-bf-router="false", download, target, rel=external)
prefetch / prefetchDelaytrue / 65hover/focus/pointerdown prefetch + modulepreload
cacheFreshMs / cacheStaleMs / cacheCap15000 / 60000 / 30SWR + LRU snapshot cache
scrollToToptruescroll to top after a swap, instantly (ignores scroll-behavior: smooth)
manageFocustruemove focus into the swapped content (first swapped region with a heading) + announce the route
morphtruepreserve [data-bf-permanent] live nodes across a swap (no-op when none present); false forces a plain replaceChildren

Correct by default

The client runtime is an optional peer: a fully static shell ships the router with zero @barefootjs/client. When islands are present, dispose and rehydrate degrade through the same fallback chain (window.__bf_* seams → @barefootjs/client/runtime's disposeScope/rehydrateScope) and never silently no-op. There is no opt-in setup step.

Behaviour

  • Last-wins across overlapping navigations (a newer nav aborts the older).
  • SWR cache stores promises (a prefetch + click share one request); aging entries serve instantly and refresh in the background; failures aren't cached.
  • Module-aware: a response's new island modules are imported before the re-hydration walk, and deduped across navigations.
  • Redirect-aware: history commits at the response's final URL.
  • In-page anchors stay the browser's: a same-page #hash link is not intercepted, and the popstate the browser fires for it (and for back/forward between such entries) swaps nothing — the page is already displayed, so island state and scroll position are kept.
  • History.state preserved: a router replace merges rather than clobbers existing state (scroll-restoration libs, framework state).
  • A11y: focus moves into the swapped content — the first swapped region that has a heading, at that heading — and the new title is announced via a polite live region. Sibling regions can therefore keep their natural document order (navigation before content) without focus landing in the navigation.
  • Head metadata: title, description, og:/twitter:, canonical and friends are reconciled against the incoming page; head resources are not (see below).
  • Persistence (data-bf-permanent): an element marked <div data-bf-permanent="player"> keeps its live node across a swap — its state, media playback, scroll, and hydrated scope survive — matched between documents by the attribute value (or id). A no-op when no element is marked; pass morph: false for a plain swap.
  • Swap in flight (data-bf-navigating): set on <html> for the duration of a region swap. A swap commits the new markup before re-hydrating it, so "present in the DOM" and "interactive" are two moments — until this clears, a swapped-in island may still be server markup with no handlers, and a click on it is lost. Style off it for a loading indicator, or wait for its absence before driving new content. NAVIGATING_ATTR is exported so callers need not hard-code the string. Query-only navigations swap nothing and never set it.

<head>: metadata is reconciled, resources are not

A region is a body subtree, so <head> is not swapped wholesale. It splits in two, and the split is the whole contract.

Page metadata — reconciled on every swap, always

Page metadata is page-scoped by definition, so the router brings a closed allowlist of it in line with the incoming document. This always runs and there is no flag to disable it: a stale <meta name="description"> is wrongness you cannot see in development (unlike the tab title), and this package doesn't leave that class opt-in.

head nodekey
<title>—
<meta name="description | keywords | robots | author | theme-color">name
<meta property="og:*"> / <meta name="twitter:*">name or property
<link rel="canonical | alternate | prev | next">rel + hreflang/type/media

A key in both documents is replaced (skipped when the nodes are already equal, so metadata shared across routes causes no DOM churn); a key only in the incoming page is added; a key only in the live page is removed, so it can't leak forward into every later route.

Anything whose key isn't in that table is never read, replaced, or removed — runtime-injected analytics tags, CSP <meta http-equiv>, <link rel=preconnect> and friends are safe by construction. (This is the deliberate difference from Turbo, which removes every untracked head element.) Opt a node out with data-bf-head="false" when the page itself owns it.

Head resources — untouched

<link rel="stylesheet">, <script>, and <style> in <head> are left alone in both directions. Not an oversight: a resource's lifetime isn't derivable from the incoming document. The shell, a [data-bf-permanent] node, a portal, or an island that outlives the region may still depend on a sheet the next page's head doesn't list, so "absent downstream" is no evidence of "no longer needed".

That makes a route-scoped stylesheet in <head> the one real trap: navigating into the route renders it unstyled (a reload "fixes" it, which points the investigation at caching or the build instead of at navigation), and navigating out leaves the sheet linked, so its rules then apply to every route after it.

Put it inside the region, where it enters and leaves with the swap:

<Region>
  {isEditor ? <link rel="stylesheet" href="/editor.css" /> : null}
  {children}
</Region>

rel="stylesheet" is body-ok per HTML, so this is valid — and it is the right placement under a region-swap contract, not a workaround. It gets both orderings right by construction (the sheet is inserted with the content it styles and removed with it), with no load awaited in the navigation path. Sheets that are genuinely global stay in <head>, where never touching them is exactly what you want.

Scope

A single authored region (the broadest [bf-region] match), correct by default. searchParams() (query-only navigation without a swap) ships in @barefootjs/client (v0.5); the router drives it via the __bf_pushSearch seam, so query-only navigations short-circuit once a consumer is present. data-bf-permanent persistence is v1. Compiler-derived nested regions (deepest-differs swap, sibling master–detail) are v2.

版本与发布记录

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

版本发布日期距今
0.40.2latest2026/11/025 天前
0.40.12026/10/299 天前
0.40.02026/10/2612 天前
0.39.92026/10/2513 天前
0.39.82026/10/2117 天前
0.39.72026/10/1820 天前

关于 @barefootjs/router 的常见问题

@barefootjs/router 是什么?

Backend-agnostic partial-navigation client router for BarefootJS — swaps only the page region and re-hydrates the islands inside it

如何安装 @barefootjs/router?

在项目目录运行 npm install @barefootjs/router,或使用 pnpm add / yarn add / bun add。

@barefootjs/router 支持 TypeScript 吗?

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

@barefootjs/router 支持 ESM 还是 CommonJS?

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

@barefootjs/router 可以商用吗?

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

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

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

@barefootjs/router 还在维护吗?

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