为什么想换成unocss,除了是我爱学习这一方面因素外,tailwind-css 火蛮久了,但始终是更方便后端开发写前端的方式,不考虑维护的时候,确实写起来快
为什么不直接 tailwind-css,看了文档,当下感觉有些太贯彻 inline style 的写法了,部分论坛看的使用感受也是对它的一长串的,难以团队协作+维护;其次感觉tailwind-css的自定义思路和我不是我想法中的方法,刚好在看的过程中了解到了unocss,反正 unocss 也是来源于tailwind-css 的灵感,但自定义的方式相对符合我的想法
同样,又用博客做为学习并重构样式的机会。我遇到了下面的纠结
问题1:原子化CSS 与 CSS Module
先看常见的用法
tailwind-css 的示例 有种 CSS 里面找结构的感觉
<div class="mx-auto max-w-md overflow-hidden rounded-xl bg-white shadow-md md:max-w-2xl">
<div class="md:flex">
<div class="md:shrink-0">
<img
class="h-48 w-full object-cover md:h-full md:w-48"
src="/img/building.jpg"
alt="Modern building architecture"
/>
</div>
<div class="p-8">
<div class="text-sm font-semibold tracking-wide text-indigo-500 uppercase">Company retreats</div>
<a href="#" class="mt-1 block text-lg leading-tight font-medium text-black hover:underline">
Incredible accommodation for your team
</a>
<p class="mt-2 text-gray-500">
Looking to take your team away on a retreat to enjoy awesome food and take in some sunshine? We have a list of
places to do just that.
</p>
</div>
</div>
</div>
原子化CSS在对一个元素既有暗黑模式、响应式、多种交互情况下,写在一堆简直眼晕到爆炸,有种写完就最好别反工
不然你会有种我在哪?这™️还是国内吗?的感觉。但如果用 CSS Module 后,unocss 还有什么意义,除了一些在交互性不强的地方写写?
解决:指令转换器
我最后决定用 unocss 的指令转换器,让项目整体以原子化CSS样式,贯彻只用一套标准,复杂且需要交互改变多的地方就用 CSS Module 写到样式文件中
<template>
<div class="panel relative z-3">
<div class="panel-title p-m2 justify-between">
<div class="flex items-center gap-s1">
<Icon name="friend" size="18px"></Icon>
大佬们
</div>
<NuxtLink to="/friend_link" class="text-bs auto-text-regular">
更多大佬
</NuxtLink>
</div>
<div class="grid grid-cols-3 place-items-center gap-m2 p-m2">
<template v-if="loading">
<div
v-for="item in 6"
:key="item"
class="skeleton rounded-full size-[60px]"
></div>
</template>
<NuxtLink
v-for="item in friendLinks"
:key="item.id"
target="_blank"
:href="item.attributes.url"
:class="$style.friend"
>
<div :class="$style.avatar">
<img
:src="mediaUrl(item.attributes.avatarMedia.data.attributes.url, 90)"
alt="avatar"
loading="lazy"
/>
</div>
<div :class="$style.name">
{{ item.attributes.name }}
</div>
</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
import type { Strapi4ResponseData } from "@nuxtjs/strapi";
import type { FriendLink } from "~/types";
const friendLinks = ref<Strapi4ResponseData<FriendLink>[]>([]);
const loading = ref(true);
const getFriendLinks = async () => {
try {
loading.value = true;
friendLinks.value = await useFriendLinks();
friendLinks.value.sort(() => (Math.round(Math.random()) >= 1 ? 1 : -1));
friendLinks.value = friendLinks.value.slice(0, 6);
} finally {
loading.value = false;
}
};
onMounted(() => {
getFriendLinks();
});
</script>
<style lang="stylus" module>
.friend{
@apply: "flex items-end mb-m4 relative"
&:hover{
@apply: "z-9"
.avatar{
@apply: "scale-110"
}
.name{
@apply: "max-w-[160px] pr-m3 pl-[38px] rotate-0"
}
}
.avatar{
@apply: "aspect-1 rounded-full size-[60px] outline outline-2 outline-brand dark:outline-brand-DARK auto-bg-bg shadow-md overflow-hidden relative z-2 transition-transform duration-500"
img{
@apply: "object-cover relative "
}
}
.name{
@apply: "absolute-lb-30px_0 z-1 h-[40px] max-w-0 auto-bg-brand text-white text-hs rounded-r-full flex items-center truncate transition-all origin-left-bottom rotate--30 duration-500"
}
}
</style>
问题2:原子化CSS写的也太散了
有些样式真的是一个样式名也真的一个属性在里面,那我直接写原来的 CSS 不就好了?
解决:自定义常用组合样式与制定theme
以前开源过 stylus-shortcut 的一种方式,利用特性将常见的样式属性设置,像
- 设置 position 属性后 一般也会定义 left 与 top 属性
- 设置 flex 属性后 一般也会定义 justify-content 与 align-items 属性
在实现优秀交互设计规范的设计稿时,通常设计师也会设计 字符规范、颜色规范 等 并贯彻整体的设计。
所以通过自定义配置后,可以进一步简化与规范
import {
defineConfig,
presetUno,
presetTypography,
transformerDirectives,
type CSSObject,
} from "unocss";
// 字体规范
let spacing: Record<string, string> = {};
for (let i = 1; i <= 6; i++) {
spacing[`m${i}`] = `${i * 8}px`;
spacing[`l${i}`] = `${i * 16}px`;
}
// 字体规范
let fontSize: Record<string, [string, CSSObject]> = {
dl: ["64px", { "line-height": "78px", "font-weight": 600 }],
dm: ["48px", { "line-height": "58px", "font-weight": 600 }],
ds: ["36px", { "line-height": "48px", "font-weight": 600 }],
hl: ["32px", { "line-height": "40px", "font-weight": 600 }],
hm: ["24px", { "line-height": "32px", "font-weight": 600 }],
hs: ["18px", { "line-height": "28px", "font-weight": 600 }],
bl: ["16px", { "line-height": "24px", "font-weight": 400 }],
bm: ["14px", { "line-height": "20px", "font-weight": 400 }],
bs: ["12px", { "line-height": "18px", "font-weight": 400 }],
};
export default defineConfig({
presets: [
presetUno(), // 官方 tailwind 与 windcss的合集预设配置
presetTypography() // 官方文章样式预设配置
],
transformers: [transformerDirectives()], // 指令转换器
rules: [
[
//定位组合规则
/^(absolute|relative|fixed|sticky)-(lt|rt|lb|rb)_(.*)-(.*)/,
([, posMod, dir, x, y]) => {
let dirEnum: Record<string, Record<string, string>> = {
lt: { left: x, top: y },
rt: { right: x, top: y },
lb: { left: x, bottom: y },
rb: { right: x, bottom: y },
};
return {
position: posMod,
...dirEnum[dir],
};
},
],
[
// flex 组合规则
/^flex-(start|end|center|between|around|evenly)-(start|end|center|stretch)/,
([, justify, items]) => {
let justifyMap: Record<string, string> = {
start: "flex-start",
end: "flex-end",
center: "center",
between: "space-between",
around: "space-around",
evenly: "space-evenly",
};
let alignMap: Record<string, string> = {
start: "flex-start",
end: "flex-end",
center: "center",
stretch: "stretch",
};
return {
display: "flex",
justifyContent: justifyMap[justify],
alignItems: alignMap[items],
};
},
],
],
//#9ecbe8
theme: {
spacing,
fontSize,
container: {
center: true,
padding: "16px",
},
animation: {
// 自定义动效配置
keyframes: {
"fade-in-up":
"{from{opacity:0;transform:translate3d(0,30px,0)}to{opacity:1;transform:translate3d(0,0,0)}}",
rollup: "{from{transform:translateX(0)}to{transform:translateX(-50%)}}",
},
durations: {
"fade-in-up": "400ms",
},
counts: {
rollup: "infinite",
},
},
colors: {
sky: {
DEFAULT: "#fff",
DARK: "#000",
},
bg: {
DEFAULT: "#f2f2f2",
DARK: "#202020",
},
fg: {
DEFAULT: "#fff",
DARK: "#0b0b0b",
},
brand: {
DEFAULT: "#252435",
DARK: "#8a91db",
},
title: {
DEFAULT: "#000",
DARK: "#fff",
},
regular: {
DEFAULT: "#2B394C",
DARK: "#ccc",
},
line: {
DEFAULT: "#efefef",
DARK: "#2c2c2c",
},
},
},
// 设定改变 color mode 时自动变色的便捷样式
shortcuts: [
[
/^auto-bg-(.*)$/,
([, c]) => `bg-${c} dark:bg-${c.replace(/([^\/]+)(.*)/, "$1-DARK$2")}`,
],
[
/^auto-border-(.*)$/,
([, c]) =>
`b-solid border-${c} dark:border-${c.replace(
/([^\/]+)(.*)/,
"$1-DARK$2"
)}`,
],
[
/^auto-text-(.*)$/,
([, c]) =>
`color-${c} dark:color-${c.replace(/([^\/]+)(.*)/, "$1-DARK$2")}`,
],
{
"skeleton-wrap": "pointer-events-none",
skeleton: "animate-pulse bg-gray-200 h-[24px] dark:bg-gray-700 rounded",
},
],
});
presetUno的一个小BUG
- theme中 spacing 不要用s开头
let spacing: Record<string, string> = {};
for (let i = 1; i <= 6; i++) {
spacing[`s${i}`] = `${i * 4}px`;
}
// p-s1 你想要 padding: 4px
// 它会识别成 padding-inline-start: 4px;
最后
UnoCSS 实际使用起来真的还蛮符合我的需求的,社区生态看着也还不错,但配置的文档真的是一言难尽了,很多时候真的是直接看 presetUno 的源码才知道他还可以这样配置。
还好 CSS方案 也不用深入配置到特别的细节。在解决了这些纠结后,确实在开发的过程的,效率高了许多