写在前面:今天学的东西特别"工程化"。老师说,一个企业级 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。
为什么需要接口? 因为 ColorBrowser 和 ColorPicker 两个组件都用到了 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 是一个"纯展示"组件:
- 接收
colorProps。 - 把 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 () => { ... })() 这种写法。
组件的渲染分两步:
- 先渲染空表格(
memberCollection初始为空数组)。 - 数据请求回来后再更新------用户不会看到"白屏"。
八、总结:React + TS 企业级项目的"三件套"
| 目录 | 职责 | 本项目的例子 |
|---|---|---|
| model | 定义数据接口 | Color、MemberEntity |
| api | 封装后端接口 | getMembersCollection() |
| components | UI 组件 | ColorBrowser、ColorPicker、MemberTable |
| 核心概念 | 本项目怎么体现的 |
|---|---|
| 单一数据源 | 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 在挂载后异步加载数据,实现'先渲染后更新'的体验。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。