创建组件 API:flexbox 布局
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
你写了多少次display: flex?这种情况太常见了,以至于有些人试图将其应用于display: flex页面上的几乎所有元素。
在这篇文章中,我们将探讨最常用组件的 API 设计背后的思路。
我一直想写这篇文章,因为我看到同一个flexbox组件有多种不同的实现方式,每种方式都有自己的 API。我认为我们应该停止这种各自为政的做法,而是应该将其标准化。
开始
本文将使用 React 和Stitches(我仍然很喜欢它们)。但本文的主要目的是论证 API 设计决策的合理性,这些决策可以应用于 Vue、Svelte、Lit 或任何其他前端工具。
让我们从简单的开始:
import { styled } from '@stitches/react'
export const Flex = styled('div', {
display: 'flex',
})
为了简单起见,我styled直接使用了预配置的库stitches,但我鼓励在你的库中使用主题令牌来保持一致的布局属性、颜色、字体大小等。
包装
让我们从简单的开始,逐步添加flex-wrap控制功能:
import { styled } from '@stitches/react'
export const Flex = styled('div', {
display: 'flex',
variants: {
wrap: {
'wrap': {
flexWrap: 'wrap',
},
'no-wrap': {
flexWrap: 'nowrap',
},
'wrap-reverse': {
flexWrap: 'wrap-reverse',
},
},
},
})
我正在使用stitches一些变体Flex,它们会为组件生成漂亮的 TypeScript 属性。
这是最简单的 API 设计决策,我们移除 `<props>` 只是flex为了避免重复,因为所有属性Flex都已存在于元素上下文中。请注意,浏览器默认值为 `<props>` nowrap,因此使用 ` <props>` 可能很常见。虽然感觉可能有点奇怪,但与自定义 API 相比,<Flex wrap="wrap">它仍然更容易学习和使用(例如 `<props>` )。flex-wrap: wrap
流向
接下来我们来看第二个属性:flex-direction。
我在一些设计系统中见过direction它,但对某些人(包括我)来说,它甚至比写 `<style>` 更糟糕cosnt,尤其因为它是一个常用属性。
其他设计系统会整合`<style>`Row和Column`<style>` 组件——它们为用户提供了很好的上下文信息:
// Flex defined as before
export const Row = styled(Flex, {
flexDirection: 'row',
})
export const Column = styled(Flex, {
flexDirection: 'column'
})
不过现在我们也需要处理使用 `.` 的情况flex-direction: row-reverse; // or column-reverse。因此,我们可以添加reverse一个布尔属性(因为它不是一个常用的属性):
// Flex defined as before
export const Row = styled(Flex, {
flexDirection: 'row',
variants: {
reverse: {
true: {
flexDirection: 'row-reverse'
}
}
}
})
export const Column = styled(Flex, {
flexDirection: 'column',
variants: {
reverse: {
true: { // neat way to create boolean variants in stitches
flexDirection: 'column-reverse'
}
}
}
})
或者我们直接在组件中定义流向Flex:
export const Flex = styled('div', {
display: 'flex',
variants: {
wrap: {}, // defined as before
flow: {
'row': {
flexDirection: 'row',
},
'column': {
flexDirection: 'column',
},
'row-reverse': {
flexDirection: 'row-reverse',
},
'column-reverse': {
flexDirection: 'column-reverse',
},
},
},
})
如您所知,是和的flex-flow简写,所以我们不是重新创建 API,而是采用它。flex-directionflex-wrap
目前为止的用法是(覆盖浏览器默认设置):
<Flex flow="row-reverse" wrap="wrap" />
<Flex flow="column" />
// or with dedicated components
<Row reverse wrap="wrap" />
<Column />
哪个 API 更合你心意,它们都很好用。我个人更倾向于只用Flex其中一个,或者三个都用。Flex它本身易于维护,而且通过查看属性就能立即提供足够的上下文信息flow,尤其是在需要根据屏幕尺寸进行更改时,可以使用响应样式。
<Flex flow={{ '@tablet': 'row', '@mobile': 'column' }} />
想象一下用专用组件来实现这Row一点Column。
结盟
既然进展顺利,我们接下来进入最有趣的部分:对齐方式。
虽然官方 API 建议使用 ` align` 和 `align-items`,justify-content但align-items我总觉得这两个词在编写 CSS 时不太容易理解。也许是因为我的英语不是母语,又或许是因为在考虑弹性盒子布局时,它们本身就不太适用。
帮助我理解这些属性的一篇最棒的文章是《Flexbox 完全指南》(我们大多数人现在仍然会参考它)。它有非常棒的可视化图表,展示了这些属性如何通过改变主轴和交叉轴来影响元素的位置。不过,真正帮助我的是flutter`<div>`Flex组件。它有两个很棒的属性:`mainAxisAlignment`和`crossAxisAlignment`,用法如下:
Flex(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
)
这个 API 最棒的地方在于它非常容易在脑海中可视化。如果你有一个 `<a>` 标签row,它的主轴是水平的;如果你有一个 `<b>` 标签column,它的主轴是垂直的。所以,无论方向如何,你都可以想象你的元素在主轴上均匀分布,并在交叉轴上与容器的末端对齐。
了解了这一点,我们就可以将新的 API 集成到我们自己的组件中:
export const Flex = styled('div', {
display: 'flex',
variants: {
wrap: {},
flow: {},
main: {
'start': {
justifyContent: 'flex-start',
},
'center': {
justifyContent: 'center',
},
'end': {
justifyContent: 'flex-end',
},
'stretch': {
justifyContent: 'stretch',
},
'space-between': {
justifyContent: 'space-between',
},
},
cross: {
start: {
alignItems: 'flex-start',
},
center: {
alignItems: 'center',
},
end: {
alignItems: 'flex-end',
},
stretch: {
alignItems: 'stretch',
},
},
},
})
flutter与TypeScript 的API相比Flex,我分别将 `property` 和 `property` 缩短mainAxisAlignment为`property`main和crossAxisAlignment`property` cross。虽然 TypeScript 提供了出色的自动补全体验,但在组合多个Flex组件时看到这些冗长的属性名实在令人眼花缭乱。由于这两个属性都存在于组件的上下文中Flex,我认为理解它们就足够了。
现在,它的用法是:
<Flex flow="column" main="space-between" cross="center" />
这个组件的设计思路相当简单(或者说很容易理解):它是一个列,因此项目将均匀分布在主轴上(y),并且在各个轴上x居中。
顺便说一句,新的 Chrome 开发者工具的 flexbox 可视化调试功能太棒了。
间距
现在,我们需要添加的最后一个属性是控制子元素间距的属性。通常有两种方法:一种是使用嵌套的div元素,这种方法不会产生副作用,而是将所有子元素包裹在一个带有负边距的盒子中,从而实现正确的换行效果,同时又不改变子节点的样式;另一种是使用flex-gap-polyfill,这种方法通过选择器来改变子元素的样式> *。值得庆幸的是,我们今天不需要讨论这些,因为Safari 14.1是主流浏览器中最后一个支持flexboxgap属性的版本。值得庆幸的是,苹果在更新方面非常积极,因此我们可以看到全球浏览器对该属性的支持正在迅速增长。
export const Flex = styled('div', {
display: 'flex',
variants: {
// the rest of the variants
gap: {
none: {
gap: 0,
},
sm: {
gap: '4px',
},
md: {
gap: '8px',
},
lg: {
gap: '16px',
},
},
},
})
这里有几点需要说明。首先,您仍然可以使用填充选项,可以参考Joe Bell的一个优秀示例。其次,仅当 `<div>` 、`<span>` 等标记已集成到您的设计系统中时才使用它们,否则,您可以考虑使用数字标记。第三,我们没有实现强大的`row-gap`和`column-gap` CSS 属性,但您可以像设置 `<div>` 属性一样设置它们。第四,我们保留了`<div> `选项,以便能够以清晰的方式进行条件设置,例如通过断点。xssmTailwindCSSgap'none'gap@mediagap={{ '@desktop': 'none', '@tablet': 'lg' }}
结尾
就是这样!我真心希望越来越多的人能开始将他们的用户界面视为布局和交互元素的组合,并尽可能少地编写 CSS 代码。
您可以在这里看到一些使用示例。就像很多事情一样,您会在实践中逐渐掌握要领,所以请随意尝试这些示例,看看这些道具如何帮助您更好地理解物品的视觉化。
完整示例
import { stlyed } from '@stitches/react'
export const Flex = styled('div', {
display: 'flex',
variants: {
wrap: {
'wrap': {
flexWrap: 'wrap',
},
'no-wrap': {
flexWrap: 'nowrap',
},
'wrap-reverse': {
flexWrap: 'wrap-reverse',
},
},
flow: {
'row': {
flexDirection: 'row',
},
'column': {
flexDirection: 'column',
},
'row-reverse': {
flexDirection: 'row-reverse',
},
'column-reverse': {
flexDirection: 'column-reverse',
},
},
main: {
'start': {
justifyContent: 'flex-start',
},
'center': {
justifyContent: 'center',
},
'end': {
justifyContent: 'flex-end',
},
'stretch': {
justifyContent: 'stretch',
},
'space-between': {
justifyContent: 'space-between',
},
},
cross: {
start: {
alignItems: 'flex-start',
},
center: {
alignItems: 'center',
},
end: {
alignItems: 'flex-end',
},
stretch: {
alignItems: 'stretch',
},
},
gap: {
none: {
gap: 0,
},
sm: {
gap: '4px',
},
md: {
gap: '8px',
},
lg: {
gap: '16px',
},
},
display: {
flex: {
display: 'flex',
},
inline: {
display: 'inline-flex',
},
},
},
})
要点总结:
- 尽可能使 API 符合官方规范,以便于学习。
- 自己创建 API 是可行的,但或许已经有一些比较常见的解决方案,人们也已经习惯使用了。
- 学习其他工具,例如……
Flutter可以开拓新的视角。


