# Framework 核心入口、生命周期和 Hooks

本文件只覆盖 `@doubao-dev/framework` 和 `@doubao-dev/framework/config` 的开发范式：
App / Page / Widget 入口、生命周期、Hooks、viewData 和 `app.config.ts` 配置。

豆包智能服务的端能力 API（也称 Open API，例如网络、Storage、位置、路由、Toast、模型上下文等）从
`@doubao-dev/framework/api` 导入，不在本文件展开。只要代码要从该路径导入 API，必须先读
[豆包智能服务的端能力 API 速查](../doubao-agentic-service-api/quick-reference.md) 和 [分组目录](../doubao-agentic-service-api/groups.md)。

---

## 导入边界

| 能力 | 导入路径 | 本文件是否展开 |
|-----|----------|----------------|
| 入口定义、Hooks、viewData | `@doubao-dev/framework` | 是 |
| App 配置、入口 metadata | `@doubao-dev/framework/config` | 是 |
| 网络、Storage、位置、路由、Toast、模型上下文等端能力 | `@doubao-dev/framework/api` | 否，见 `../doubao-agentic-service-api/` |

不要从 `react` 直接导入 Hooks；在豆包智能服务代码里使用 `@doubao-dev/framework` 导出的 Hooks。

---

## 入口定义

| 入口 | 文件位置 | 用途 |
|-----|----------|------|
| `defineApp()` | `src/app.ts` | 定义应用入口和 App 生命周期 |
| 默认导出组件函数 | `src/pages/<page-name>/index.tsx` | 推荐的 Page（全屏页面）入口 |
| 默认导出组件函数 | `src/widgets/<widget-name>/index.tsx` | 推荐的 Widget（聊天卡片）入口 |
| 默认导出组件函数 | `src/auth/login-page/index.tsx` | 推荐的自定义登录页入口；没有 `src/auth` 入口时旧路径 `src/mcp-ui/mcp-login-page/index.tsx` 继续兼容 |

Page / Widget 的名称、标题、描述、`boxType` 等 metadata 写在 `src/app.config.ts`，不要写到
源码入口。Page / Widget 直接默认导出组件函数，并在组件内使用生命周期 Hooks。

`src/auth/login-page/index.tsx` 与兼容的 `mcp-login-page` 约定路径会由构建工具自动注入登录页固定 metadata。
已有 `defineLoginPage()` 对象入口无需迁移；新登录页不要使用其他目录名。

### Page 示例

```tsx
import { useMounted, useState, useViewData } from '@doubao-dev/framework';
import './index.scss';

interface DetailPageData {
  id: string;
  title?: string;
}

export default function DetailPage() {
  const viewData = useViewData<DetailPageData>();
  const [selected, setSelected] = useState(false);
  useMounted(() => console.log('detail page mounted'));

  return (
    <view className="detail-page">
      <text className="title">{viewData.title || '详情'}</text>
      <text className="id">{viewData.id}</text>
      <button text={selected ? '已选择' : '选择'} onClick={() => setSelected(true)} />
    </view>
  );
}
```

### Widget 示例

Widget 卡片展示层优先使用 [Widget 模板库](../widget-templates/overview.md)。本文件只展示 Framework 入口形态。

```tsx
import { useViewData } from '@doubao-dev/framework';
import { InfoListTemplate, type InfoListTemplateProps } from '@doubao-dev/template';

interface ProductWidgetData {
  title: string;
  items: InfoListTemplateProps['items'];
}

export default function ProductWidget() {
  const viewData = useViewData<ProductWidgetData>();

  return <InfoListTemplate title={viewData.title} items={viewData.items} />;
}
```

---

## App 生命周期

`defineApp()` 放在 `src/app.ts`，只负责应用级生命周期。App metadata 写在 `src/app.config.ts`。

| 钩子 | 触发时机 | 常见用途 |
|------|----------|----------|
| `onLaunch()` | 应用启动 | 初始化全局状态、注册全局监听 |
| `onPageOpened({ viewId })` | Page 打开 | 记录页面打开事件、调试 viewId |
| `onForeground()` | 应用进入前台 | 恢复全局订阅、刷新全局状态 |
| `onBackground()` | 应用进入后台 | 暂停任务、保存状态 |
| `onDestroy()` | 应用销毁 | 清理全局资源 |
| `onError(error)` | 生命周期或工具调用报错 | 兜底上报、错误隔离 |

```tsx
import { defineApp } from '@doubao-dev/framework';

export default defineApp({
  onLaunch() {
    console.log('app launched');
  },
  onPageOpened({ viewId }) {
    console.log('page opened', viewId);
  },
  onForeground() {
    console.log('app foreground');
  },
  onBackground() {
    console.log('app background');
  },
  onDestroy() {
    console.log('app destroyed');
  },
  onError(error) {
    console.error(error);
  }
});
```

---

## Page / Widget 生命周期

函数入口使用生命周期 Hooks；对象入口继续支持对应的 `onXxx` 字段：

| Hook / 对象字段 | 触发时机 | 常见用途 |
|------|----------|----------|
| `useCreated()` / `onCreated()` | View 创建时 | 初始化非渲染数据结构 |
| `useMounted()` / `onMounted()` | 首次渲染后，只触发一次 | 首次加载、订阅、启动一次性任务 |
| `useShow()` / `onShow()` | 首次显示或从隐藏恢复 | 刷新可见数据、恢复轮询 |
| `useHide()` / `onHide()` | View 隐藏 | 暂停任务、保存草稿 |
| `useDestroy()` / `onDestroy()` | View 销毁 | 清理订阅、计时器和外部资源 |
| `useError()` / `onError()` | View 生命周期报错 | 错误兜底和上报 |

Widget 额外支持：

| 钩子 | 触发时机 | 常见用途 |
|------|----------|----------|
| `useForeground()` / `onForeground()` | 应用回到前台或设备解锁 | 恢复卡片可见内容 |
| `useBackground()` / `onBackground()` | 应用进入后台或设备锁屏 | 暂停卡片任务 |

异步端能力不要直接写在组件函数或对象入口的 `render()` 中；首次加载放在 `useEffect` / `useMounted()`，用户动作放在事件处理函数。

---

## Hooks

常用 Hooks 从 `@doubao-dev/framework` 导入：

```tsx
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useReducer,
  useState
} from '@doubao-dev/framework';
```

| Hook | 用途 |
|------|------|
| `useState(initial)` | 组件局部状态 |
| `useEffect(effect, deps)` | 副作用、订阅和清理 |
| `useReducer(reducer, initial)` | 复杂状态流转 |
| `useMemo(factory, deps)` | 缓存计算结果 |
| `useCallback(callback, deps)` | 缓存事件处理函数 |
| `createContext(defaultValue)` / `useContext(context)` | 跨组件共享状态 |

Framework 还提供视图生命周期 Hooks：

| Hook | 对应能力 |
|------|----------|
| `useCreated(callback)` | View created |
| `useMounted(callback)` | View mounted |
| `useShow(callback)` / `useHide(callback)` | View show / hide |
| `useDestroy(callback)` | View destroy |
| `useError(callback)` | View error |
| `useForeground(callback)` / `useBackground(callback)` | 应用前后台变化 |
| `useViewData<T>()` | 读取并订阅 viewData 更新 |

---

## View Data

| API | 用途 |
|-----|------|
| `useViewData<T>()` | 读取 Page / Widget 输入数据并响应后续更新，必须传泛型 |
| `getWidgetInstanceId()` | 在 Widget 中获取当前实例 ID，Page 中通常为空 |
| `getCurrentPages()` | 获取当前页面栈，返回 `CurrentPage[]`，当前页面信息包含 `route` |

```tsx
import {
  getCurrentPages,
  getWidgetInstanceId,
  useViewData
} from '@doubao-dev/framework';

interface CardData {
  title: string;
  count?: number;
}

export default function Card() {
  const viewData = useViewData<CardData>();
  const widgetInstanceId = getWidgetInstanceId();
  const pages = getCurrentPages();

  return (
    <view>
      <text>{viewData.title}</text>
      <text>{pages.at(-1)?.route || ''}</text>
      <text>{widgetInstanceId || ''}</text>
    </view>
  );
}
```

在 Page / Widget 组件内使用 `useViewData<T>()` 读取输入数据；它会在 viewData 更新时触发重新渲染。

---

## App Config

`src/app.config.ts` 使用 `defineAppConfig()` 配置 App 元信息和入口 metadata。

```ts
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  description: '应用描述',
  keywords: ['doubao', 'demo'],
  pages: [
    'pages/home/index',
    {
      entry: 'pages/account/demo/index',
      title: '账号示例页'
    }
  ],
  widgets: [
    {
      entry: 'widgets/product-card/index',
      name: '商品卡片',
      description: '展示商品摘要',
      boxType: 'inbox',
      border: true,
      titleType: 'none'
    }
  ]
});
```

配置规则：

- `appId` 使用开放平台 `db_xxxxxx` 风格，`name` 必填。
- `pages` / `widgets` 可不写；不写时会按一级目录自动发现入口。
- 需要稳定首页顺序、补标题或补 Widget metadata 时，使用数组配置。
- 字符串数组项只声明入口；对象数组项用 `entry` 声明入口并补 metadata。
- `entry` 写完整到 `index`，例如 `pages/home/index`、`widgets/product-card/index`。
- Page 对象常用 `title`；Widget 对象常用 `name`、`description`、`boxType`、`border`、`titleType`。
- Widget 默认无标题时显式配置 `titleType: 'none'`。

更完整的入口、目录和 metadata 规则见 [开发规则](../rules/dos-and-donts.md)。

---

## 相关文档

- [组件开发完整指南](../guides/component-development.md)
- [开发规则](../rules/dos-and-donts.md)
- [Page / Widget 基础示例](../examples/page-widget-basics.md)
- [Widget 模板库](../widget-templates/overview.md)
- [豆包智能服务的端能力 API 速查](../doubao-agentic-service-api/quick-reference.md)
