Tailwind CSS 在前端开发中逐渐成为了大家的首选样式处理方案,它的简单的集成性以及灵活的可配置性让它在前端开发中逐渐占据较高的地位,但是随着 Tailwind CSS 的使用,很多前端开发人员也出现了不适,classname
太多,导致代码看起来不整洁,样式复杂的情况下,书写 Tailwind CSS 很冗杂,不利于维护等,那么在使用Tailwind CSS 的时候遵循原子组件的理论能不能解决以上问题呢?
Atomic Component
在讨论如何结合原子化组件和Tailwind CSS之前,我们先了解一下原子元组件方法论,就是将页面分为以下5个层级
- Atoms
- Molecules
- Organisms
- Templates
- Pages
每个层级都是由前面的层级拼凑而成,最终形成页面,在这个方法论中,最主要的就是寻找到原子组件,下图帮助我们很好的标注出了原子组件有哪些(原生的 Html Element)。
注:图片来源 (atomicdesign.bradfrost.com/chapter-2/)
Atomic Component 与 Tailwind CSS 的契合
页面是由基础组件拼凑而成的,原子化组件方法论并不是将页面组件尽可能拆分的很小,而是通过原子组件组成分子组件,组织组件,再到页面模板,进而进行页面的搭建,是一个从小到大的过程。
在利用原子化组件开发方法论中,我们需要识别清楚识别到哪些是原子组件,然后对原子组件进行修饰,Tailwind CSS 正好就是利用的 Atomic CSS 的方法论,将冗杂的 css 样式变成单个的 css 属性,这就正好能够解决在代码中出现难以阅读的有很多 classname 的 html 代码,那就是对原子组件进行封装,然后使用原子组件进行分子组件的组合。
例如:
jsx
function ShoppingCard() {
return <div className="flex font-sans">
<div className="flex-none w-48 relative">
<img src="/classNameic-utility-jacket.jpg" alt="" className="absolute inset-0 w-full h-full object-cover" loading="lazy" />
</div>
<form className="flex-auto p-6">
<div className="flex flex-wrap">
<h1 className="flex-auto text-lg font-semibold text-slate-900">
Utility Jacket
</h1>
<div className="text-lg font-semibold text-slate-500">
$110.00
</div>
<div className="w-full flex-none text-sm font-medium text-slate-700 mt-2">
In stock
</div>
</div>
<div className="flex items-baseline mt-4 mb-6 pb-6 border-b border-slate-200">
<div className="space-x-2 flex text-sm">
<label>
<input className="sr-only peer" name="size" type="radio" value="xs" checked />
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-slate-700 peer-checked:font-semibold peer-checked:bg-slate-900 peer-checked:text-white">
XS
</div>
</label>
<label>
<input className="sr-only peer" name="size" type="radio" value="s" />
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-slate-700 peer-checked:font-semibold peer-checked:bg-slate-900 peer-checked:text-white">
S
</div>
</label>
<label>
<input className="sr-only peer" name="size" type="radio" value="m" />
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-slate-700 peer-checked:font-semibold peer-checked:bg-slate-900 peer-checked:text-white">
M
</div>
</label>
<label>
<input className="sr-only peer" name="size" type="radio" value="l" />
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-slate-700 peer-checked:font-semibold peer-checked:bg-slate-900 peer-checked:text-white">
L
</div>
</label>
<label>
<input className="sr-only peer" name="size" type="radio" value="xl" />
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-slate-700 peer-checked:font-semibold peer-checked:bg-slate-900 peer-checked:text-white">
XL
</div>
</label>
</div>
</div>
<div className="flex space-x-4 mb-6 text-sm font-medium">
<div className="flex-auto flex space-x-4">
<button className="h-10 px-6 font-semibold rounded-md bg-black text-white" type="submit">
Buy now
</button>
<button className="h-10 px-6 font-semibold rounded-md border border-slate-200 text-slate-900" type="button">
Add to bag
</button>
</div>
<button className="flex-none flex items-center justify-center w-9 h-9 rounded-md text-slate-300 border border-slate-200" type="button" aria-label="Like">
<svg width="20" height="20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" />
</svg>
</button>
</div>
<p className="text-sm text-slate-700">
Free shipping on all continental US orders.
</p>
</form>
</div>
}
这段代码是官网的一个例子,我们就这个例子来进行重构
Button 组件抽离
首先我们可以提取的原子组件是大家最常见的 <Button>
我们期望的Button
的使用方式为
tsx
<Button variant="filled">Buy now</Button>
所以我们可以将按钮做一个简单的封装
tsx
import { ButtonHTMLAttributes } from "react"
type ButtonVariant =
| 'filled'
| 'outlined'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant: ButtonVariant;
}
export default function Button(props: ButtonProps) {
const {
type,
variant,
className,
children
} = props;
const variantClassNames: {[key: string]: string} = {
filled: 'bg-black text-white',
outlined: 'border border-slate-200 text-slate-900'
}
return <button
className={`h-10 px-6 font-semibold rounded-md ${variantClassNames[variant]} ${className}`}
type={type}>
{children}
</button>
}
然后我们就能够将这段代码
替换为
Icon 组件抽离
当然这里的 svg 也被抽离成为了一个原子组件
tsx
interface IconProps {
size?: number
}
export default function HeartIcon (props: IconProps) {
const { size = 20 } = props;
const rate = size / 20;
return <svg style={{ transform: `scale(${rate})` }} width='20' height='20' fill="currentColor" aria-hidden="true">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
/>
</svg>
}
Radio 组件抽离
在代码的中间我们能够识别到有 Radio
单选组件,那么作为原子组件列表中的 <input type="radio" />
自然也是需要对其进行修饰的。
tsx
import { InputHTMLAttributes } from "react";
interface RadioProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
}
export default function Radio(props: RadioProps) {
const { label, name, value, checked, className, onChange } = props;
return <label className="relative cursor-pointer" htmlFor={label}>
<input
className="sr-only peer"
type="radio"
id={label}
value={value}
name={name}
checked={checked}
onChange={onChange}
/>
<div
className={`min-w-[36px] min-h-[36px] p-2 rounded-lg flex items-center justify-center text-slate-700 peer-checked:font-semibold peer-checked:bg-slate-900 peer-checked:text-white ${className}`}
>
{label}
</div>
</label>
}
将原本单个的单选组件的html直接挪进我们的组件文件中,然后对其进行封装,最后的使用效果为
tsx
function ShopingCard() {
const sizeList = [
'XS', 'S', 'M', 'L', 'XL'
];
const onSizeChange = (event: ChangeEvent<HTMLInputElement>) => {
setCurrentSize(event.target.value);
}
const [currentSize, setCurrentSize] = useState(sizeList[0])
return (
...
<div className="space-x-2 flex text-sm">
{
sizeList.map(size => <Radio
key={size}
name="size"
value={size}
label={size}
checked={currentSize === size}
onChange={onSizeChange}
/>)
}
</div>
...
)
}
在上面我们完成了 Forms
原子组件的封装修饰,接下来需要识别到别的类型的原子组件,即 document sections
和 group content
这类内容型的原子组件的封装修饰。
Typography 组件抽离
简单的文本组件抽离
tsx
import { PropsWithChildren } from "react";
interface TypographyProps extends PropsWithChildren {
className?: string;
variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'lead' | 'paragraph'
}
interface TextProps extends PropsWithChildren {
className?: string;
}
export default function Typography(props: TypographyProps) {
const { variant, children, className } = props;
const baseClassNames = 'block antialiased tracking-normal font-sans font-semibold';
const typographies = {
h1: (p: TextProps) => <h1 className={`${baseClassNames} text-5xl leading-tight ${p.className}`}>{p.children}</h1>,
h2: (p: TextProps) => <h2 className={`${baseClassNames} text-4xl leading-[1.3] ${p.className}`}>{p.children}</h2>,
h3: (p: TextProps) => <h3 className={`${baseClassNames} text-3xl leading-snug ${p.className}`}>{p.children}</h3>,
h4: (p: TextProps) => <h4 className={`${baseClassNames} text-2xl leading-snug ${p.className}`}>{p.children}</h4>,
h5: (p: TextProps) => <h5 className={`${baseClassNames} text-xl leading-snug ${p.className}`}>{p.children}</h5>,
h6: (p: TextProps) => <h6 className={`${baseClassNames} text-lg leading-relaxed ${p.className}`}>{p.children}</h6>,
lead: (p: TextProps) => <p className={`${baseClassNames} text-base font-normal leading-relaxed ${p.className}`}>{p.children}</p>,
paragraph: (p: TextProps) => <p className={`${baseClassNames} text-sm font-normal leading-relaxed ${p.className}`}>{p.children}</p>,
}
return typographies[variant]({
className,
children
});
}
如果想要更加表意的写法,即不使用 Tailwind CSS 的方式书写,我们依然是可以使用样式文件对我们的样式进行进一步的封装,Tailwind CSS 提供了 @apply
装饰器,可以使的我们的 classname 更加表意。
- style.css
css
.base-font {
@apply block antialiased tracking-normal font-sans font-semibold;
}
.heading1 {
@apply text-5xl leading-tight;
}
.heading2 {
@apply text-4xl leading-[1.3];
}
.heading3 {
@apply text-3xl leading-snug;
}
.heading4 {
@apply text-2xl leading-snug;
}
.heading5 {
@apply text-xl leading-snug;
}
.heading6 {
@apply text-lg leading-relaxed;
}
.lead {
@apply text-base font-normal leading-relaxed;
}
.paragraph {
@apply text-sm font-normal leading-relaxed;
}
- typography.tsx
tsx
import { PropsWithChildren, createElement } from "react";
import './style.css'; // 引入样式
interface TypographyProps extends PropsWithChildren {
className?: string;
variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'lead' | 'paragraph';
}
export default function Typography(props: TypographyProps) {
const { variant, children, className } = props;
const headingElements = ['h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6'];
const typographiesClassName = {
h1: 'heading1',
h2: 'heading2',
h3: 'heading3',
h4: 'heading4',
h5: 'heading5',
h6: 'heading6',
lead: 'lead',
paragraph: 'paragraph'
}
// 根据不同标签创建不同样式的元素
return createElement(
headingElements.includes(variant) ? variant : 'p',
{ className: `base-font ${typographiesClassName[variant]} ${className}` },
children
);
}
Card 组件
简单的面板组件
tsx
import { HTMLAttributes } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> { }
export default function Card(props: CardProps) {
const { children, className } = props;
return <div className={`flex font-sans shadow-md p-2 rounded-xl overflow-hidden ${className}`}>
{children}
</div>
}
最终的样子
最后我们的这个购物卡片的代码变得比之前容易维护并且易读,这里面其实还有一些小组件能够进行抽离,比如分割线,图片。
tsx
function ShoppingCard() {
const sizeList = [
'XS', 'S', 'M', 'L', 'XL'
];
const onSizeChange = (event: ChangeEvent<HTMLInputElement>) => {
setCurrentSize(event.target.value);
}
const [currentSize, setCurrentSize] = useState(sizeList[0]);
return (
<div className="flex items-center justify-center h-screen">
<Card>
<div className="flex-none w-48 relative">
<img
src="https://www.tailwindcss.cn/_next/static/media/classic-utility-jacket.82031370.jpg"
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
<form className="flex-auto p-6">
<div className="flex flex-wrap">
<Typography variant="h6" className="flex-auto text-slate-900" >Utility Jacket</Typography>
<Typography variant="h6" className="text-slate-500" >$110.00</Typography>
<Typography variant="lead" className="text-slate-700 mt-2 w-full" >In stock</Typography>
</div>
<div className="flex items-baseline mt-4 mb-6 pb-6 border-b border-slate-200">
<div className="space-x-2 flex text-sm">
{
sizeList.map(size => <Radio
key={size}
name="size"
value={size}
label={size}
checked={currentSize === size}
onChange={onSizeChange}
/>)
}
</div>
</div>
<div className="flex space-x-4 mb-6 text-sm font-medium">
<div className="flex-auto flex space-x-4">
<Button variant="filled">Buy now</Button>
<Button variant="outlined"> Add to bag</Button>
</div>
<Button variant="outlined" className="px-2.5">
<HeartIcon size={24} />
</Button>
</div>
<Typography variant="paragraph" className="text-slate-700" >
Free shipping on all continental US orders.
</Typography>
</form>
</Card>
</div>
)
}
在官网中也有对应的指导,怎么样去做组件的拆分
随着原子组件的抽离,拼凑的页面将会变得直观明了,这是 Atomic Component 和 Atomic CSS 的结合,一个是逻辑层面的原子化,一个是样式层面的原子化,这样子才能真正的达到 Atomic methodology。
参考