Styled-Components:现代 Web 的 CSS-in-JS 库
已发表: 2022-03-11CSS 是为文档设计的,“旧网络”应该包含的内容。 像 Sass 或 Less 这样的预处理器的出现表明,社区需要的比 CSS 提供的要多。 随着网络应用程序随着时间的推移变得越来越复杂,CSS 的局限性变得越来越明显并且难以缓解。
Styled-components利用完整的编程语言(JavaScript)的强大功能及其范围功能来帮助将代码结构化为组件。 这有助于避免为大型项目编写和维护 CSS 的常见陷阱。 开发人员可以在没有副作用风险的情况下描述组件的样式。
有什么问题?
使用 CSS 的一个优点是样式与代码完全分离。 这意味着开发人员和设计人员可以并行工作而不会相互干扰。
另一方面, styled-components更容易陷入风格和逻辑强耦合的陷阱。 Max Stoiber 解释了如何避免这种情况。 虽然分离逻辑和表示的想法绝对不是新的,但在开发 React 组件时可能会想走捷径。 例如,很容易为一个验证按钮创建一个组件来处理点击动作以及按钮的样式。 将它分成两个组件需要更多的努力。
容器/展示架构
这是一个非常简单的原则。 组件要么定义事物的外观,要么管理数据和逻辑。 表示组件的一个非常重要的方面是它们不应该有任何依赖关系。 他们接收道具并相应地渲染 DOM(或孩子)。 另一方面,容器知道数据架构(状态、redux、flux 等),但绝不应该负责显示。 Dan Abramov 的文章对这种架构进行了非常好的和详细的解释。
记住 SMACSS
尽管 CSS 的 Scalable and Modular Architecture 是组织 CSS 的样式指南,但基本概念是样式组件自动遵循的,大部分情况下是自动遵循的。 这个想法是将 CSS 分为五类:
- Base包含所有一般规则。
- Layout的目的是定义结构属性以及内容的各个部分(例如页眉、页脚、侧边栏、内容)。
- 模块包含 UI 的各种逻辑块的子类别。
- 状态定义修饰符类来指示元素的状态,例如错误的字段,禁用的按钮。
- 主题包含可以修改或取决于用户偏好的颜色、字体和其他外观方面。
在使用样式组件时保持这种分离很容易。 项目通常包括某种 CSS 规范化或重置。 这通常属于基本类别。 您还可以定义一般的字体大小、线条大小等。这可以通过普通的 CSS(或 Sass/Less)或通过styled-components提供的injectGlobal
函数来完成。
对于布局规则,如果您使用 UI 框架,那么它可能会定义容器类或网格系统。 您可以轻松地将这些类与您自己编写的布局组件中的规则结合使用。
模块自动跟随styled-components的架构,因为样式直接附加到组件,而不是在外部文件中描述。 基本上,您编写的每个样式组件都是其自己的模块。 您可以编写样式代码而不必担心副作用。
状态将是您在组件中定义为变量规则的规则。 您只需定义一个函数来插入 CSS 属性的值。 如果使用 UI 框架,您可能还会将有用的类添加到您的组件中。 您可能还会有 CSS 伪选择器规则(悬停、焦点等)
主题可以简单地插入到您的组件中。 将您的主题定义为一组要在整个应用程序中使用的变量是一个好主意。 您甚至可以通过编程方式(使用库或手动)获取颜色,例如处理对比度和高光。 请记住,您拥有编程语言的全部功能!
将他们聚集在一起寻求解决方案
为了更轻松的导航体验,将它们放在一起很重要; 我们不想按类型(表示与逻辑)来组织它们,而是按功能来组织它们。
因此,我们将为所有通用组件(按钮等)创建一个文件夹。 其他应根据项目及其功能进行组织。 例如,如果我们有用户管理功能,我们应该对特定于该功能的所有组件进行分组。
要将styled-components 的容器/表示架构应用于 SMACSS 方法,我们需要一种额外的组件类型:结构。 我们最终得到三种组件; 样式、结构和容器。 由于styled-components装饰了一个标签(或组件),我们需要第三种类型的组件来构建 DOM。 在某些情况下,可能允许容器组件处理子组件的结构,但是当 DOM 结构变得复杂并且出于视觉目的需要时,最好将它们分开。 一个很好的例子是一个表格,其中 DOM 通常会变得非常冗长。
示例项目
让我们构建一个显示食谱的小应用程序来说明这些原则。 我们可以开始构建一个食谱组件。 父组件将是一个控制器。 它将处理状态——在本例中是配方列表。 它还将调用 API 函数来获取数据。
class Recipes extends Component{ constructor (props) { super(props); this.state = { recipes: [] }; } componentDidMount () { this.loadData() } loadData () { getRecipes().then(recipes => { this.setState({recipes}) }) } render() { let {recipes} = this.state return ( <RecipesContainer recipes={recipes} /> ) } }
它将呈现食谱列表,但它不需要(也不应该)知道如何。 所以我们渲染另一个组件,它获取配方列表并输出 DOM:
class RecipesContainer extends Component{ render() { let {recipes} = this.props return ( <TilesContainer> {recipes.map(recipe => (<Recipe key={recipe.id} {...recipe}/>))} </TilesContainer> ) } }
在这里,实际上,我们想要制作一个平铺网格。 将实际的 tile 布局设为通用组件可能是个好主意。 因此,如果我们提取它,我们会得到一个如下所示的新组件:

class TilesContainer extends Component { render () { let {children} = this.props return ( <Tiles> { React.Children.map(children, (child, i) => ( <Tile key={i}> {child} </Tile> )) } </Tiles> ) } }
TilesStyles.js:
export const Tiles = styled.div` padding: 20px 10px; display: flex; flex-direction: row; flex-wrap: wrap; ` export const Tile = styled.div` flex: 1 1 auto; ... display: flex; & > div { flex: 1 0 auto; } `
请注意,此组件纯粹是展示性的。 它定义了它的样式并将它接收到的任何子元素包装在另一个样式化的 DOM 元素中,该元素定义了瓦片的外观。 这是一个很好的例子,说明您的通用表示组件在架构上的外观。
然后,我们需要定义配方的外观。 我们需要一个容器组件来描述相对复杂的 DOM,并在必要时定义样式。 我们最终得到了这个:
class RecipeContainer extends Component { onChangeServings (e) { let {changeServings} = this.props changeServings(e.target.value) } render () { let {title, ingredients, instructions, time, servings} = this.props return ( <Recipe> <Title>{title}</Title> <div>{time}</div> <div>Serving <input type="number" min="1" max="1000" value={servings} onChange={this.onChangeServings.bind(this)}/> </div> <Ingredients> {ingredients.map((ingredient, i) => ( <Ingredient key={i} servings={servings}> <span className="name">{ingredient.name}</span> <span className="quantity">{ingredient.quantity * servings} {ingredient.unit}</span> </Ingredient> ))} </Ingredients> <div> {instructions.map((instruction, i) => (<p key={i}>{instruction}</p>))} </div> </Recipe> ) } }
请注意,容器会生成一些 DOM,但它是它包含的唯一逻辑。 请记住,您可以定义嵌套样式,因此您不需要为每个需要样式的标签创建样式元素。 这就是我们在这里为成分项目的名称和数量所做的。 当然,我们可以进一步拆分它并为一种成分创建一个新组件。 这取决于您(取决于项目的复杂性)来确定粒度。 在这种情况下,它只是与 RecipeStyles 文件中的其余部分一起定义的样式组件:
export const Recipe = styled.div` background-color: ${theme('colors.background-highlight')}; `; export const Title = styled.div` font-weight: bold; ` export const Ingredients = styled.ul` margin: 5px 0; ` export const Ingredient = styled.li` & .name { ... } & .quantity { ... } `
出于本练习的目的,我使用了 ThemeProvider。 它将主题注入到样式化组件的 props 中。 您可以简单地将其用作color: ${props => props.theme.core_color}
,我只是使用一个小包装器来防止主题中缺少属性:
const theme = (key) => (prop) => _.get(prop.theme, key) || console.warn('missing key', key)
您还可以在模块中定义自己的常量并使用这些常量。 例如: color: ${styleConstants.core_color}
优点
使用styled-components的一个好处是您可以尽可能少地使用它。 您可以使用自己喜欢的 UI 框架并在其上添加样式组件。 这也意味着您可以轻松地逐个迁移现有项目组件。 您可以选择使用标准 CSS 为大部分布局设置样式,并且仅将styled-components用于可重用组件。
缺点
设计师/风格整合者需要学习非常基本的 JavaScript 来处理变量并使用它们来代替 Sass/Less。
他们还必须学习导航项目结构,尽管我认为在该组件的文件夹中查找组件的样式比必须找到包含您需要修改的规则的正确 CSS/Sass/Less 文件更容易。
如果他们想要语法高亮、linting 等,他们还需要稍微改变他们的工具。一个很好的起点是这个 Atom 插件和这个 babel 插件。