# 开发规则 - Do's and Don'ts

Doubao Apps SDK 框架的开发规则和最佳实践。

新 Page / Widget 直接导出组件函数，并在组件内使用生命周期 Hooks。

---

## ✅ 推荐做法（Do's）

### 项目管理

#### ✅ 使用 pnpm 包管理器
```bash
# 推荐
pnpm install
pnpm add @doubao-dev/framework

# 不推荐（除非项目已使用 npm）
npm install
npm install @doubao-dev/framework
```


### 组件开发

#### ✅ 在 app.config.ts 配置 App 元信息
```ts
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用'
});
```

#### ✅ 需要稳定首页时在 app.config.ts 明确入口
```ts
// ✅ pages 数组第一项会作为首页（appletEntry）
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  pages: [
    {
      entry: 'pages/home/index',
      title: '首页',
      description: '应用默认打开的页面'
    }
  ]
});
```

一级页面目录不写进 `pages` 也会自动发现；如果首页不是默认入口 `index`，或需要固定首页顺序，建议显式配置
`pages` 数组第一项。entry 必须写到 `index`，例如 `pages/home/index`。

#### ✅ 需要补充展示信息时在 app.config.ts 定义 metadata
```ts
// ✅ 好的做法
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  pages: [
    {
      entry: 'pages/user-profile/index',
      title: '用户资料',
      description: '展示和编辑用户个人资料'
    }
  ]
});
```

```ts
// ❌ 不好的做法：缺少 title、description 等信息
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  pages: [
    {
      entry: 'pages/user-profile/index'
    }
  ]
});
```

#### ✅ 多级目录必须显式声明 entry
```ts
// src/pages/account/demo/index.tsx
// src/widgets/order/detail/index.tsx
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  pages: [
    // Page 只声明入口、不补 metadata 时，可以直接写字符串
    'pages/account/demo/index',
    {
      entry: 'pages/account/settings/index',
      title: '账号设置页'
    }
  ],
  widgets: [
    {
      entry: 'widgets/order/detail/index',
      titleType: 'none'
    },
    {
      entry: 'widgets/order/summary/index',
      id: 'order-summary',
      name: '订单摘要卡片',
      titleType: 'none'
    }
  ]
});
```

Page 使用由完整 entry 派生的 URL，例如 `pages/account/demo/index` 对应 `/pages/account/demo/index`。
Widget 如果不配置 `id`，默认取入口最后一级目录名，例如 `widgets/order/detail/index` 默认为 `detail`；该 ID
必须在应用内全局唯一。

#### ✅ 分离样式文件
```tsx
// ✅ 好的做法
// index.tsx
import './index.scss';

export default function Page() {
  return <view className="container">内容</view>;
}
```

```scss
// index.scss
.container {
  padding: 16px;
  background: #fff;
}
```

```tsx
// ❌ 不好的做法
export default function Page() {
  return <view style={{ padding: '16px', background: '#fff' }}>内容</view>;
}
```

#### ✅ Page 默认使用可滚动安全区布局

Page 默认按沉浸式全屏承载，内容会从顶部开始铺满。除地图、相机、视频、沉浸式大屏等明确不滚动的场景外，
普通全页默认使用垂直 `scroll-view` 作为根滚动容器，由内容区负责顶部安全距离和页面内边距，避免被状态栏和右上角胶囊按钮遮挡。

```tsx
// ✅ 好的做法：Page 根节点提供垂直滚动，内容区负责安全区和页面内边距
import './index.scss';

export default function DemoPage() {
  return (
    <scroll-view className="demo-page" scrollOrientation="vertical" enableScroll>
      <view className="demo-page__content">
        <view className="demo-page__header">
          <text className="demo-page__title">页面标题</text>
        </view>
        <view className="demo-page__body">页面内容</view>
      </view>
    </scroll-view>
  );
}
```

```scss
.demo-page {
  height: 100vh;
  background: #f7f8fa;
}

.demo-page__content {
  min-height: 100%;
  box-sizing: border-box;
  padding: 88px 24px 32px;
}

.demo-page__header {
  // 右上角通常有宿主胶囊按钮；标题或自定义操作区不要贴到右上角。
  padding-right: 160px;
}
```

全页布局先串清三件事：谁占满视口、谁承接滚动、谁负责安全区和页面内边距。
内容超过首屏，或存在固定头部、大尺寸业务区时，先检查这条链路，再处理具体模块样式。
不要依赖内容自然撑高、普通 `view` 的 `overflow: scroll`，或让多个节点隐式争夺滚动和裁剪职责。

#### ✅ 复杂布局先定义边界和收缩语义

Page / Widget 中出现多层容器、固定尺寸模块、横向 grid / flex、卡片底部操作区或内置组件组合时，
都要先定义边界和收缩语义：哪个容器给出可用空间，哪个节点可以增长，哪个节点可以收缩、换行或独占一行，哪个节点不能越界。
元素被挤出、重叠或裁剪时，不要只给最里层补样式；通常要沿父子链路同时检查容器边界、盒模型、间距、固定尺寸和收缩能力。

- `button`、`switch`、`slider`、`input` 等内置组件通常自带最小尺寸、内边距和交互态样式；放入横向布局时，要按有固有尺寸的内容处理。
- 固定尺寸的业务模块要和外层可用空间一起设计，避免尺寸、边距和边框叠加后把父容器撑出。
- 多列、多按钮或图文混排时，优先明确容器和子项的收缩、换行或独占一行策略，再处理视觉细节。

#### ✅ 使用 TypeScript 类型定义
```tsx
// ✅ 好的做法
interface UserInfo {
  name: string;
  age: number;
  avatar?: string;
}

export default function UserWidget() {
  const viewData = useViewData<UserInfo>();
  return <view>{viewData.name}</view>;
}
```

```ts
// ✅ 把 metadata 写到 src/app.config.ts
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  widgets: [
    {
      entry: 'widgets/user-card/index',
      id: 'user-card',
      name: '用户卡片',
      description: '用户信息卡片',
      titleType: 'none'
    }
  ]
});
```

```tsx
// ❌ 不好的做法
export default function UserWidget() {
  // 错误：类型和实际任务字段不匹配
  const viewData = useViewData<{ title: string }>();
  return <view>{viewData.name}</view>;
}
```

#### ✅ 使用生命周期钩子
```tsx
// ✅ 好的做法
import { useDestroy, useHide, useShow } from '@doubao-dev/framework';

export default function Page() {
  useShow(() => console.log('Page shown'));
  useHide(() => console.log('Page hidden'));
  useDestroy(() => console.log('Page destroyed'));

  return <view>内容</view>;
}
```


#### ✅ 错误处理和边界情况
```tsx
// ✅ 好的做法
export default function ItemsWidget() {
  const viewData = useViewData<{ items: string[] }>();
  if (!viewData.items || viewData.items.length === 0) {
    return <view>暂无数据</view>;
  }

  return (
    <view>
      {viewData.items.map((item, index) => (
        <view key={index}>{item}</view>
      ))}
    </view>
  );
}
```

```tsx
// ❌ 不好的做法
export default function ItemsWidget() {
  const viewData = useViewData<{ items?: string[] }>();
  // 没有检查 viewData.items 是否存在
  return <view>{viewData.items.map(item => <view>{item}</view>)}</view>;
}
```



## ❌ 禁止做法（Don'ts）

### 样式相关

#### ❌ 不在 TSX 中写 style 对象

样式统一写到 `index.scss`，TSX 中通过 `className` 切换状态。不要把 Web 写法里的内联 style 对象迁移过来。

```tsx
// ❌ 错误：内联 style 对象
<view style={{ width: 100, height: 56, fontSize: 14 }} />

// ✅ 正确：使用 className
<view className="profile-card" />
```

```scss
.profile-card {
  width: 100px;
  height: 56px;
  font-size: 14px;
}
```

#### ❌ 不写内联样式
```tsx
// ❌ 错误
export default function Page() {
  return <view style={{ padding: 16, background: '#fff' }}>内容</view>;
}
```

```tsx
// ✅ 正确
import './index.scss';

export default function Page() {
  return <view className="container">内容</view>;
}
```

### 异步操作

#### ❌ 不在 render 中发起请求
```tsx
// ❌ 错误 - 每次渲染都会发请求
import { request } from '@doubao-dev/framework/api';

export default function Page() {
  void request({ url: 'https://api.example.com/data', method: 'GET' });
  return <view>内容</view>;
}
```

```tsx
// ✅ 推荐 - 使用 Hook 管理请求和状态
import { useEffect, useState } from '@doubao-dev/framework';
import { request } from '@doubao-dev/framework/api';

export default function UserPage() {
  const [userData, setUserData] = useState<{ name?: string } | null>(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const loadUser = async () => {
      try {
        setError('');
        const response = await request({ url: 'https://api.example.com/data', method: 'GET' });
        setUserData(response.data as { name?: string });
      } catch (caughtError) {
        setError(caughtError instanceof Error ? caughtError.message : '请求失败');
      }
    };

    void loadUser();
  }, []);

  if (error) {
    return <view>{error}</view>;
  }

  return <view>{userData?.name}</view>;
}
```

### 组件结构

#### ❌ 不过度嵌套组件
```tsx
// ❌ 错误 - 过度嵌套
export default function Page() {
  return (
    <view>
      <view>
        <view>
          <view>
            <view>
              <view>内容</view>
            </view>
          </view>
        </view>
      </view>
    </view>
  );
}
```

```tsx
// ✅ 正确 - 扁平结构
export default function Page() {
  return (
    <view className="page">
      <view className="header">头部</view>
      <view className="content">内容</view>
      <view className="footer">底部</view>
    </view>
  );
}
```

### 类型使用

#### ❌ 不忽略 TypeScript 类型
```tsx
// ❌ 错误
export default function UserWidget() {
  const viewData = useViewData<{ name: string }>();
  // @ts-ignore
  return <view>{viewData.unknownProperty}</view>;
}
```

```tsx
// ✅ 正确
interface UserInfo {
  name: string;
  age: number;
}

export default function UserWidget() {
  const viewData = useViewData<UserInfo>();
  return <view>{viewData.name}</view>;
}
```

### 状态管理

#### ❌ 不在全局作用域定义状态
```tsx
// ❌ 错误 - 全局变量会在多个实例间共享
let count = 0;

export default function CounterWidget() {
  count++; // 所有实例共享这个 count
  return <view>Count: {count}</view>;
}
```

```tsx
// ✅ 推荐 - 使用组件内状态，实例之间互不影响
import { useState } from '@doubao-dev/framework';

export default function CounterWidget() {
  const [count, setCount] = useState(0);

  return (
    <view
      onClick={() => {
        setCount(prev => prev + 1);
      }}
    >
      Count: {count}
    </view>
  );
}
```

## 📚 相关文档

- [组件开发指南](../guides/component-development.md)
- [最佳实践](../guides/best-practices.md)
- [性能优化](../guides/performance-optimization.md)
