
2000 年代初期,創(chuàng)造了一個新術語 Divitis,用來指代 使用許多 div 元素編寫網頁代碼的做法有意義的語義 HTML 元素
的位置。這是在漸進增強技術框架內提高 HTML 語義意識的努力的一部分。
快進 20 年 - 我目睹了一種影響網絡開發(fā)者的新綜合癥,我稱之為組件炎。這是我編造的定義:
組件化:為 UI 的各個方面創(chuàng)建組件來代替更簡單、更可重用的解決方案的做法。
成分
那么,首先,什么是組件?我認為 React 普及了這個術語來指代它的構建塊:
React 可讓您將標記、CSS 和 JavaScript 組合到自定義“組件”中,應用程序的可重用 UI 元素。
— React 文檔 - 您的第一個組件
雖然可重用 UI 元素的概念在當時并不新鮮(在 CSS 中,我們已經有了 OOCSS、SMACSS 和 BEM 等技術),但關鍵的區(qū)別在于它對標記、樣式和元素位置的原始方法。相互作用。使用 React 組件(以及所有后續(xù) 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 的可能實現(xiàn)。該組件通常是一個包裝器,以確保所有直接子元素垂直堆疊且均勻分布:
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 類引用,并且沒有交互性或業(yè)務邏輯。
靈活性的(可避免的)成本
現(xiàn)在,如果我們希望能夠將根元素呈現(xiàn)為一個部分并可能添加一些屬性怎么辦?我們需要進入多態(tài)組件的領域。在 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 開發(fā)一個寵物項目。由于習慣了用組件來思考,我聯(lián)系他們創(chuàng)建了一個堆棧。事實證明,在 Angular 中,多態(tài)組件的創(chuàng)建更加復雜。
我開始質疑我的實現(xiàn)設計,然后我突然頓悟:當解決方案一直就在我面前時,為什么還要花時間和代碼行來實現(xiàn)復雜的實現(xiàn)?
<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 的準系統(tǒng) 本機 實現(xiàn)。在布局中加載 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>
);
}
在 JavaScript 框架中添加類型安全
純 CSS 解決方案既不提供打字功能,也不提供 IDE 自動完成功能。
此外,如果我們不使用間距變體,那么編寫類和樣式屬性而不是間距屬性可能會感覺太冗長。假設您正在使用 React,您可以利用 JSX 并創(chuàng)建一個實用函數:
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 模式的開發(fā)人員體驗:
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 和實用程序文件中硬編碼了間距的預期值。如果刪除或添加某個值,這可能會成為一個問題,因為我們必須保持兩個文件同步。
如果您正在構建一個庫,自動化視覺回歸測試可能會發(fā)現(xiàn)此類問題。無論如何,如果它仍然困擾您,解決方案可能是使用 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>
*/
替代方案
如果你仍然認為多態(tài)組件會更好,確實無法處理純 HTML,或者不想在單獨的文件中編寫 CSS(雖然我不確定為什么),我的下一個建議是看看 PandaCSS 并創(chuàng)建自定義模式或探索其他選項,例如 vanilla-extract。在我看來,這些工具是一種過度設計的 CSS 元語言,但仍然比多態(tài)組件更好。
另一個值得考慮的替代方案是 Tailwind CSS,它具有語言和框架之間可互操作的優(yōu)勢。
使用 Tailwind 定義的默認間距比例,我們可以創(chuàng)建一個如下所示的堆棧插件:
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 規(guī)則集,即使它沒有創(chuàng)建任何 真實 組件。也許另一個例子可以說明這個概念是多么普遍?
要點
成分炎的案例,除了技術方面之外,還表明了停下來檢查和質疑我們的心理模式和習慣的重要性。與軟件開發(fā)中的許多模式一樣,組件的出現(xiàn)是為了解決實際問題,但是當我們開始默認這種模式時,它就成為了復雜性的無聲來源。 成分炎類似于限制飲食引起的營養(yǎng)缺乏:問題不在于任何單一食物,而在于錯過了其他一切。
以上是并非所有東西都需要組件的詳細內容。更多信息請關注PHP中文網其他相關文章!