
2000 年代初期,創(chuàng)造了一個(gè)新術(shù)語(yǔ)Divitis,用來(lái)指稱使用許多div 元素編寫(xiě)網(wǎng)頁(yè)程式碼的做法有意義的語(yǔ)義HTML元素
的位置。這是在漸進(jìn)增強(qiáng)技術(shù)框架內(nèi)提高 HTML 語(yǔ)義意識(shí)的努力的一部分。
快轉(zhuǎn) 20 年 - 我目睹了一種影響網(wǎng)絡(luò)開(kāi)發(fā)者的新綜合癥,我稱之為組件炎。這是我編的定義:
元件化:為 UI 的各個(gè)面向建立元件來(lái)取代更簡(jiǎn)單、更可重複使用的解決方案的做法。
成分
那麼,首先,什麼是組件?我認(rèn)為 React 普及了這個(gè)術(shù)語(yǔ)來(lái)指稱它的構(gòu)建塊:
React 可讓您將標(biāo)記、CSS 和 JavaScript 組合到自訂「元件」中,應(yīng)用程式的可重複使用 UI 元素。
— React 文件 - 您的第一個(gè)元件
雖然可重複使用 UI 元素的概念在當(dāng)時(shí)並不新鮮(在 CSS 中,我們已經(jīng)有了 OOCSS、SMACSS 和 BEM 等技術(shù)),但關(guān)鍵的區(qū)別在於它對(duì)標(biāo)記、樣式和元素位置的原始方法。相互作用。使用 React 元件(以及所有後續(xù) UI 庫(kù)),可以將所有內(nèi)容共同定位在元件邊界內(nèi)的單一檔案中。
因此,使用 Facebook 最新的 CSS 庫(kù) Stylex,您可以編寫(xiě):
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>
);
}
你可能喜歡或不喜歡用對(duì)象表示法編寫(xiě)CSS(我不是),但這種級(jí)別的共置通常是使基於組件的項(xiàng)目更易於維護(hù)的好方法:一切都觸手可及,並且明確綁定。
在像 Svelte 這樣的函式庫(kù)中,共置更加清晰(且程式碼更加簡(jiǎ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>
隨著時(shí)間的推移,這種模式獲得瞭如此大的吸引力,以至於所有東西都封裝在組件中。您可能曾經(jīng)遇到這樣的頁(yè)面元件:
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>
);
}
並置一處
上面的程式碼看起來(lái)乾淨(jìng)且一致:我們使用元件介面來(lái)描述頁(yè)面。
那麼,讓我們來(lái)看看 Stack 的可能實(shí)作。該組件通常是一個(gè)包裝器,以確保所有直接子元素垂直堆疊且均勻分佈:
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>
);
}
我們只定義元件的樣式和根元素。
在這種情況下,我們甚至可以說(shuō)我們唯一共同定位的是樣式塊,因?yàn)?HTML 僅用於保存 CSS 類引用,並且沒(méi)有交互性或業(yè)務(wù)邏輯。
靈活性的(可避免的)成本
現(xiàn)在,如果我們希望能夠?qū)⒏爻尸F(xiàn)為一個(gè)部分並可能添加一些屬性怎麼辦?我們需要進(jìn)入多態(tài)組件的領(lǐng)域。在 React 和 TypeScript 中,這最終可能類似於以下內(nèi)容:
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>
);
}
在我看來(lái),乍看之下這不是很可讀。請(qǐng)記?。何覀冎皇怯?3 個(gè) CSS 宣告來(lái)渲染一個(gè)元素。
回到基礎(chǔ)知識(shí)
不久前,我正在用 Angular 開(kāi)發(fā)一個(gè)寵物專案。由於習(xí)慣了用組件來(lái)思考,我聯(lián)繫他們創(chuàng)建了一個(gè)堆疊。事實(shí)證明,在 Angular 中,多型元件的創(chuàng)建更加複雜。
我開(kāi)始質(zhì)疑我的實(shí)作設(shè)計(jì),然後我突然頓悟:當(dāng)解決方案一直就在我面前時(shí),為什麼還要花時(shí)間和程式碼行來(lái)實(shí)現(xiàn)複雜的實(shí)作?
<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 的準(zhǔn)系統(tǒng) 本機(jī) 實(shí)作。在佈局中載入 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 自動(dòng)完成功能。
此外,如果我們不使用間距變體,那麼編寫(xiě)類別和樣式屬性而不是間距屬性可能會(huì)感覺(jué)太冗長(zhǎng)。假設(shè)您正在使用 React,您可以利用 JSX 並建立一個(gè)實(shí)用函數(shù):
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>
);
}
請(qǐng)注意,React TypeScript 不允許未知的 CSS 屬性。為了簡(jiǎn)潔起見(jiàn),我使用了類型斷言,但您應(yīng)該選擇更強(qiáng)大的解決方案。
如果您使用變體,您可以修改實(shí)用函數(shù)以提供類似於 PandaCSS 模式的開(kāi)發(fā)人員體驗(yàn):
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>
);
}
防止程式碼重複和硬編碼值
你們中的一些人可能已經(jīng)注意到,在最後一個(gè)範(fàn)例中,我在 CSS 和實(shí)用程式檔案中硬編碼了間距的預(yù)期值。如果刪除或新增某個(gè)值,這可能會(huì)成為一個(gè)問(wèn)題,因?yàn)槲覀儽仨毐3謨蓚€(gè)檔案同步。
如果您正在建立一個(gè)庫(kù),自動(dòng)化視覺(jué)回歸測(cè)試可能會(huì)發(fā)現(xiàn)此類問(wèn)題。無(wú)論如何,如果它仍然困擾您,解決方案可能是使用 CSS 模組並使用 typed-css-modules 或針對(duì)不支援的值拋出運(yùn)行時(shí)錯(cuò)誤:
<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>
*/
替代方案
如果你仍然認(rèn)為多態(tài)元件會(huì)更好,確實(shí)無(wú)法處理純HTML,或者不想在單獨(dú)的檔案中編寫(xiě)CSS(雖然我不確定為什麼),我的下一個(gè)建議是看看PandaCSS 並建立自訂模式或探索其他選項(xiàng),例如vanilla-extract。在我看來(lái),這些工具是一種過(guò)度設(shè)計(jì)的 CSS 元語(yǔ)言,但仍然比多態(tài)元件更好。
另一個(gè)值得考慮的替代方案是 Tailwind CSS,它具有語(yǔ)言和框架之間可互通的優(yōu)點(diǎn)。
使用 Tailwind 定義的預(yù)設(shè)間距比例,我們可以建立一個(gè)如下所示的堆疊插件:
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>
);
}
順便說(shuō)一句:有趣的是,Tailwind 使用 matchComponents 中的組件心智模型來(lái)描述複雜的 CSS 規(guī)則集,即使它沒(méi)有創(chuàng)建任何 真實(shí) 組件。也許另一個(gè)例子可以說(shuō)明這個(gè)概念是多麼普遍?
重點(diǎn)
成分炎的案例,除了技術(shù)方面之外,還表明了停下來(lái)檢查和質(zhì)疑我們的心理模式和習(xí)慣的重要性。與軟體開(kāi)發(fā)中的許多模式一樣,元件的出現(xiàn)是為了解決實(shí)際問(wèn)題,但是當(dāng)我們開(kāi)始預(yù)設(shè)這種模式時(shí),它就成為了複雜性的無(wú)聲來(lái)源。 成分炎類似於限制飲食引起的營(yíng)養(yǎng)缺乏:?jiǎn)栴}不在於任何單一食物,而在於錯(cuò)過(guò)了其他一切。
以上是並非所有東西都需要組件的詳細(xì)內(nèi)容。更多資訊請(qǐng)關(guān)注PHP中文網(wǎng)其他相關(guān)文章!