⛵ 我用 React + TS 做了个"调色盘",顺便学会了企业级项目的目录架构

写在前面:今天学的东西特别"工程化"。老师说,一个企业级 React + TS 项目,目录不是随便建的------model(数据模型)、api(接口封装)、components(组件) 三个目录各司其职。我们做了一个"调色盘"(Color Picker):左边一个彩色方块实时显示颜色,右边三个滑杆分别控制红、绿、蓝。拖动滑杆,方块颜色跟着变------数据驱动的感觉,真的爽。做完调色盘还顺便搞了个"成员表格",用 useEffect 模拟了异步请求 GitHub 用户数据。


一、企业级项目的目录架构

1.1 为什么要分目录?

老师说:

"React + TS 适合大型项目开发,代码量大、成员多。目录结构要清晰,团队才能好协作。"

这个项目的目录:

bash 复制代码
src/
├── App.tsx                      # 根组件
├── model/                       # ① 数据模型目录
│   ├── color.ts                 #    Color 接口
│   └── member.ts                #    MemberEntity 接口
├── api/                         # ② 接口封装目录
│   └── memberApi.ts             #    成员列表 API
└── components/                  # ③ 组件目录
    ├── ColorBrowser.tsx         #    颜色展示组件
    ├── ColorPicker.tsx          #    颜色选择组件
    └── MemberTable.tsx          #    成员表格组件

1.2 三个目录各管什么?

老师说:

"model------前端数据模型。api------前端需要的接口都在这里模块化定义,统一接口声明,好管理,前后端接口方法(Application Interface)。"

目录 作用 比喻
model 定义数据接口(TypeScript interface) 公司的"岗位说明书"
api 封装后端接口调用 公司的"对外窗口"
components UI 组件 公司的"生产线"

model 和 api 分离开,是前后端分离的关键 ------前端团队改接口,只需要改 api 目录,组件层完全不用动。


二、model 目录:数据模型

2.1 Color 接口

typescript 复制代码
// model/color.ts
export interface Color {
    red: number;
    green: number;
    blue: number;
}

老师说:

"数据接口,好多地方使用到。model 项目架构目录之一。"

Color 接口定义了一个颜色由红、绿、蓝三个数字组成,每个 0-255。

为什么需要接口? 因为 ColorBrowserColorPicker 两个组件都用到了 Color------如果不用接口约束,一个用 red,一个用 r,就乱了。

2.2 MemberEntity 接口

typescript 复制代码
// model/member.ts
export interface MemberEntity {
    id: number;
    login: string;
    avatar_url: string;
}

这个接口对应 GitHub API 返回的用户数据------id、用户名、头像地址。


三、api 目录:接口封装

typescript 复制代码
// api/memberApi.ts
import { type MemberEntity } from '../model/member';

export const getMembersCollection = (): Promise<MemberEntity[]> => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([
                { id: 1457912, login: "brauliodiez", avatar_url: "..." },
                { id: 4374977, login: "Nasdan", avatar_url: "..." }
            ]);
        }, 500);
    });
};

getMembersCollection 模拟了一个异步接口------500ms 后返回 GitHub 用户列表。

返回类型是 Promise<MemberEntity[]>------TypeScript 保证了返回的数据结构一定符合 MemberEntity 接口。


四、App 根组件:状态管理的"总指挥"

tsx 复制代码
import { useState } from 'react';
import ColorBrowser from './components/ColorBrowser';
import { type Color } from './model/color';
import ColorPicker from './components/ColorPicker';
import MemberTable from './components/MemberTable';

function App() {
    const [color, setColor] = useState<Color>({
        red: 120,
        green: 100,
        blue: 120
    });

    return (
        <>
            <ColorBrowser color={color} />
            <ColorPicker color={color} onColorUpdated={setColor} />
            <MemberTable />
        </>
    );
}

App 是"总指挥",持有唯一的颜色状态 color

css 复制代码
App(持有 color 状态)
├── ColorBrowser(展示颜色------Props 传入 color)
└── ColorPicker(修改颜色------Props 传入 color + onColorUpdated 事件)

数据流非常清晰:

  • color 状态在 App。
  • ColorBrowser 只负责展示(接收 color Props)。
  • ColorPicker 负责修改 (调用 onColorUpdated 上报新颜色)。

五、ColorBrowser:只展示,不修改

tsx 复制代码
import * as React from 'react';
import { type Color } from '../model/color';

interface Props {
    color: Color;
}

const ColorBrowser: React.FC<Props> = (props) => {
    const divStyle: React.CSSProperties = {
        "width": "11rem",
        "height": "7rem",
        backgroundColor: `rgb(${props.color.red}, ${props.color.green}, ${props.color.blue})`
    };
    return <div style={divStyle} />;
};

export default ColorBrowser;

ColorBrowser 是一个"纯展示"组件:

  • 接收 color Props。
  • 把 RGB 拼成 CSS 的 rgb(120, 100, 120) 字符串。
  • 渲染成一个彩色方块。

React.CSSProperties 是 React 内置的行内样式类型------保证 style 对象的属性名和 CSS 一致。


六、ColorPicker:滑杆控制颜色

tsx 复制代码
import * as React from 'react';
import { type Color } from '../model/color';

interface Props {
    color: Color;
    onColorUpdated: (color: Color) => void;
}

const ColorPicker: React.FC<Props> = (props) => {
    return (
        <div>
            <input
                type="range"
                min={0}
                max={255}
                value={props.color.red}
                onChange={event => props.onColorUpdated({
                    ...props.color,          // 展开原颜色
                    red: +event.target.value // 只改 red
                })}
            />
            {props.color.red}
            {/* green 和 blue 的滑杆同理 */}
        </div>
    );
};

export default ColorPicker;

三个滑杆控制 RGB,每个滑杆的逻辑都一样:

csharp 复制代码
用户拖动滑杆
  ↓ onChange 事件
props.onColorUpdated({
    ...props.color,          // 保留 red、green、blue 其他两个不变
    red: +event.target.value // 只更新 red
})
  ↓ App 的 setColor 更新状态
新的 color 对象
  ↓ Props 向下
ColorBrowser 重新渲染,方块颜色变化

注意 +event.target.value+------滑杆的值是字符串,+ 把它转成数字 ,因为 Color 接口要求 red: number

...props.color(展开运算符)是实现"不可变更新"的关键------不修改原对象,而是创建一个新对象,只改变其中一个属性。


七、MemberTable:useEffect 异步加载数据

tsx 复制代码
import * as React from 'react';
import { type MemberEntity } from '../model/member';
import { getMembersCollection } from '../api/memberApi';

const MemberRow = (props) => {
    const { member } = props;
    return (
        <tr>
            <td><img src={member.avatar_url} style={{ maxWidth: '10rem' }} /></td>
            <td><span>{member.id}</span></td>
            <td><span>{member.login}</span></td>
        </tr>
    );
};

const MemberTable = () => {
    const [memberCollection, setMemberCollection] = React.useState<MemberEntity[]>([]);

    React.useEffect(() => {
        // 挂载后请求接口,不会影响组件的渲染
        (async () => {
            const members = await getMembersCollection();
            setMemberCollection(members);
        })();
    }, []);

    return (
        <table>
            <thead>
                <tr><th>Avatar</th><th>Id</th><th>Name</th></tr>
            </thead>
            <tbody>
                {memberCollection.map((member: MemberEntity) => (
                    <MemberRow key={member.id} member={member} />
                ))}
            </tbody>
        </table>
    );
};

组件挂载时的数据加载流程:

scss 复制代码
组件挂载
  ↓ useEffect(只在挂载时执行一次,[] 依赖)
发起异步请求 getMembersCollection()
  ↓ 500ms 后
返回成员数据
  ↓ setMemberCollection
响应式更新 → 表格渲染出成员

这里有个关键设计:useEffect 里用立即执行函数(IIFE)包裹异步请求 ------useEffect 本身不直接支持 async 函数,所以需要 (async () => { ... })() 这种写法。

组件的渲染分两步:

  1. 先渲染空表格(memberCollection 初始为空数组)。
  2. 数据请求回来后再更新------用户不会看到"白屏"。

八、总结:React + TS 企业级项目的"三件套"

目录 职责 本项目的例子
model 定义数据接口 ColorMemberEntity
api 封装后端接口 getMembersCollection()
components UI 组件 ColorBrowserColorPickerMemberTable
核心概念 本项目怎么体现的
单一数据源 color 状态只在 App,没有第二份
单向数据流 状态向下传 Props,事件向上传
纯展示组件 ColorBrowser 只接收 Props 渲染
不可变更新 ...props.color 展开创建新对象
useEffect 异步 挂载后请求接口,先渲染再更新
泛型约束 useState<Color>React.FC<Props>

企业级 React 项目 = 清晰的目录结构(model/api/components)+ 严格的类型约束 + 清晰的单向数据流。


写在最后

今天最大的收获,是理解了企业级项目的目录架构。以前我写 React,所有代码堆在 App 里,一个文件几百行。现在知道了------数据模型放 model,接口封装放 api,UI 放 components,各司其职。而且 TypeScript 的接口约束让"跨组件传数据"变得安全可靠,不用担心传错字段。

下次面试官问你:"企业级 React + TS 项目怎么组织目录?"

你可以淡定地说:

"企业级 React + TS 项目通常分三层目录:model (数据模型)------用 TypeScript interface 定义数据结构,比如 Color { red, green, blue }api (接口封装)------把后端接口统一封装成模块,比如 getMembersCollection(),返回 Promise<MemberEntity[]>components (组件)------UI 组件,通过 Props 接收数据和事件。状态管理遵循单一数据源和单向数据流:父组件持有状态,通过 Props 传给子组件,子组件通过自定义事件上报修改。展示型组件用 React.FC<Props> 定义类型,用 useEffect 在挂载后异步加载数据,实现'先渲染后更新'的体验。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

相关推荐
DevUI团队2 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
weixin_431600442 小时前
NestJS 入门(3):Guard 如何挡住未登录请求?
前端·后端·学习·nest.js
avi91113 小时前
[AI教做人]AI平台做2项目;一个3D模型展示,另一个框架多人
javascript·人工智能·ai·3d模型·3d引擎·顶点和法线
kyriewen3 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
IT_陈寒3 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端
用户938515635073 小时前
Type vs Interface:读完这篇就没有面试官能难倒你了
前端·面试·typescript
用户938515635074 小时前
手写一个 LLM Harness 框架:用工程化手段把大模型幻觉踩在脚下
javascript·人工智能·后端
油丶酸萝卜别吃4 小时前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang4 小时前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试
柒和远方5 小时前
V063: TS 面试必考:interface 与 type 的四大差异,与 LLM Harness 的自动化择优
前端·javascript