开发

本文介绍资源的结构、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"
  }
}

2. register.ts:declare / activate

每个资源导出 declare(同步、轻量) 与 activate(异步、重量) 两个生命周期函数,签名约定有两种,prod 加载器优先取 uniform 的 declare/activate,否则回退到 declare<Id>/activate<Id>:

以内置 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. 资源生命周期

关键不变式:

4. 依赖声明:deps

闭包解析(resolve-closure.mjs)用两种方式推导资源依赖,取并集:

  1. 源码 import 推导:扫描资源内 import ... from '@/resource/<other>/...',other 即依赖。
  2. 声明式 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"]。

依赖规则:

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. 下一步