在一个理想的世界里,我们的项目将拥有无限的资源和时间。我们的团队将使用经过精心思考和高度优化的 UX 设计开始编码。开发人员之间将就最佳的样式方法达成共识。团队中将有一位或多位 CSS 专家,他们可以确保功能和样式可以同时推出,而不会变成一团糟。
我确实在大型企业环境中见过这种情况。这是一件美好的事情。本文不适用于这些人。
另一方面,小型创业公司资金为零,只有一到两位前端开发人员,并且需要在很短的时间内展示一些功能。它不必看起来完美,但至少应该在台式机、平板电脑和手机上合理地呈现。这将使他们能够将其展示给顾问和早期用户;甚至可能向对该概念表示兴趣的潜在投资者展示。一旦他们从销售和/或投资中获得一些现金流,他们就可以获得专门的 UX 设计师并完善界面。
以下内容适用于后一组人员。
项目启动会议
让我们发明一家公司来推动项目发展。
Solar Excursions 是一家小型旅行社,旨在服务于近未来蓬勃发展的太空旅游行业。
我们的小型开发团队已同意使用 React 作为 UI。我们的一位前端开发人员非常喜欢 Sass,而另一位则迷恋于 JavaScript 中的 CSS。但他们很难完成其最初的冲刺目标;当然没有时间争论最佳的样式方法。两位编码人员都同意,从长远来看,选择并不重要,只要执行一致即可。他们确信,现在在压力下从头开始实现样式将产生技术债务,以后必须清理。
经过一番讨论,团队选择计划进行一个或多个“样式重构”冲刺。目前,我们将专注于使用 React-Bootstrap 在屏幕上显示一些内容。这样,我们就可以快速构建工作台式机和移动版布局,而无需太多麻烦。
在前端样式上花费的时间越少越好,因为我们还需要 UI 连接到我们的后端开发人员将要推出的服务。并且,随着我们的应用程序架构开始成形,两位前端开发人员都同意对其进行单元测试非常重要。他们的工作量很大。
根据我与上级领导的讨论,作为一名专门的项目经理,我至少在 Balsamiq 上辛苦工作了十分钟,为团队提供了台式机和移动设备上预订页面的线框图。我假设他们会使平板电脑介于两者之间并看起来合理。

冲刺零:审查会议
人人都有披萨!团队非常努力地实现了其目标,我们现在有一个布局接近线框图的预订页面。服务的架构正在逐步完善,但在我们将 UI 连接到它之前还有很长的路要走。在此期间,前端开发人员正在使用硬编码的模拟数据结构。

以下是我们迄今为止的 UI 代码
这都是简单的 React。我们正在使用一些 Hooks 热点,但对于你们中的大多数人来说,这可能已经过时了。
这里需要注意的关键要点是,我们的五个应用程序组件中有四个导入并使用了来自 react-bootstrap 的组件。只有主 App 组件不受影响。这是因为它只是使用我们的自定义组件组合顶级视图。
// App.js imports
import React, { useState } from "react";
import Navigation from "./Navigation";
import Page from "./Page";
// Navigation.js imports
import React from "react";
import { Navbar, Dropdown, Nav } from "react-bootstrap";
// Page.js imports
import React from "react";
import PosterCarousel from "./PosterCarousel";
import DestinationLayout from "./DestinationLayout";
import { Container, Row, Col } from "react-bootstrap";
// PosterCarousel.js imports
import React from "react";
import { Alert, Carousel, Image } from "react-bootstrap";
// DestinationLayout.js imports
import React, { useState, useEffect } from "react";
import {
Button,
Card,
Col,
Container,
Dropdown,
Jumbotron,
ListGroup,
Row,
ToggleButtonGroup,
ToggleButton
} from "react-bootstrap";
快速使用 Bootstrap 的决定使我们能够实现冲刺目标,但我们已经开始积累 技术债务。这仅仅是四个受影响的组件,但随着应用程序的增长,我们计划进行的“样式重构”冲刺显然会变得越来越难。而且我们甚至没有太多自定义这些组件。一旦我们拥有数十个组件,所有组件都使用 Bootstrap 并使用大量内联样式来美化它们,重构它们以删除 react-bootstrap 依赖项将是一个非常可怕的提议。
团队决定,与其构建更多预订流程页面,不如在下一个冲刺中努力将 react-bootstrap 的使用隔离到自定义组件套件中,因为我们的服务仍在构建中。应用程序组件将仅使用此套件中的组件。这样,当我们需要摆脱 react-bootstrap 时,该过程就会容易得多。我们不必在整个应用程序中重构 30 个 react-bootstrap Button
的用法,而只需重写 KitButton
组件的内部。
冲刺一:审查会议
好吧,这很容易。击掌。 UI 的视觉外观没有变化,但我们现在在 React 源代码中的“组件”旁边有一个“套件”文件夹。它包含许多文件,例如 KitButton.js,这些文件基本上导出重命名的 react-bootstrap 组件。
我们套件中的一个示例组件如下所示
// KitButton.js
import { Button, ToggleButton, ToggleButtonGroup } from "react-bootstrap";
export const KitButton = Button;
export const KitToggleButton = ToggleButton;
export const KitToggleButtonGroup = ToggleButtonGroup;
我们将所有套件组件包装到类似这样的模块中
// kit/index.js
import { KitCard } from "./KitCard";
import { KitHero } from "./KitHero";
import { KitList } from "./KitList";
import { KitImage } from "./KitImage";
import { KitCarousel } from "./KitCarousel";
import { KitDropdown } from "./KitDropdown";
import { KitAttribution } from "./KitAttribution";
import { KitNavbar, KitNav } from "./KitNavbar";
import { KitContainer, KitRow, KitCol } from "./KitContainer";
import { KitButton, KitToggleButton, KitToggleButtonGroup } from "./KitButton";
export {
KitCard,
KitHero,
KitList,
KitImage,
KitCarousel,
KitDropdown,
KitAttribution,
KitButton,
KitToggleButton,
KitToggleButtonGroup,
KitContainer,
KitRow,
KitCol,
KitNavbar,
KitNav
};
现在,我们的应用程序组件完全独立于 react-bootstrap。以下是受影响组件的导入
// Navigation.js imports
import React from "react";
import { KitNavbar, KitNav, KitDropdown } from "../kit";
// Page.js imports
import React from "react";
import PosterCarousel from "./PosterCarousel";
import DestinationLayout from "./DestinationLayout";
import { KitContainer, KitRow, KitCol } from "../kit";
// PosterCarousel.js imports
import React from "react";
import { KitAttribution, KitImage, KitCarousel } from "../kit";
// DestinationLayout.js imports
import React, { useState, useEffect } from "react";
import {
KitCard,
KitHero,
KitList,
KitButton,
KitToggleButton,
KitToggleButtonGroup,
KitDropdown,
KitContainer,
KitRow,
KitCol
} from "../kit";
现在是前端代码库
尽管我们已将所有 react 导入都集中到套件组件中,但我们的应用程序组件仍然依赖于 react-bootstrap 实现,因为我们放置在套件组件实例上的属性与 react-bootstrap 的属性相同。这在重新实现套件组件时会限制我们,因为我们需要遵守相同的 API。例如
// From Navigation.js
<KitNavbar bg="dark" variant="dark" fixed="top">
理想情况下,在实例化 KitNavbar
时,我们不必添加那些特定于 react-bootstrap 的属性。
前端开发人员承诺在继续进行的过程中重构这些属性,因为我们现在已将它们识别为有问题的。任何对 react-bootstrap 组件的新引用都将进入我们的套件,而不是直接进入应用程序组件。
同时,我们已与服务器工程师共享了我们的模拟数据,服务器工程师正在努力构建单独的服务器环境,实现数据库模式,并向我们公开一些服务。
这使我们有时间在下一个冲刺中为我们的 UI 添加一些光泽——这很好,因为上级领导希望看到每个目的地的单独主题。当用户浏览目的地时,我们需要 UI 颜色方案更改以匹配显示的旅行海报。此外,我们希望尝试稍微修饰这些组件,以开始发展我们自己的外观和风格。一旦我们获得一些收入,我们将聘请设计师进行全面大修,但希望我们能够为我们的早期用户找到一个折衷方案。
冲刺二:审查会议
哇!团队在这个冲刺中确实全力以赴。我们获得了每个目的地的主题、自定义组件以及从应用程序组件中删除了许多残留的 react-bootstrap API 实现。
以下是台式机现在的样子

为了实现这一点,前端开发人员引入了 Styled Components 库。它使为各个套件组件设置样式变得轻而易举,并添加了对多个主题的支持。
让我们看看他们这个冲刺中的一些亮点。
首先,对于导入字体和设置页面主体样式等全局内容,我们有一个名为 KitGlobal
的新套件组件。
// KitGlobal.js
import { createGlobalStyle } from "styled-components";
export const KitGlobal = createGlobalStyle`
body {
@import url('https://fonts.googleapis.com/css?family=Orbitron:500|Nunito:600|Alegreya+Sans+SC:700');
background-color: ${props => props.theme.foreground};
overflow-x: hidden;
}
`;
它使用 createGlobalStyle
帮助程序来定义 body 元素的 CSS。它从 Google 导入我们所需的网页字体,将背景颜色设置为当前主题的“前景”值,并在 x 方向上关闭溢出以消除讨厌的水平滚动条。我们在 App
组件的 render 方法中使用该 KitGlobal
组件。
同样在 App
组件中,我们从 styled-components 中导入 ThemeProvider
,以及从 ../theme
中导入名为“themes”的内容。我们使用 React 的 useState
将初始主题设置为 themes.luna
,并使用 React 的 useEffect
在“destination”更改时调用 setTheme
。返回的组件现在包装在 ThemeProvider
中,并将其“theme”作为 prop 传递。以下是完整的 App
组件。
// App.js
import React, { useState, useEffect } from "react";
import { ThemeProvider } from "styled-components";
import themes from "../theme/";
import { KitGlobal } from "../kit";
import Navigation from "./Navigation";
import Page from "./Page";
export default function App(props) {
const [destinationIndex, setDestinationIndex] = useState(0);
const [theme, setTheme] = useState(themes.luna);
const destination = props.destinations[destinationIndex];
useEffect(() => {
setTheme(themes[destination.theme]);
}, [destination]);
return (
<ThemeProvider theme={theme}>
<React.Fragment>
<KitGlobal />
<Navigation
{...props}
destinationIndex={destinationIndex}
setDestinationIndex={setDestinationIndex}
/>
<Page
{...props}
destinationIndex={destinationIndex}
setDestinationIndex={setDestinationIndex}
/>
</React.Fragment>
</ThemeProvider>
);
}
KitGlobal
就像任何其他组件一样渲染。那里没有什么特别的,只是 body 标签受到影响。ThemeProvider
使用 React Context API 将 theme
传递给任何需要它的组件(即所有组件)。为了充分理解这一点,我们还需要查看主题的实际内容。
为了创建主题,我们的一位前端开发人员获取了所有旅行海报,并通过提取主要颜色为每个海报创建了调色板。这相当简单。

显然,我们不会使用所有颜色。主要的方法是将两个最常用的颜色分别命名为foreground
和background
。然后,我们又选择了三种颜色,通常按照从浅到深的顺序命名为accent1
、accent2
和accent3
。最后,我们选择了两种对比色,分别命名为text1
和text2
。对于上述目的地,看起来像这样
// theme/index.js (partial list)
const themes = {
...
mars: {
background: "#a53237",
foreground: "#f66f40",
accent1: "#f8986d",
accent2: "#9c4952",
accent3: "#f66f40",
text1: "#f5e5e1",
text2: "#354f55"
},
...
};
export default themes;
一旦我们为每个目的地设置了主题,并且该主题被传递到所有组件(包括我们应用程序组件现在构建的套件组件),我们就需要使用styled-components来应用这些主题颜色以及我们自定义的视觉样式,例如面板角和“边框发光”。
这是一个简单的例子,我们使我们的KitHero
组件将主题和自定义样式应用于Bootstrap Jumbotron
// KitHero.js
import styled from "styled-components";
import { Jumbotron } from "react-bootstrap";
export const KitHero = styled(Jumbotron)`
background-color: ${props => props.theme.accent1};
color: ${props => props.theme.text2};
border-radius: 7px 25px;
border-color: ${props => props.theme.accent3};
border-style: solid;
border-width: 1px;
box-shadow: 0 0 1px 2px #fdb813, 0 0 3px 4px #f8986d;
font-family: "Nunito", sans-serif;
margin-bottom: 20px;
`;
在这种情况下,我们可以使用styled-components返回的内容,因此我们只需将其命名为KitHero并导出它。
当我们在应用程序中使用它时,它看起来像这样
// DestinationLayout.js (partial code)
const renderHero = () => {
return (
<KitHero>
<h2>{destination.header}</h2>
<p>{destination.blurb}</p>
<KitButton>Book Your Trip Now!</KitButton>
</KitHero>
);
};
然后还有更复杂的情况,我们希望预设react-bootstrap组件的一些属性。例如,我们之前识别出的KitNavbar
组件,它具有一堆我们宁愿不从应用程序的组件声明中传递的react-bootstrap属性。
现在让我们看看它是如何处理的
// KitNavbar.js (partial code)
import React, { Component } from "react";
import styled from "styled-components";
import { Navbar } from "react-bootstrap";
const StyledBootstrapNavbar = styled(Navbar)`
background-color: ${props => props.theme.background};
box-shadow: 0 0 1px 2px #fdb813, 0 0 3px 4px #f8986d;
display: flex;
flex-direction: horizontal;
justify-content: space-between;
font-family: "Nunito", sans-serif;
`;
export class KitNavbar extends Component {
render() {
const { ...props } = this.props;
return <StyledBootstrapNavbar fixed="top" {...props} />;
}
}
首先,我们使用styled-components创建一个名为StyledBootstrapNavbar
的组件。我们能够使用传递给styled-components的CSS处理一些属性。但是为了继续利用(目前)组件粘贴到屏幕顶部的可靠性,同时其他所有内容都被滚动,我们的前端开发人员选择继续使用react-bootstrap的fixed
属性。为此,我们必须创建一个KitNavbar
组件,该组件呈现一个带有fixed=top
属性的StyledBootstrapNavbar
实例。我们还传递了所有道具,包括其子元素。
只有当我们希望在我们的套件组件中默认显式设置某些属性时,我们才需要创建一个单独的类来呈现styled-component的工作并将其道具传递给它。在大多数情况下,我们只需命名和返回styled-component的输出,并像上面对KitHero
所做的那样使用它。
现在,当我们在应用程序的Navigation
组件中渲染KitNavbar
时,它看起来像这样
// Navigation.js (partial code)
return (
<KitNavbar>
<KitNavbarBrand>
<KitLogo />
Solar Excursions
</KitNavbarBrand>
{renderDestinationMenu()}
</KitNavbar>
);
最后,我们第一次尝试将我们的套件组件从react-bootstrap中重构出来。KitAttribution
组件是一个Bootstrap Alert
,就我们的目的而言,它只不过是一个普通的div。我们能够轻松地重构以消除其对react-bootstrap的依赖。
这是该组件在上一冲刺中产生的样子
// KitAttribution.js (using react-bootstrap)
import { Alert } from "react-bootstrap";
export const KitAttribution = Alert;
现在它看起来像这样
// KitAttribution.js
import styled from "styled-components";
export const KitAttribution = styled.div`
text-align: center;
background-color: ${props => props.theme.accent1};
color: ${props => props.theme.text2};
border-radius: 7px 25px;
border-color: ${props => props.theme.accent3};
border-style: solid;
border-width: 1px;
box-shadow: 0 0 1px 2px #fdb813, 0 0 3px 4px #f8986d;
font-family: "Alegreya Sans SC", sans-serif;
> a {
color: ${props => props.theme.text2};
font-family: "Nunito", sans-serif;
}
> a:hover {
color: ${props => props.theme.background};
text-decoration-color: ${props => props.theme.accent3};
}
`;
注意我们不再导入react-bootstrap,并且我们使用styled.div
作为组件基础。它们不会都那么容易,但这是一个过程。
以下是我们的团队在第二轮冲刺中进行的样式和主题工作的结果
单独查看主题页面此处。
结论
经过三个冲刺,我们的团队已在为UI建立可扩展的组件架构的道路上取得了良好的进展。
- 由于react-bootstrap,我们正在快速前进,但不再因此积累大量技术债务。
- 借助styled-components,我们能够实现多个主题(就像如今互联网上几乎每个应用程序都支持深色和浅色模式一样)。我们也不再像一个开箱即用的Bootstrap应用程序了。
- 通过实现一个包含所有对react-bootstrap的引用的自定义组件套件,我们可以随着时间的推移将其重构出来。
在GitHub上分叉最终代码库。
你能描述一下以下内容在styled components示例中目标是什么吗?
嗨,Marc,
抓住了要点。在幕后,这实际上是一个CSS技巧。
在styled-components中,&符号将被生成的类名替换:https://www.styled-components.com/docs/api#supported-css
在CSS中,您可以重复类名以提高特异性:https://www.w3.org/TR/selectors-3/#specificity
因此,当您尝试覆盖现有样式(例如React-Bootstrap中的MenuItem上的样式),并且您定义的样式实际上并没有覆盖库中的样式时,您可以使用此技巧强制覆盖,而不是诉诸于!important(有时!important本身也不起作用)。您可以一直添加&符号,直到覆盖生效。
当然,这是一个权宜之计,直到您重构所有预先存在的样式(如果您遵循文章中讨论的轨迹)。