funhub-material-starter
v0.0.5
Published
A starter for creating a funhub material package.
Readme
funhub-material-starter
一个用于开发、预览和发布 Funhub 物料包的 SDK 模板。
当前模板已按最新物料文档调整,核心链路是:
- 用
defineComponentPropsSchema((t) => ({ ... }))定义可编辑字段 - 用
getSchemaDefaultProps(...)取 schema 顶层默认值 - 用
defineMaterial(...)绑定serverComponent、clientComponent和propsSchema
快速开始
环境要求
- Node.js
>= 22.22.0 - 推荐使用
nvm管理 Node 版本,项目内已提供.nvmrc和.node-version
Node 安装与版本管理
方案一:使用 nvm(推荐)
nvm 官方 GitHub:https://github.com/nvm-sh/nvm
安装完成后,在项目目录执行:
nvm install
nvm use
node -v如果你想手动指定版本,也可以执行:
nvm install 22.22.0
nvm use 22.22.0方案二:直接安装 Node.js
Node.js 官方 GitHub:https://github.com/nodejs/node
如果不使用版本管理器,请直接安装 22.22.0 或更高版本,并确认:
node -vpnpm install
pnpm run dev常用命令:
pnpm run dev
pnpm run build
pnpm run typecheck
pnpm run dev:preview
pnpm run build:preview
pnpm run start:preview
pnpm run publish:npm
pnpm run create:material
npx <packageName> setup app
npx <packageName> setup adminpnpm run dev:监听构建 SDK 产物pnpm run build:构建 SDK 到dist/pnpm run typecheck:校验类型pnpm run dev:preview:启动内置 playground,默认http://localhost:3031pnpm run create:material:交互式创建一个新的 material 脚手架并自动导出npx <packageName> setup app:在 APP 项目里自动接入 SDK 物料和 i18n Providernpx <packageName> setup admin:在 ADMIN 项目里自动注册 SDK 物料,并尝试接入 Provider / i18n
项目结构
sdk/
├─ src/
│ ├─ index.ts
│ ├─ preview.ts
│ ├─ global.css
│ ├─ components/sdk-funhub-provider.tsx
│ ├─ i18n/
│ └─ sdk/materials/
│ ├─ example/
│ │ ├─ material.ts
│ │ ├─ schema.ts
│ │ ├─ server.tsx
│ │ └─ client.tsx
│ └─ input/
│ ├─ material.ts
│ ├─ schema.ts
│ ├─ server.tsx
│ └─ client.tsx
├─ app/
├─ scripts/
├─ tsdown.config.mts
└─ package.json最小物料写法
schema.ts
import type z from 'zod';
import { defineComponentPropsSchema, getSchemaDefaultProps } from '@funhub/platform/utils';
export const bannerPropsSchema = defineComponentPropsSchema((t) => ({
title: t.string('默认标题'),
variant: t.enum(['blue', 'red'] as const, 'blue'),
autoplay: t.boolean(true),
}));
export const bannerDefaultProps = getSchemaDefaultProps(bannerPropsSchema);
export type BannerProps = z.infer<typeof bannerPropsSchema>;material.ts
import { defineMaterial } from '@funhub/platform/utils';
import { BannerClient } from './client';
import { BannerServer } from './server';
import { bannerPropsSchema } from './schema';
export const bannerMaterial = defineMaterial({
type: 'basic-banner',
name: '基础轮播图',
icon: '/icon/banner.png',
category: '行为组件',
serverComponent: BannerServer,
clientComponent: BannerClient,
propsSchema: bannerPropsSchema,
});server.tsx
import type { BannerProps } from './schema';
import { HydrationBoundary } from '@funhub/platform/hooks';
import { BannerClient } from './client';
async function getBannerDetail() {
return Promise.resolve({
description: '这是通过 HydrationBoundary 预取的文案',
});
}
export async function BannerServer(props: BannerProps) {
const prefetch: Parameters<typeof HydrationBoundary>[0]['prefetch'] = [
{
queryKey: ['banner-detail'],
queryFn: getBannerDetail,
},
];
return (
<HydrationBoundary prefetch={prefetch}>
<BannerClient {...props} />
</HydrationBoundary>
);
}client.tsx
'use client';
import type { BannerProps } from './schema';
import { useRequest } from '@funhub/platform/hooks';
export function BannerClient(props: BannerProps) {
const { title, variant, autoplay, mode = 'renderer' } = props;
const { data } = useRequest(['banner-detail'], () => {
return Promise.resolve({
description: '这是通过 HydrationBoundary 预取的文案',
});
}, {
enabled: mode === 'renderer',
});
return (
<section data-variant={variant}>
<h2>{title}</h2>
<p>{data?.description}</p>
<button type="button">
{autoplay ? '暂停轮播' : '开始轮播'}
</button>
</section>
);
}三个核心概念
propsSchema
propsSchema 只描述 inspector 可编辑字段,不等于组件完整 props。
- schema 里有的字段,组件必须支持
- schema 类型要和组件 props 类型兼容
- 组件可以有更多 props,但不一定都开放给编辑器
defineComponentPropsSchema 还会自动补上:
mode?: 'renderer' | 'editor'所以组件 props 通常直接写成:
export type BannerProps = z.infer<typeof bannerPropsSchema>;serverComponent
常见职责:
- 做 prefetch
- 做服务端数据收口
- 返回
clientComponent
在 C 端应用渲染时,通常优先走 material.serverComponent。
从 React 19 / Next.js App Router 的语义看,Server Component 默认运行在服务端环境,适合:
- 靠近数据源取数,比如数据库、内部 API、BFF
- 使用 token、密钥、服务端环境变量等不能暴露到浏览器的信息
- 减少发送到浏览器的 JavaScript 体积
- 配合流式渲染更快输出首屏内容
在物料里,serverComponent 通常负责“取数和收口”,然后把可序列化的 props 传给 clientComponent。
注意点:
- Server Component 不是拿来做浏览器交互的,不适合放
onClick、useState、useEffect - 传给 Client Component 的 props 需要是 React 可序列化数据
- 如果某个模块依赖服务端密钥或服务端专属逻辑,应放在 server 侧而不是 client 侧
clientComponent
常见职责:
- 管理端编辑器内预览
- 交互渲染
- 消费
HydrationBoundary预取结果
在编辑器场景里,通常走 material.clientComponent。
从 React 19 / Next.js 的语义看,Client Component 通过文件顶部的 'use client' 声明为客户端组件,适合:
- 处理交互事件,比如
onClick、onChange - 使用
useState、useEffect等客户端 hooks - 访问浏览器 API,比如
window、localStorage、navigator - 承担编辑器预览、动画、表单输入、弹层切换等交互逻辑
在物料里,clientComponent 更像“真正负责交互展示的那层 UI”。
注意点:
- 一旦文件声明了
'use client',它的 imports 和子组件都会进入 client bundle,所以边界要尽量收小 - 不要在 Client Component 中直接依赖服务端密钥、私有 token 或 server-only 逻辑
- 编辑器态通常建议关闭真实请求、副作用和真实跳转,只保留稳定可预览行为
为什么物料要拆成 server/client 两层
这是因为 Funhub 的物料运行场景天然同时覆盖服务端渲染和客户端交互:
- C 端应用更适合优先走
serverComponent,先完成取数、首屏输出和预取 - 管理端编辑器更适合直接走
clientComponent,保证可编辑、可交互、可即时预览
推荐心智模型:
serverComponent= 负责数据与服务端边界clientComponent= 负责交互与编辑器预览
一个常见模式就是当前模板里的 example:
server.tsx中用HydrationBoundaryprefetchclient.tsx中用useRequest(...)消费结果mode === 'editor'时关闭真实请求或副作用
这样做的好处是:
- 运行时首屏更稳
- 编辑器预览更可控
- server/client 职责更清晰
使用 server/client component 时的几个注意点
- 最小化 client 边界:不是整个物料都必须是 client component,只有需要交互的部分才加
'use client' - props 必须可序列化:server 传给 client 的数据不要放函数、类实例、不可序列化对象
- 避免环境串用:不要在 client component 里导入依赖服务端环境变量或私密逻辑的模块
- context 只能在 client 中创建:如果某些能力依赖 React context provider,应由 client component 承担 provider 边界
- 第三方库要注意运行环境:依赖浏览器能力的库,应包在 client component 里使用
builder helper 怎么用
标量字段
title: t.string('默认标题', { label: '标题' })
count: t.number(3, { label: '数量' })
visible: t.boolean(true, { label: '是否展示', fieldType: 'switch' })
period: t.timeRange({ label: '展示周期' })枚举字段
variant: t.enum(['blue', 'red'] as const, 'blue', {
label: '样式',
})列表和对象
items: t.array(
t.object({
title: t.string({ label: '标题' }),
image: t.string({ label: '图片' }),
}),
{
label: '列表项',
defaultValue: [{ title: '第一项', image: '/a.png' }],
},
)
click: t.object({
enabled: t.boolean(true, {
label: '支持点击',
fieldType: 'switch',
}),
link: t.string({ label: '跳转链接' }),
}, {
label: '点击配置',
defaultValue: {
enabled: true,
link: '',
},
})默认值规则
defaultValue 不是 zod 的 .default()。
safeParse({})不会自动补默认值- 要通过
getSchemaDefaultProps(propsSchema)显式取默认值 getSchemaDefaultProps(...)当前只提取顶层字段默认值
推荐运行时写法:
const defaultProps = getSchemaDefaultProps(material.propsSchema);
const mergedProps = {
...defaultProps,
...remoteProps,
};
const parsed = material.propsSchema.safeParse(mergedProps);新增物料流程
- 在
src/sdk/materials/<your-material>/新建目录 - 在
schema.ts里用defineComponentPropsSchema((t) => ({ ... }))定义 schema - 在
server.tsx/client.tsx里实现渲染逻辑 - 在
material.ts里调用defineMaterial(...) - 在
src/sdk/materials/index.ts里补充导出 - 执行
pnpm run dev:preview检查效果
如果想直接生成这套脚手架,可以执行:
pnpm run create:materialCLI 会依次询问:
- 物料文件名:用于目录名
src/sdk/materials/<fileName> - 组件
type:APP / ADMIN 运行时识别这个物料的唯一标识 - 组件
name:后台和预览界面展示的物料名称 - 组件
category:物料分组名称
执行完成后会自动创建 material.ts、schema.ts、server.tsx、client.tsx,并把导出追加到 src/sdk/materials/index.ts。
Playground 怎么工作
内置 playground 会:
- 自动扫描
src/sdk/**/material.ts - 用
getSchemaDefaultProps(...)生成默认 props - 用当前物料的
serverComponent进行预览渲染 - 支持
?material=<type>&locale=<locale>&mode=<renderer|editor>切换
因此只要物料目录和导出规范正确,就会自动出现在预览列表里。
对外导出
当前模板默认使用子路径导出:
import { SDKFunhubProvider } from '<packageName>/components';
import { getSDKMessages, mergeSDKMessages } from '<packageName>/i18n';
import { exampleButtonMaterial, exampleInputMaterial } from '<packageName>/materials';新增物料后,记得同步补到 src/sdk/materials/index.ts。
i18n
模板内置 next-intl,推荐业务侧直接复用 SDKFunhubProvider:
import { SDKFunhubProvider } from '<packageName>/components';
export function AppProviders({ children }: { children: React.ReactNode }) {
return <SDKFunhubProvider locale="zh-CN">{children}</SDKFunhubProvider>;
}自动接入脚本
发布后的 SDK 包默认带有 CLI,可在业务项目根目录直接执行:
npx <packageName> setup app
npx <packageName> setup admin也支持额外参数:
npx <packageName> setup app --dry-run
npx <packageName> setup admin --cwd ../your-admin-project当前脚本默认会做这些事情:
setup app- 把
'<packageName>/materials'合并进src/components/index.ts的materialMap - 把
SDKFunhubProvider和mergeSDKMessages接入到src/app/**/layout.tsx
- 把
setup admin- 把
'<packageName>/materials'合并进src/materials/index.ts - 尝试把
SDKFunhubProvider和mergeSDKMessages接入到src/app/**/layout.tsx
- 把
注意:
admin侧的 editor 映射和materialMapByCategory往往和具体后台项目结构强相关,当前脚本不会强行生成这部分 UI。- 如果目标项目文件结构和模板约定差异较大,脚本会跳过无法识别的文件,并在终端输出说明。
构建与发布
本地构建:
pnpm run build发布到 npm:
pnpm run publish:npm参考文档
- 文档总览:
https://docs-dev.guadd.fun/docs - Guide 索引:
https://docs-dev.guadd.fun/docs/guide - 物料指南:
https://docs-dev.guadd.fun/docs/guide/material - Server and Client Components 章节:
https://nextjs.org/docs/app/getting-started/server-and-client-components - use client 指令:
https://nextjs.org/docs/app/api-reference/directives/use-client - SDK 初始化:
https://docs-dev.guadd.fun/docs/guide/sdk-init - 应用自定义 Material 接入:
https://docs-dev.guadd.fun/docs/guide/custom-material-integration - 管理端自定义 Material 接入:
https://docs-dev.guadd.fun/docs/guide/admin-custom-material-integration - 项目配置:
https://docs-dev.guadd.fun/docs/guide/next-config - 项目初始化:
https://docs-dev.guadd.fun/docs/guide/project-init - next-intl:
https://next-intl.dev/docs/getting-started/app-router
