开发
本文介绍资源的结构、register.ts 的 declare / activate 生命周期、依赖声明(deps)、clientx create 生成的骨架、自定义块与 i18n。
1. 资源结构
一个资源是 src/resource/<id>/ 下的一个目录,识别它的标志是资源根目录下的 register.ts:
src/resource/msgx/ # 以内置 msgx 为例
├── register.ts # 自声明入口(declare / activate)
├── api/ # 服务端 API 客户端
├── stores/ # zustand 状态
├── hooks/ # react hooks(副作用/数据订阅)
├── modules/ # 路由页面组件
├── components/ # 复用组件
├── conversation-provider.tsx # 会话 provider
├── i18n/
│ ├── zh.json
│ └── en.json
└── package.json # 可选:clientx.deps 声明式依赖
资源按 package.json 的 clientx 字段描述自身:
{
"name": "@me/hello",
"clientx": {
"id": "hello",
"kind": "app",
"deps": ["client"],
"entry": "register.ts"
}
}
id:资源 id,等于目录名。kind:app/user/msg/agent/model/content/tool/skill/mcp/site。deps:声明式依赖(与源码 import 推导取并集)。entry:自声明入口,固定为register.ts。
2. register.ts:declare / activate
每个资源导出 declare(同步、轻量) 与 activate(异步、重量) 两个生命周期函数,签名约定有两种,prod 加载器优先取 uniform 的 declare/activate,否则回退到 declare<Id>/activate<Id>:
declare:进主 chunk,同步登记资源身份、路由(懒加载组件)、纯数据 feature / 站点内容。不做任何重活。activate:动态 import 重量实现(api/stores/hooks/组件/realtime mount),只有资源未被禁用(absent) 时才调用 —— 门控同时管「下载」。
以内置 msgx 的 register.ts 为例:
// src/resource/msgx/register.ts
import {
resourceRegistry, slotRegistry, featureRegistry, defaultResources, routeRegistry,
} from '@clientx/runtime/lib/kernel/instance'
import { ROUTES } from '@clientx/runtime/lib/kernel/routes'
import type { Resource } from '@clientx/runtime/lib/kernel/types'
export const MSGX_RESOURCE: Resource = {
id: 'msgx',
kind: 'msg',
source: { type: 'config' },
endpoint: '/api/msgx',
dependsOn: ['app', 'user'], // 类型级依赖
}
export function declareMsgx(): void {
resourceRegistry.declare(MSGX_RESOURCE) // 1. 登记资源身份
defaultResources.register(MSGX_RESOURCE) // 2. 加入默认资源目录
// 3. 注册路由(组件懒加载, loader 只引 core 的 queryClient/queries)
routeRegistry.register('msgx', [
{ id: ROUTES.sessions, path: 'sessions', parent: ROUTES.app,
component: () => import('./modules/session-list-page') },
])
// 4. 纯数据 feature
featureRegistry.register({ id: 'unread-sync', dependsOn: ['msgx'] })
}
export async function activateMsgx(): Promise<void> {
// 1. 动态 import 重量实现
const { keysApi, messageApi } = await import('./api/msgx')
const { msgxProvider } = await import('./conversation-provider')
const { SessionsUnreadBadge } = await import('./hooks/sessions-unread-badge')
// 2. 注册能力(capability)—— 供其他资源经接口调用
capabilityRegistry.register(MSGX_SEND_CAPABILITY, messageApi)
// 3. 注册会话 provider 与槽位(slot)
conversationRegistry.register(msgxProvider)
slotRegistry.register('tab-unread:sessions', SessionsUnreadBadge)
// 4. 副作用 feature:realtime WS 生命周期
featureRegistry.register({
id: 'realtime',
dependsOn: ['msgx'],
mount: () => {
const connect = () => { /* ... */ }
connect()
return () => { /* 清理 */ }
},
})
}
3. 资源生命周期
关键不变式:
- declare 必须同步:身份/路由/纯数据在首帧就绪;activate 可以异步。
- 门控管下载:被用户关闭的资源在 import/declare/activate 之前被过滤 —— 不下载、不激活。
- 依赖拓扑序:dev 与 prod 都按依赖序先激活依赖资源(
client永远排最前)。
4. 依赖声明:deps
闭包解析(resolve-closure.mjs)用两种方式推导资源依赖,取并集:
- 源码 import 推导:扫描资源内
import ... from '@/resource/<other>/...',other即依赖。 - 声明式 deps:资源自带
package.json的clientx.deps(clientx create --deps a,b写入)。骨架资源没有跨资源 import 时靠它参与闭包。
node scripts/p3/resolve-closure.mjs --resources userx # 查看闭包与每资源 external
示例输出(节选):
{ "closure": [ { "id": "userx", "entry": "src/resource/userx/register.ts", "deps": ["client"], "externals": [...] } ] }
闭包的 deps 是资源 id(构建期解析结果);Resource.dependsOn 里的 app/user 是类型/kind级约束(打开某资源前需有该 kind 的资源),二者不要混淆。userx 构建期闭包依赖实测为 ["client"]。
依赖规则:
- 依赖必须是已知资源(
src/resource/<id>/register.ts存在)。create时就会校验。 - 闭包是传递的:
A → B → C时声明A会自动带上B、C。 - 每个依赖产生一个
@/resource/<dep>/的 rollup external(跨资源包前缀),运行时由 importmap 精确解析。
5. 自定义块(site 资源)
site 类资源(如 portal)用 sitex 的块注册表渲染页面。内置块有 text / cards / feature / stats / cta / news / logos;自定义块在 activate 里注册:
// resource/portal/register.ts
import { registerBlock } from '@/resource/sitex/blocks'
export async function activatePortal(): Promise<void> {
const { DownloadCard } = await import('./components/download-card')
const { PortalChangelog } = await import('./components/portal-changelog')
registerBlock('download', DownloadCard) // 自定义块: 下载卡片
registerBlock('portal-changelog', PortalChangelog) // 自定义块: 版本日志
}
块是纯数据 + renderer:内容里只有 { type, ...props },渲染时按 type 查块注册表。独立文档站(如 docs / help)则作为 Astro 静态资源构建,不进入运行时资源闭包。
6. i18n
资源自带 i18n/zh.json 与 i18n/en.json,键统一挂在资源名下(portal.nav.product 等)。declare/activate 时由资源注册系统(loadEnabledResourceLocales / loader 的 loadResourceLocales)按启用资源注入 i18next;禁用资源不下载语言包。
{ "portal": { "nav": { "product": "产品&功能", "docs": "文档&开发" } } }
在组件里用 useTranslation() 的 t() 取文案;站点 chrome 的 titleKey / labelKey / bodyKey 全部走 i18n key。注意:当前 markdown 文档按 slug 加载(docs/<slug>.md,不分语言),文档本体为中文;中英双语分文件(<slug>.zh.md / <slug>.en.md)作为后续增强。
7. 下一步
- 「打包」页:
clientx build如何把资源闭包产出dist/。 - 「案例」页:一个最小
hello资源的完整源码。