用组件的方式思考
构建现代界面,关注的不再只是页面,而是系统。一旦开始用组件的方式思考,一切都会不同。
我还记得第一次理解这一点的时刻。当时,我正盯着一个复杂的仪表盘,试图弄清楚为什么一次小修改会破坏三个不同的区域。代码纠缠在一起:样式四处影响,逻辑重复,修改任何东西都得在几十个文件里翻找。
后来,我用组件重新构建了它。混乱一下子有了结构。
组件究竟是什么
组件是一块相对独立的界面,管理自己的逻辑、样式和状态。它就像一块乐高积木:单独看很简单,与其他积木组合起来,却能形成强大的整体。
function Button({ children, onClick, variant = 'primary' }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{children}
</button>
);
}
这个按钮不关心自己出现在哪里。它可以放在弹窗、表单或者导航栏中。这就是封装的力量。
思维方式的转变
组件思维需要一套不同的思考方式:
- 拆解界面。 每个界面都是由更小部分组成的一棵树。
- 识别模式。 如果同样的东西正在构建第二次,它很可能应该成为一个组件。
- 划清边界。 每个组件都应该只承担一种职责。
- 为复用而设计。 让组件能够在不同场景中工作。
组合优于配置
好的组件可以相互组合。与其把每个选项都塞进属性,不如让组件协同工作:
// 与其创建一个拥有 20 个属性的巨大 Card 组件……
<Card
title="Hello"
subtitle="World"
image="/photo.jpg"
footer="Read more"
variant="outlined"
/>
// ……不如组合更小、职责明确的组件
<Card>
<CardImage src="/photo.jpg" />
<CardContent>
<CardTitle>Hello</CardTitle>
<CardSubtitle>World</CardSubtitle>
</CardContent>
<CardFooter>Read more</CardFooter>
</Card>
第二种方式更灵活。需要一张没有图片的卡片?不放入 CardImage 就好。想自定义底部区域?把 CardFooter 换成你需要的内容即可。
状态应该放在需要它的地方
组件思维中最难的一部分,是决定状态应该归谁管理。这里有一个简单的规则:
- 局部状态: 只有当前组件需要,例如下拉菜单是否展开。
- 提升的状态: 多个同级组件需要,把它提升到共同的父组件。
- 全局状态: 许多没有直接关系的组件都需要,使用上下文或状态仓库。
// 局部状态:只有下拉菜单关心它
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
// ...
}
// 提升的状态:表单需要知道所有输入框的值
function Form() {
const [values, setValues] = useState({});
return (
<>
<Input value={values.name} onChange={...} />
<Input value={values.email} onChange={...} />
</>
);
}
组件本身也是文档
设计良好的组件能够说明自身。阅读组件树时,你应该能直接理解界面结构:
<Page>
<Header>
<Logo />
<Navigation />
<UserMenu />
</Header>
<Main>
<Sidebar />
<Content />
</Main>
<Footer />
</Page>
无需注释,结构本身已经说明了一切。
调试的优势
在基于组件的系统中,出了问题,你知道应该去哪里查找。问题通常在以下位置:
- 组件内部:检查它自己的逻辑
- 传入的属性:检查父组件
- 状态管理:检查状态存放的位置
再想想调试一个两千行、所有东西都彼此牵连的文件是什么感受。组件带来隔离,隔离让问题更清晰。
从小处开始
你不需要一夜之间重构整个代码库。先从一个组件开始,把它提取出来,定义清晰的属性,看看使用起来如何。
然后再做一个,再做一个。
很快,你看到的就不再只是页面,而是一套由可复用、可组合的部分构成的系统。构建界面的乐趣,也会在这时显现出来。
模板示例