2000 年代初期,创造了一个新术语 Divitis,用来指代 使用许多 div 元素编写网页代码的做法有意义的语义 HTML 元素
的位置。这是在渐进增强技术框架内提高 HTML 语义意识的努力的一部分。
快进 20 年 - 我目睹了一种影响网络开发者的新综合症,我称之为组件炎。这是我编造的定义:
组件化:为 UI 的各个方面创建组件来代替更简单、更可重用的解决方案的做法。
那么,首先,什么是组件?我认为 React 普及了这个术语来指代它的构建块:
React 可让您将标记、CSS 和 JavaScript 组合到自定义“组件”中,应用程序的可重用 UI 元素。
— React 文档 - 您的第一个组件
虽然可重用 UI 元素的概念在当时并不新鲜(在 CSS 中,我们已经有了 OOCSS、SMACSS 和 BEM 等技术),但关键的区别在于它对标记、样式和元素位置的原始方法。相互作用。使用 React 组件(以及所有后续 UI 库),可以将所有内容共同定位在组件边界内的单个文件中。
因此,使用 Facebook 最新的 CSS 库 Stylex,您可以编写:
import * as stylex from "@stylexjs/stylex"; import { useState } from "react"; // styles const styles = stylex.create({ base: { fontSize: 16, lineHeight: 1.5, color: "#000", }, }); export function Toggle() { // interactions const [toggle, setToggle] = useState(false); const onClick = () => setToggle((t) => !t); // markup return ( <button {...stylex.props(styles.base)} type="button" onClick={onClick}> {toggle} </button> ); }
你可能喜欢或不喜欢用对象表示法编写 CSS(我不是),但这种级别的共置通常是使基于组件的项目更易于维护的好方法:一切都触手可及,并且明确绑定。
在像 Svelte 这样的库中,共置更加清晰(并且代码更加简洁):
<script> let toggle = $state(false) const onclick = () => toggle = !toggle </script> <button type='button' {onclick}> {toggle} </button> <style> button { font-size: 16px; line-height: 1.5; color: #000; } </style>
随着时间的推移,这种模式获得了如此大的吸引力,以至于所有东西都封装在组件中。您可能遇到过这样的页面组件:
export function Page() { return ( <Layout> <Header nav={<Nav />} /> <Body> <Stack spacing={2}> <Item>Item 1</Item> <Item>Item 2</Item> <Item>Item 3</Item> </Stack> </Body> <Footer /> </Layout> ); }
上面的代码看起来干净且一致:我们使用组件接口来描述页面。
那么,让我们看看 Stack 的可能实现。该组件通常是一个包装器,以确保所有直接子元素垂直堆叠且均匀分布:
import * as stylex from "@stylexjs/stylex"; import type { PropsWithChildren } from "react"; const styles = stylex.create({ root: { display: "flex", flexDirection: "column", }, spacing: (value) => ({ rowGap: value * 16, }), }); export function Stack({ spacing = 0, children, }: PropsWithChildren<{ spacing?: number }>) { return ( <div {...stylex.props(styles.root, styles.spacing(spacing))}> {children} </div> ); }
我们只定义组件的样式和根元素。
在这种情况下,我们甚至可以说我们唯一共同定位的是样式块,因为 HTML 仅用于保存 CSS 类引用,并且没有交互性或业务逻辑。
现在,如果我们希望能够将根元素呈现为一个部分并可能添加一些属性怎么办?我们需要进入多态组件的领域。在 React 和 TypeScript 中,这最终可能会类似于以下内容:
import * as stylex from "@stylexjs/stylex"; import { useState } from "react"; // styles const styles = stylex.create({ base: { fontSize: 16, lineHeight: 1.5, color: "#000", }, }); export function Toggle() { // interactions const [toggle, setToggle] = useState(false); const onClick = () => setToggle((t) => !t); // markup return ( <button {...stylex.props(styles.base)} type="button" onClick={onClick}> {toggle} </button> ); }
在我看来,乍一看这不是很可读。请记住:我们只是用 3 个 CSS 声明来渲染一个元素。
不久前,我正在用 Angular 开发一个宠物项目。由于习惯了用组件来思考,我联系他们创建了一个堆栈。事实证明,在 Angular 中,多态组件的创建更加复杂。
我开始质疑我的实现设计,然后我突然顿悟:当解决方案一直就在我面前时,为什么还要花时间和代码行来实现复杂的实现?
<script> let toggle = $state(false) const onclick = () => toggle = !toggle </script> <button type='button' {onclick}> {toggle} </button> <style> button { font-size: 16px; line-height: 1.5; color: #000; } </style>
真的,这就是 Stack 的准系统 本机 实现。在布局中加载 CSS 后,就可以立即在代码中使用它:
export function Page() { return ( <Layout> <Header nav={<Nav />} /> <Body> <Stack spacing={2}> <Item>Item 1</Item> <Item>Item 2</Item> <Item>Item 3</Item> </Stack> </Body> <Footer /> </Layout> ); }
纯 CSS 解决方案既不提供打字功能,也不提供 IDE 自动完成功能。
此外,如果我们不使用间距变体,那么编写类和样式属性而不是间距属性可能会感觉太冗长。假设您正在使用 React,您可以利用 JSX 并创建一个实用函数:
import * as stylex from "@stylexjs/stylex"; import type { PropsWithChildren } from "react"; const styles = stylex.create({ root: { display: "flex", flexDirection: "column", }, spacing: (value) => ({ rowGap: value * 16, }), }); export function Stack({ spacing = 0, children, }: PropsWithChildren<{ spacing?: number }>) { return ( <div {...stylex.props(styles.root, styles.spacing(spacing))}> {children} </div> ); }
请注意,React TypeScript 不允许未知的 CSS 属性。为了简洁起见,我使用了类型断言,但您应该选择更强大的解决方案。
如果您使用变体,您可以修改实用函数以提供类似于 PandaCSS 模式的开发人员体验:
import * as stylex from "@stylexjs/stylex"; type PolymorphicComponentProps<T extends React.ElementType> = { as?: T; children?: React.ReactNode; spacing?: number; } & React.ComponentPropsWithoutRef<T>; const styles = stylex.create({ root: { display: "flex", flexDirection: "column", }, spacing: (value) => ({ rowGap: value * 16, }), }); export function Stack<T extends React.ElementType = "div">({ as, spacing = 1, children, ...props }: PolymorphicComponentProps<T>) { const Component = as || "div"; return ( <Component {...props} {...stylex.props(styles.root, styles.spacing(spacing))} > {children} </Component> ); }
你们中的一些人可能已经注意到,在最后一个示例中,我在 CSS 和实用程序文件中硬编码了间距的预期值。如果删除或添加某个值,这可能会成为一个问题,因为我们必须保持两个文件同步。
如果您正在构建一个库,自动化视觉回归测试可能会发现此类问题。无论如何,如果它仍然困扰您,解决方案可能是使用 CSS 模块并使用 typed-css-modules 或针对不支持的值抛出运行时错误:
<div> <pre class="brush:php;toolbar:false">.stack { --s: 0; display: flex; flex-direction: column; row-gap: calc(var(--s) * 16px); }
export function Page() { return ( <Layout> <Header nav={<Nav />} /> <Body> <div className="stack"> <p>Let's see the main advantages of this approach:</p> <ul> <li>reusability</li> <li>reduced complexity</li> <li>smaller JavaScript bundle and less overhead</li> <li><strong>interoperability</strong></li> </ul> <p>The last point is easy to overlook: Not every project uses React, and if you’re including the stack layout pattern in a Design System or a redistributable UI library, developers could use it in projects using different UI frameworks or a server-side language like PHP or Ruby.</p> <h2> Nice features and improvements </h2> <p>From this base, you can iterate to add more features and improve the developer experience. While some of the following examples target React specifically, they can be easily adapted to other frameworks.</p> <h3> Control spacing </h3> <p>If you're developing a component library you definitely want to define a set of pre-defined spacing variants to make space more consistent. This approach also eliminates the need to explicitly write the style attribute:<br> </p> <pre class="brush:php;toolbar:false">.stack { --s: 0; display: flex; flex-direction: column; row-gap: calc(var(--s) * 16px); &.s\:1 { --s: 1 } &.s\:2 { --s: 2 } &.s\:4 { --s: 4 } &.s\:6 { --s: 6 } } /** Usage: <div> <p>For a bolder approach to spacing, see Complementary Space by Donnie D'Amato.</p> <h3> Add better scoping </h3> <p>Scoping, in this case, refers to techniques to prevent conflicts with other styles using the same selector. I’d argue that scoping issues affects a pretty small number of projects, but if you are really concerned about it, you could:</p> <ol> <li>Use something as simple as CSS Modules, which is well supported in all major bundlers and frontend frameworks.</li> <li>Use cascade layers resets to prevent external stylesheets from modifying your styles (this is an interesting technique).</li> <li>Define a specific namespace like .my-app-... for your classes.</li> </ol> <p>Here is the result with CSS Modules:<br> </p> <pre class="brush:php;toolbar:false">.stack { --s: 0; display: flex; flex-direction: column; row-gap: calc(var(--s) * 16px); &.s1 { --s: 1 } &.s2 { --s: 2 } &.s4 { --s: 4 } &.s6 { --s: 6 } } /** Usage import * from './styles/stack.module.css' <div className={`${styles.stack} ${styles.s2}`}> // ... </div> */
如果你仍然认为多态组件会更好,确实无法处理纯 HTML,或者不想在单独的文件中编写 CSS(虽然我不确定为什么),我的下一个建议是看看 PandaCSS 并创建自定义模式或探索其他选项,例如 vanilla-extract。在我看来,这些工具是一种过度设计的 CSS 元语言,但仍然比多态组件更好。
另一个值得考虑的替代方案是 Tailwind CSS,它具有语言和框架之间可互操作的优势。
使用 Tailwind 定义的默认间距比例,我们可以创建一个如下所示的堆栈插件:
import * as stylex from "@stylexjs/stylex"; import { useState } from "react"; // styles const styles = stylex.create({ base: { fontSize: 16, lineHeight: 1.5, color: "#000", }, }); export function Toggle() { // interactions const [toggle, setToggle] = useState(false); const onClick = () => setToggle((t) => !t); // markup return ( <button {...stylex.props(styles.base)} type="button" onClick={onClick}> {toggle} </button> ); }
顺便说一句:有趣的是,Tailwind 使用 matchComponents 中的组件心智模型来描述复杂的 CSS 规则集,即使它没有创建任何 真实 组件。也许另一个例子可以说明这个概念是多么普遍?
成分炎的案例,除了技术方面之外,还表明了停下来检查和质疑我们的心理模式和习惯的重要性。与软件开发中的许多模式一样,组件的出现是为了解决实际问题,但是当我们开始默认这种模式时,它就成为了复杂性的无声来源。 成分炎类似于限制饮食引起的营养缺乏:问题不在于任何单一食物,而在于错过了其他一切。
以上是并非所有东西都需要组件的详细内容。更多信息请关注PHP中文网其他相关文章!