preact-iso
preact-iso 是 Preact 的同构异步工具集合。
¥preact-iso is a collection of isomorphic async tools for Preact.
"同构" 描述了可以在浏览器和服务器(理想情况下无缝)运行的代码。preact-iso
旨在支持这些环境,允许用户构建应用,而无需创建单独的浏览器和服务器路由,也不必担心数据或组件加载的差异。在预渲染期间,相同的应用代码可以在浏览器和服务器上使用,无需任何调整。
¥"Isomorphic" describes code that can run (ideally seamlessly) across both the browser and server. preact-iso
is made for supporting these environments, allowing users to build apps without having to create separate browser and server routers or worry about differences in data or component loading. The same app code can be used in the browser and on a server during prerendering, no adjustments necessary.
注意:虽然这是一个来自 Preact 团队的路由库,但在更广泛的 Preact/React 生态系统中,还有许多其他路由可供选择,你可能更喜欢使用它们,包括 wouter 和 react-router。这是一个不错的首选方案,但如果你愿意,也可以将你最喜欢的路由带到 Preact 中。
¥Note: Whilst this is a routing library that comes from the Preact team, many other routers are available in the wider Preact/React ecosystem that you may prefer to use instead, including wouter and react-router. It's a great first option but you can bring your favorite router to Preact if you prefer.
路由
¥Routing
preact-iso
为 Preact 提供了一个简单的路由,它包含传统的 API 和基于钩子的 API。<Router>
组件具有异步感知能力:当从一个路由转换到另一个路由时,如果传入路由暂停(抛出一个 Promise),则传出路由将保留,直到新路由准备就绪。
¥preact-iso
offers a simple router for Preact with conventional and hooks-based APIs. The <Router>
component is async-aware: when transitioning from one route to another, if the incoming route suspends (throws a Promise), the outgoing route is preserved until the new one becomes ready.
import {
lazy,
LocationProvider,
ErrorBoundary,
Router,
Route
} from 'preact-iso';
// Synchronous
import Home from './routes/home.js';
// Asynchronous (throws a promise)
const Profiles = lazy(() => import('./routes/profiles.js'));
const Profile = lazy(() => import('./routes/profile.js'));
const NotFound = lazy(() => import('./routes/_404.js'));
function App() {
return (
<LocationProvider>
<ErrorBoundary>
<Router>
<Home path="/" />
{/* Alternative dedicated route component for better TS support */}
<Route path="/profiles" component={Profiles} />
<Route path="/profile/:id" component={Profile} />
{/* `default` prop indicates a fallback route. Useful for 404 pages */}
<NotFound default />
</Router>
</ErrorBoundary>
</LocationProvider>
);
}
渐进式数据融合:当应用在客户端进行 hydrated 操作时,路由(在本例中为 Home
或 Profile
)将暂停。这会导致页面该部分的 hydration 被推迟到路由的 import()
解析完成之后,此时页面该部分会自动补齐 hydration。
¥Progressive Hydration: When the app is hydrated on the client, the route (Home
or Profile
in this case) suspends. This causes hydration for that part of the page to be deferred until the route's import()
is resolved, at which point that part of the page automatically finishes hydrating.
无缝路由:在客户端的路由之间切换时,路由会感知路由中的异步依赖。路由不会清除当前路由并在等待下一个路由时显示加载旋转图标,而是会保留当前路由,直到传入的路由加载完成,然后交换它们。
¥Seamless Routing: When switching between routes on the client, the Router is aware of asynchronous dependencies in routes. Instead of clearing the current route and showing a loading spinner while waiting for the next route, the router preserves the current route in-place until the incoming route has finished loading, then they are swapped.
预渲染
¥Prerendering
prerender()
使用 `preact-render-to-string` 将虚拟 DOM 树渲染为 HTML 字符串。prerender()
返回的 Promise 解析为具有 html
和 links[]
属性的对象。html
属性包含预渲染的静态 HTML 标记,而 links
是一个数组,其中包含生成页面上链接中找到的任何非外部 URL 字符串。
¥prerender()
renders a Virtual DOM tree to an HTML string using `preact-render-to-string`. The Promise returned from prerender()
resolves to an Object with html
and links[]
properties. The html
property contains your pre-rendered static HTML markup, and links
is an Array of any non-external URL strings found in links on the generated page.
主要用于通过 `@preact/preset-vite` 或其他共享 API 的预渲染系统进行预渲染。如果你通过任何其他方法在服务器端渲染应用,则可以直接使用 preact-render-to-string
(特别是 renderToStringAsync()
)。
¥Primarily meant for use with prerendering via `@preact/preset-vite` or other prerendering systems that share the API. If you're server-side rendering your app via any other method, you can use preact-render-to-string
(specifically renderToStringAsync()
) directly.
import {
LocationProvider,
ErrorBoundary,
Router,
lazy,
prerender as ssr
} from 'preact-iso';
// Asynchronous (throws a promise)
const Foo = lazy(() => import('./foo.js'));
function App() {
return (
<LocationProvider>
<ErrorBoundary>
<Router>
<Foo path="/" />
</Router>
</ErrorBoundary>
</LocationProvider>
);
}
hydrate(<App />);
export async function prerender(data) {
return await ssr(<App />);
}
嵌套路由
¥Nested Routing
使用多个 Router
组件支持嵌套路由。部分匹配的路由以通配符 (/*
) 结尾,剩余值将被传递以继续匹配(如果还有其他路由)。
¥Nested routes are supported by using multiple Router
components. Partially matched routes end with a wildcard (/*
) and the remaining value will be passed to continue matching with if there are any further routes.
import {
lazy,
LocationProvider,
ErrorBoundary,
Router,
Route
} from 'preact-iso';
const NotFound = lazy(() => import('./routes/_404.js'));
function App() {
return (
<LocationProvider>
<ErrorBoundary>
<Router>
<Route path="/movies/*" component={Movies} />
<NotFound default />
</Router>
</ErrorBoundary>
</LocationProvider>
);
}
const TrendingMovies = lazy(() => import('./routes/movies/trending.js'));
const SearchMovies = lazy(() => import('./routes/movies/search.js'));
const MovieDetails = lazy(() => import('./routes/movies/details.js'));
function Movies() {
return (
<ErrorBoundary>
<Router>
<Route path="/trending" component={TrendingMovies} />
<Route path="/search" component={SearchMovies} />
<Route path="/:id" component={MovieDetails} />
</Router>
</ErrorBoundary>
);
}
这将匹配以下路由:
¥This will match the following routes:
/movies/trending
/movies/search
/movies/Inception
API 文档
¥API Docs
LocationProvider
提供当前位置给其子项的上下文提供程序。这是路由正常运行所必需的。
¥A context provider that provides the current location to its children. This is required for the router to function.
属性:
¥Props:
scope?: string | RegExp
- 设置路由将处理(拦截)的路径范围。如果路径与范围不匹配(无论是以提供的字符串开头还是与正则表达式匹配),路由都会忽略它并应用默认浏览器导航。¥
scope?: string | RegExp
- Sets a scope for the paths that the router will handle (intercept). If a path does not match the scope, either by starting with the provided string or matching the RegExp, the router will ignore it and default browser navigation will apply.
通常,你会将整个应用封装在此提供程序中:
¥Typically, you would wrap your entire app in this provider:
import { LocationProvider } from 'preact-iso';
function App() {
return (
<LocationProvider scope="/app">{/* Your app here */}</LocationProvider>
);
}
路由
¥Router
属性:
¥Props:
onRouteChange?: (url: string) => void
- 路由更改时调用的回调函数。¥
onRouteChange?: (url: string) => void
- Callback to be called when a route changes.onLoadStart?: (url: string) => void
- 当路由开始加载时(即,如果路由暂停)调用的回调函数。这不会在导航到同步路由或后续导航到异步路由之前调用。¥
onLoadStart?: (url: string) => void
- Callback to be called when a route starts loading (i.e., if it suspends). This will not be called before navigations to sync routes or subsequent navigations to async routes.onLoadEnd?: (url: string) => void
- 路由加载完成后(例如,如果挂起)调用的回调函数。这不会在导航到同步路由或后续导航到异步路由之后调用。¥
onLoadEnd?: (url: string) => void
- Callback to be called after a route finishes loading (i.e., if it suspends). This will not be called after navigations to sync routes or subsequent navigations to async routes.
import { LocationProvider, Router } from 'preact-iso';
function App() {
return (
<LocationProvider>
<Router
onRouteChange={url => console.log('Route changed to', url)}
onLoadStart={url => console.log('Starting to load', url)}
onLoadEnd={url => console.log('Finished loading', url)}
>
<Home path="/" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</LocationProvider>
);
}
路由
¥Route
使用 preact-iso
定义路由有两种方式:
¥There are two ways to define routes using preact-iso
:
将路由参数直接附加到路由组件:
<Home path="/" />
¥Append router params to the route components directly:
<Home path="/" />
请改用
Route
组件:<Route path="/" component={Home} />
¥Use the
Route
component instead:<Route path="/" component={Home} />
在 JavaScript 中将任意 props 附加到组件并非不合理,因为 JS 是一种动态语言,非常乐意支持动态和任意接口。然而,即使在编写 JS(通过 TS 的语言服务器)时,我们许多人也会使用的 TypeScript,它并不完全支持这种接口设计。
¥Appending arbitrary props to components not unreasonable in JavaScript, as JS is a dynamic language that's perfectly happy to support dynamic & arbitrary interfaces. However, TypeScript, which many of us use even when writing JS (via TS's language server), is not exactly a fan of this sort of interface design.
TS 尚不支持从父组件覆盖子组件的 props,因此我们不能将 <Home>
定义为不接受 props,除非它是 <Router>
的子组件,在这种情况下,它可以接受 path
props。这给我们带来了一些困扰:我们要么将所有路由定义为接受 path
的 props,这样在编写 <Home path="/" />
时就不会看到 TS 错误,要么创建封装器组件来处理路由定义。
¥TS does not (yet) allow for overriding a child's props from the parent component so we cannot, for instance, define <Home>
as taking no props unless it's a child of a <Router>
, in which case it can have a path
prop. This leaves us with a bit of a dilemma: either we define all of our routes as taking path
props so we don't see TS errors when writing <Home path="/" />
or we create wrapper components to handle the route definitions.
虽然 <Home path="/" />
完全等同于 <Route path="/" component={Home} />
,但 TS 用户可能更喜欢后者。
¥While <Home path="/" />
is completely equivalent to <Route path="/" component={Home} />
, TS users may find the latter preferable.
import { LocationProvider, Router, Route } from 'preact-iso';
function App() {
return (
<LocationProvider>
<Router>
{/* Both of these are equivalent */}
<Home path="/" />
<Route path="/" component={Home} />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
<NotFound default />
</Router>
</LocationProvider>
);
}
任何路由组件的属性:
¥Props for any route component:
path: string
- 要匹配的路径(继续阅读)¥
path: string
- The path to match (read on)default?: boolean
- 如果设置了此路由,则此路由是回退/默认路由,在其他路由均不匹配时使用。¥
default?: boolean
- If set, this route is a fallback/default route to be used when nothing else matches
特定于 Route
组件:
¥Specific to the Route
component:
component: AnyComponent
- 路由匹配时要渲染的组件¥
component: AnyComponent
- The component to render when the route matches
路径段匹配
¥Path Segment Matching
路径使用简单的字符串匹配算法进行匹配。可以使用以下特性:
¥Paths are matched using a simple string matching algorithm. The following features may be used:
:param
- 匹配任意 URL 片段,并将值绑定到标签(稍后可从useRoute()
中提取此值)¥
:param
- Matches any URL segment, binding the value to the label (can later extract this value fromuseRoute()
)/profile/:id
将匹配/profile/123
和/profile/abc
¥
/profile/:id
will match/profile/123
and/profile/abc
/profile/:id?
将匹配/profile
和/profile/123
¥
/profile/:id?
will match/profile
and/profile/123
/profile/:id*
将匹配/profile
、/profile/123
和/profile/123/abc
¥
/profile/:id*
will match/profile
,/profile/123
, and/profile/123/abc
/profile/:id+
将匹配/profile/123
、/profile/123/abc
¥
/profile/:id+
will match/profile/123
,/profile/123/abc
*
- 匹配一个或多个 URL 片段¥
*
- Matches one or more URL segments/profile/*
将匹配/profile/123
、/profile/123/abc
等。¥
/profile/*
will match/profile/123
,/profile/123/abc
, etc.
然后可以组合这些路由来创建更复杂的路由:
¥These can then be composed to create more complex routes:
/profile/:id/*
将匹配/profile/123/abc
、/profile/123/abc/def
等。¥
/profile/:id/*
will match/profile/123/abc
,/profile/123/abc/def
, etc.
/:id*
和 /:id/*
的区别在于,前者中的 id
参数会包含其后的整个路径,而后者中的 id
参数仅包含单个路径片段。
¥The difference between /:id*
and /:id/*
is that in the former, the id
param will include the entire path after it, while in the latter, the id
is just the single path segment.
/profile/:id*
,带有/profile/123/abc
¥
/profile/:id*
, with/profile/123/abc
id
是123/abc
¥
id
is123/abc
/profile/:id/*
,带有/profile/123/abc
¥
/profile/:id/*
, with/profile/123/abc
id
是123
¥
id
is123
useLocation()
一个与 LocationProvider
配合使用以访问位置上下文的钩子。
¥A hook to work with the LocationProvider
to access location context.
返回一个具有以下属性的对象:
¥Returns an object with the following properties:
url: string
- 当前路径和搜索参数¥
url: string
- The current path & search paramspath: string
- 当前路径¥
path: string
- The current pathquery: Record<string, string>
- 当前查询字符串参数 (/profile?name=John
->{ name: 'John' }
)¥
query: Record<string, string>
- The current query string parameters (/profile?name=John
->{ name: 'John' }
)route: (url: string, replace?: boolean) => void
- 以编程方式导航到新路由的函数。replace
参数可以选择性地用于覆盖历史记录,导航到其他位置而不保留历史记录堆栈中的当前位置。¥
route: (url: string, replace?: boolean) => void
- A function to programmatically navigate to a new route. Thereplace
param can optionally be used to overwrite history, navigating them away without keeping the current location in the history stack.
useRoute()
访问当前路由信息的钩子。与 useLocation
不同,此钩子仅适用于 <Router>
组件。
¥A hook to access current route information. Unlike useLocation
, this hook only works within <Router>
components.
返回一个具有以下属性的对象:
¥Returns an object with the following properties:
path: string
- 当前路径¥
path: string
- The current pathquery: Record<string, string>
- 当前查询字符串参数 (/profile?name=John
->{ name: 'John' }
)¥
query: Record<string, string>
- The current query string parameters (/profile?name=John
->{ name: 'John' }
)params: Record<string, string>
- 当前路由参数 (/profile/:id
->{ id: '123' }
)¥
params: Record<string, string>
- The current route parameters (/profile/:id
->{ id: '123' }
)
lazy()
制作组件的延迟加载版本。
¥Make a lazily-loaded version of a Component.
lazy()
接受一个解析为组件的异步函数,并返回该组件的封装器版本。封装器组件可以立即渲染,即使该组件仅在首次渲染时加载。
¥lazy()
takes an async function that resolves to a Component, and returns a wrapper version of that Component. The wrapper component can be rendered right away, even though the component is only loaded the first time it is rendered.
import { lazy, LocationProvider, Router } from 'preact-iso';
// Synchronous, not code-splitted:
import Home from './routes/home.js';
// Asynchronous, code-splitted:
const Profiles = lazy(() =>
import('./routes/profiles.js').then(m => m.Profiles)
); // Expects a named export called `Profiles`
const Profile = lazy(() => import('./routes/profile.js')); // Expects a default export
function App() {
return (
<LocationProvider>
<Router>
<Home path="/" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</LocationProvider>
);
}
lazy()
的结果还公开了一个 preload()
方法,可用于在需要渲染组件之前加载组件。完全可选,但在焦点、鼠标悬停等情况下非常有用,可以比平时更早地开始加载组件。
¥The result of lazy()
also exposes a preload()
method that can be used to load the component before it's needed for rendering. Entirely optional, but can be useful on focus, mouse over, etc. to start loading the component a bit earlier than it otherwise would be.
const Profile = lazy(() => import('./routes/profile.js'));
function Home() {
return (
<a href="/profile/rschristian" onMouseOver={() => Profile.preload()}>
Profile Page -- Hover over me to preload the module!
</a>
);
}
ErrorBoundary
一个用于捕获其下方组件树中错误的简单组件。
¥A simple component to catch errors in the component tree below it.
属性:
¥Props:
onError?: (error: Error) => void
- 捕获错误时调用的回调。¥
onError?: (error: Error) => void
- A callback to be called when an error is caught
import { LocationProvider, ErrorBoundary, Router } from 'preact-iso';
function App() {
return (
<LocationProvider>
<ErrorBoundary onError={e => console.log(e)}>
<Router>
<Home path="/" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</ErrorBoundary>
</LocationProvider>
);
}
hydrate()
Preact hydrate
导出的薄封装器,它会根据当前页面是否已预渲染,在填充和渲染提供的元素之间切换。此外,它会在尝试任何渲染之前检查自身是否在浏览器上下文中运行,这使得它在服务器端渲染 (SSR) 期间为空操作。
¥A thin wrapper around Preact's hydrate
export, it switches between hydrating and rendering the provided element, depending on whether the current page has been prerendered. Additionally, it checks to ensure it's running in a browser context before attempting any rendering, making it a no-op during SSR.
与 prerender()
函数配对。
¥Pairs with the prerender()
function.
参数:
¥Params:
jsx: ComponentChild
- 要渲染的 JSX 元素或组件¥
jsx: ComponentChild
- The JSX element or component to renderparent?: Element | Document | ShadowRoot | DocumentFragment
- 要渲染到的父元素。如果未提供,则默认为document.body
。¥
parent?: Element | Document | ShadowRoot | DocumentFragment
- The parent element to render into. Defaults todocument.body
if not provided.
import { hydrate } from 'preact-iso';
function App() {
return (
<div class="app">
<h1>Hello World</h1>
</div>
);
}
hydrate(<App />);
但是,这只是一个简单的实用方法。这并非必需使用,你始终可以直接使用 Preact 的 hydrate
导出。
¥However, it is just a simple utility method. By no means is it essential to use, you can always use Preact's hydrate
export directly.
prerender()
使用 preact-render-to-string
将虚拟 DOM 树渲染为 HTML 字符串。prerender()
返回的 Promise 解析为具有 html
和 links[]
属性的对象。html
属性包含预渲染的静态 HTML 标记,而 links
是一个数组,其中包含生成页面上链接中找到的任何非外部 URL 字符串。
¥Renders a Virtual DOM tree to an HTML string using preact-render-to-string
. The Promise returned from prerender()
resolves to an Object with html
and links[]
properties. The html
property contains your pre-rendered static HTML markup, and links
is an Array of any non-external URL strings found in links on the generated page.
主要与 `@preact/preset-vite` 的预渲染功能配对。
¥Pairs primarily with `@preact/preset-vite`'s prerendering.
参数:
¥Params:
jsx: ComponentChild
- 要渲染的 JSX 元素或组件¥
jsx: ComponentChild
- The JSX element or component to render
import {
LocationProvider,
ErrorBoundary,
Router,
lazy,
prerender
} from 'preact-iso';
// Asynchronous (throws a promise)
const Foo = lazy(() => import('./foo.js'));
const Bar = lazy(() => import('./bar.js'));
function App() {
return (
<LocationProvider>
<ErrorBoundary>
<Router>
<Foo path="/" />
<Bar path="/bar" />
</Router>
</ErrorBoundary>
</LocationProvider>
);
}
const { html, links } = await prerender(<App />);