传统上,当使用 Tailwind CSS 编写组件变体时,我会使用简单的类映射将 prop 值映射到组件插槽:
type TTheme = "DEFAULT" | "SECONDARY"; interface IComponentSlot { root: string; accent: string; } const THEME_MAP: Record<TTheme, IComponentSlot> = { DEFAULT: { root: "bg-red hover:background-pink", accent: "text-blue hover:text-green", }, SECONDARY: { root: "bg-green hover:background-black", accent: "text-pink hover:text-white", } } <div :class="THEME_MAP['DEFAULT'].root"> <div :class="THEME_MAP['DEFAULT'].accent">/**/</div> </div>
这种方法的问题是保持所需的类相互一致,确保每个变体都具有所有必需的类,特别是在更复杂的组件中。在我们想要跨不同组件插槽共享样式(例如文本颜色)的组件中,需要我们单独更新每个插槽。
Tailwind 通过扫描代码库和匹配字符串来生成实用程序类,这意味着虽然 Tailwind 可以从任意值创建类,但我们无法在不创建安全列表的情况下动态启动它们。所以这行不通:
// .ts type TTheme = "DEFAULT" | "SECONDARY"; const colors: Record<TTheme, string> = { DEFAULT: "red", SECONDARY: "blue", } // .html <div :class="`text-[${colors[DEFAULT]}]`">
但是,我们可以通过利用 CSS 变量来模仿所需的行为,这是 Tailwind 在其许多类的底层使用的东西。我们可以使用以下语法通过 Tailwind 中的类设置变量: [--my-variable-key:--my-variable-value]
那么我们如何更新上面的代码示例以使用动态值?
// .ts type TTheme = "DEFAULT" | "SECONDARY"; const colors: Record<TTheme, string> = { DEFAULT: "[--text-color:red]", SECONDARY: "[--text-color:blue]", } // .html <div :class="[ colors[DEFAULT], 'text-[--text-color]' ]">
现在我们了解了 Tailwind 的局限性,我们需要研究解决由类映射方法引起的初始问题的方法。我们可以从简化我们的类映射开始:
type TTheme = "DEFAULT" | "SECONDARY"; interface IComponentSlot { root: string; accent: string; } const THEME_MAP: Record<TTheme, string> = { DEFAULT: "[--backgound:red] [--hover__background:pink] [--text:blue] [--hover__text:green]", SECONDARY: "[--backgound:green] [--hover__background:black] [--text:pink] [--hover__text:white]", } <div> <p>Unfortunately, this alone doesn't solve our problem, we still can't ensure we've set all of the classes we need to display each variant correctly. So how can we take this a step further? Well, we could begin writing an interface to force us to set specified values:<br> </p> <pre class="brush:php;toolbar:false">interface IComponentThemeVariables { backgound: string; hover__backgound: string; text: string; hover__text: string; } const THEME_MAP: Record<TTheme, IComponentThemeVariables> = { DEFAULT: { backgound: "[--backgound:red]", text: "[--hover__background:pink]", hover__background: "[--text:blue]", hover__text:"[--hover__text:green]", }, SECONDARY: { backgound: "[--backgound:green]", text: "[--hover__background:black]", hover__background: "[--text:pink]", hover__text:"[--hover__text:white]", }, }
所以这可行,但是,仍然有一个问题,没有什么可以阻止我们混合字符串值。例如,我们可能不小心将关键背景设置为 [--text:blue]。
所以也许我们也应该输入我们的值。我们无法输入整个类,这将是维护的噩梦,那么如果我们输入颜色并编写一个辅助方法来生成 CSS 变量会怎么样:
type TColor = "red" | "pink" | "blue" | "green" | "black" | "white"; interface IComponentThemeVariables { backgound: TColor; hover__backgound: TColor; text: TColor; hover__text: TColor; } // Example variableMap method at the end of the article const THEME_MAP: Record<TTheme, string> = { DEFAULT: variableMap({ backgound: "red", text: "pink", hover__background: "blue", hover__text:"green", }), SECONDARY: variableMap({ backgound: "green", text: "black", hover__background: "pink", hover__text:"white", }), }
好的,这太棒了,我们可以确保始终为组件的每个变体设置正确的变量。但是等等,我们刚刚遇到了 Tailwind 发现的最初问题,我们不能只生成类,Tailwind 不会选择它们。那么我们该如何解决这个问题呢?
JS 中的 CSS 似乎是显而易见的答案,只需生成一个类,该类创建具有正确变量的自定义类。但有一个障碍,Javascript 在客户端上运行,这会导致“Flash”,组件最初加载时没有设置变量,然后更新才能正确显示。
像 Emotion 这样的库通过插入有关组件的内联样式标签来处理这个问题:
<body> <div> <style data-emotion-css="21cs4">.css-21cs4 { font-size: 12 }</style> <div> <p>This didn't feel like the right approach to me.</p> <h3> So how do we solve this? </h3> <p>I was working with Vue, this led me down the path of v-bind in CSS, a feature in Vue to bind Javascript as CSS values. I'd only used this feature sparingly in the past and never taken a deep dive into what it's doing. v-bind in CSS simply sets an inline style on the relevant element.</p> <p>This jogged my memory about a Tweet I saw from the creator of Tailwind CSS, Adam Wathan a couple of months previously:</p> <p>So how does this help us? Well, while we can't dynamically generate Tailwind classes, we can dynamically generate inline styles and consume those inline styles from our Tailwind classes. So what would that look like?<br> </p> <pre class="brush:php;toolbar:false">type TColor = "red" | "pink" | "blue" | "green" | "black" | "white"; interface IComponentThemeVariables { backgound: TColor; hover__backgound: TColor; text: TColor; hover__text: TColor; } // Example variableMap method at the end of the article const THEME_MAP: Record<TTheme, string> = { DEFAULT: variableMap({ backgound: "red", text: "pink", hover__background: "blue", hover__text: "green", }), SECONDARY: variableMap({ backgound: "green", text: "black", hover__background: "pink", hover__text: "white", }), } <div > <h2> Conclusion </h2> <p>By combining the powers of Typescript, CSS variables, and inline styles we were able to ensure that while using Tailwind CSS, each variant of our component would have every option set and with the correct type.</p> <p>This is an experimental approach on which I'm sure there will be some strong opinions. Am I convinced this is the best approach? At this stage, I'm not sure, but I think it has legs.</p> <p>If you've found this article interesting or useful, please follow me on Bluesky (I'm most active here), Medium, Dev and/ or Twitter.</p> <h3> Example: variableMap </h3> <pre class="brush:php;toolbar:false">// variableMap example export const variableMap = <T extends Record<string, string>>( map: T ): string => { const styles: string[] = []; Object.entries(map).forEach(([key, value]) => { const wrappedValue = value.startsWith("--") ? `var(${value})` : value; const variableClass = `--${key}: ${wrappedValue};`; styles.push(variableClass); }); return styles.join(" "); };
以上是使用 Tailwind CSS 编写组件变体的不同方法的详细内容。更多信息请关注PHP中文网其他相关文章!