@client-compose/client

中文文档
0.16.0发布于 2026/10/30MITTypeScriptES Module构建来源已验证近 30 天下载 2945

Framework-agnostic Client Compose client: island registry, screen requests and render planning.

原始使用文档
文档目录8 节
  1. Usage
  2. Streams
  3. Actions
  4. Islands with pages
  5. Strings
  6. Testing
  7. Through your own server
  8. Links

@client-compose/client

Framework-agnostic client for Client Compose: a protocol for server-composed screens made of client-owned islands.

Tier: core · Protocol: v0 · Any JS runtime with fetch (browsers, React Native, Node 20+)

npm install @client-compose/client

Using React or React Native? Use @client-compose/react, which builds on this package.

Usage

IslandRegistry<R> maps island types and versions to renderers. R is whatever your UI renders (a DOM node, a string of HTML, a framework element), so this core works with any UI layer.

import { buildRequest, fetchScreen, IslandRegistry, plan } from "@client-compose/client";
import { ProfileHeaderV2 } from "./islands.generated"; // from @client-compose/codegen

const registry = new IslandRegistry<HTMLElement>()
  .register(ProfileHeaderV2, (props) => renderProfileHeader(props));

const request = buildRequest(registry, "profile", { platform: "web", appVersion: "1.4.0" }, { userId });
const response = await fetchScreen("https://api.example.com/compose", request, {
  headers: { Authorization: `Bearer ${token}` },
});

const islands = plan(registry, response, {
  onSkipped: (entry, reason) => console.warn("skipped island", entry.key, reason),
});
container.replaceChildren(...islands.map((island) => island.render()));
  • buildRequest sends the island types and versions this client can render, so the composer never returns one it can't.
  • plan returns renderable islands in order. Errored islands are omitted unless you pass onError; islands with an unknown type, version or status are skipped and reported.
  • registry.declare(...) names islands the app renders somewhere else, such as on the server: the registry can't render them, but actions say the app can draw them, and changes to their entities apply to them (spec §5.3).
  • fetchScreen reads the screen's stream to its end, and returns it whole. It throws ScreenFetchError (with status and body) for non-2xx responses, and accepts a custom fetch as its fourth argument.

Streams

A screen arrives as a stream (spec §4.6): its head, with any island not yet ready pending, then each of those as the composer resolves it. openScreen resolves once the head is in, so you can show the screen and fill it in:

const stream = await openScreen(baseUrl, request, init);
let screen = stream.head;
show(plan(registry, screen)); // pending islands are their placeholders, or left out
for await (const island of stream.islands) {
  screen = withIsland(screen, island);
  show(plan(registry, screen));
}
  • Register a placeholder for an island with registry.registerPlaceholder(Descriptor, () => …): plan shows it while the island is pending. Without one, the island is left out until it arrives.
  • An island the stream never brings (the connection dropped) arrives as an error, incomplete, retryable.
  • collectScreen(stream) reads the rest and returns the whole screen.
  • The body is read as it comes when your fetch gives a readable stream (browsers, Node, Deno, Bun, and expo/fetch in React Native). React Native's built-in fetch doesn't, so there the screen arrives all at once.

Actions

import { ActionFetchError, callAction } from "@client-compose/client";
import { FollowUserV1 } from "./actions.generated"; // from @client-compose/codegen

const res = await callAction("https://api.example.com/compose", FollowUserV1, { userId }, {
  client: { platform: "web", appVersion: "1.4.0" },
  islands: registry.capabilities(), // updates come at versions the app can render
  init: { headers: { Authorization: `Bearer ${token}` } },
});
if (res.status === "ok") showFollowers(res.result.followers); // and apply res.effects (spec §5.3)
else if (res.error.code === "blocked") showBlocked();
else showGenericError(); // codes added after this app was built
  • Declared errors are outcomes (status: "error"), typed per action. Anything else, such as invalid_input, unauthenticated or action_failed, throws ActionFetchError with status and code.
  • Each call sends a new idempotency key. Pass idempotencyKey yourself to retry the same intent.

Props aren't validated at runtime; generate types with @client-compose/codegen to keep them in sync with the composer.

Islands with pages

An island entry carries next while the island has more pages (spec §4.5). fetchIsland(baseUrl, buildIslandRequest(screen, params, entry, client, entry.next)) fetches the next one: the island's entry again, with that page's props and its own next. Without a cursor, it resolves the island again from its first page. The React binding's usePages does this for you.

Strings

Every word comes from the composer (spec §4.7). Register an island by a descriptor whose manifest declares strings, and its renderer's props have them as t, in the screen's language, with counted strings picking their words by its plural rules:

registry.register(ArticleListV1, (props) => `${props.t.heading}: ${props.t.articleCount({ count: props.total })}`);

The app strings come from POST {base}/strings:

const response = await fetchStrings(baseUrl, client);
const t = appStrings(AppStrings, response); // t.retry, t.uploaded({ count, total })

Send the person's language as client.locale; preferredLocale picks it from a device's tags or an Accept-Language header (preferredLocale("pl-PL,pl;q=0.9") is "pl-PL"), and the composer answers in the nearest language it has. formatString(template, values, plurals) and pluralCategory(plurals, count) are there for anything else. In tests, the fake composer answers strings: () => stringsResponse("en", { … }), and island(key, Descriptor, props, { strings }) gives an island its strings. catalogStrings(catalog) reads a catalog file (codegen's catalog) as the composer would send it, for tests that read the real words: t(Descriptor), app(AppStrings), island(…) with its strings, and stringsResponse().

Testing

@client-compose/client/testing is a fake composer for tests, with no test framework of its own. createFakeGateway answers screen and action requests from handlers and records them; pass its fetch to the client. Build responses with island, islandError, screen, screenError, actionOk, actionError and actionFailure, typed by the generated descriptors. A screen is sent whole (a head and an end); to control when each island arrives, answer with streamedScreen(id, [island(…), pending(key, Descriptor)]), then call its send(entry), end(), or cut() for a dropped connection.

import { actionError, createFakeGateway } from "@client-compose/client/testing";

const gateway = createFakeGateway({
  actions: { follow: () => actionError(FollowUserV1, { code: "blocked" }) },
});
const res = await callAction(baseUrl, FollowUserV1, { userId }, { client, islands: {}, fetch: gateway.fetch });
assert.equal(gateway.actionRequests()[0].input.userId, userId);

For code that calls the global fetch itself, such as a whole app rendered in a test, installFakeGateway(handlers) puts the fake composer in its place; call restore() afterwards.

For React, @client-compose/react/testing adds a provider config built around it.

Through your own server

@client-compose/client/proxy passes a browser's screen, island and action requests to the composer through your server, so it can add what the browser mustn't hold, such as a session kept in an httpOnly cookie. createComposerProxy({ baseUrl, client, headers, onActionResponse }) checks each request (anyone can call a server endpoint with anything), sets the client context and headers, and returns the composer's failures as data. @client-compose/react's viaProxy is the browser half.

Strings go through your server too: proxy.loadStrings().

版本与发布记录

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

版本发布日期距今
0.16.0latest2026/10/3011 天前
0.15.02026/10/3011 天前
0.14.02026/10/2911 天前
0.13.02026/10/2911 天前
0.12.02026/10/2911 天前
0.11.02026/10/2912 天前

关于 @client-compose/client 的常见问题

@client-compose/client 是什么?

Framework-agnostic Client Compose client: island registry, screen requests and render planning.

如何安装 @client-compose/client?

在项目目录运行 npm install @client-compose/client,或使用 pnpm add / yarn add / bun add。

@client-compose/client 支持 TypeScript 吗?

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

@client-compose/client 支持 ESM 还是 CommonJS?

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

@client-compose/client 可以商用吗?

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

@client-compose/client 的下载量有多少?

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

@client-compose/client 还在维护吗?

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