nextjs初探之TailwindCSS和PostCSS

577 阅读9分钟

在nextjs下选择UI库有必要了解一下Tailwindcss(tailwindcss.com/docs/instal…)。

nextjs一些推荐的UI库都是基于Tailwindcss的。

官网给的例子

普通的css

TailwindCSS

<div class="chat-notification">
  <div class="chat-notification-logo-wrapper">
    <img class="chat-notification-logo" src="/img/logo.svg" alt="ChitChat Logo">
  </div>
  <div class="chat-notification-content">
    <h4 class="chat-notification-title">ChitChat</h4>
    <p class="chat-notification-message">You have a new message!</p>
  </div>
</div>

<style>
  .chat-notification {
    display: flex;
    align-items: center;
    max-width: 24rem;
    margin: 0 auto;
    padding: 1.5rem;
    border-radius: 0.5rem;
    background-color: #fff;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  }
  .chat-notification-logo-wrapper {
    flex-shrink: 0;
  }
  .chat-notification-logo {
    height: 3rem;
    width: 3rem;
  }
  .chat-notification-content {
    margin-left: 1.5rem;
  }
  .chat-notification-title {
    color: #1a202c;
    font-size: 1.25rem;
    line-height: 1.25;
  }
  .chat-notification-message {
    color: #718096;
    font-size: 1rem;
    line-height: 1.5;
  }
</style>
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-lg flex items-center gap-x-4">
  <div class="shrink-0">
    <img class="size-12" src="/img/logo.svg" alt="ChitChat Logo">
  </div>
  <div>
    <div class="text-xl font-medium text-black">ChitChat</div>
    <p class="text-slate-500">You have a new message!</p>
  </div>
</div>



带来的好处

  • 不会浪费精力发明类名
  • CSS 停止增长
  • 做出改变感觉更安全

为什么不直接使用内联样式呢

  • 有约束的设计。使用内联样式,每个值都是一个神奇的数字。使用实用程序,您可以从预定义的设计系统中选择样式,这使得构建视觉上一致的 UI 变得更加容易。
  • 响应式设计
  • 悬停、焦点等状态。内联样式无法针对悬停或焦点等状态,但 Tailwind 的状态变体可以轻松地使用实用程序类来设置这些状态的样式。

缺点(文档没提)

个人有限的认知内觉得:

  • 更多的学习成本,开发需要查阅文档。
  • 在设计系统混乱的时候依旧需要大量的自定义样式来满足UI/UE需求。
  • 平时使用的很多UI库并不是基于Tailwind CSS的,结合Tailwind CSS使用起来并不方便。



假设开始看tailwindcss.com/docs/config… 文档来写样式后,咱们还要了解TailwindCSS是如何生成的样式的。

  • tailwind.config.ts
  • postcss.config.js
  • 全局的样式文件里的
@tailwind base;
@tailwind components;
@tailwind utilities;

PostCSS

  • 不是 SassLess这样的样式预处理器。
  • 是CSS语法转换的工具, 它允许您定义自定义 CSS 之类的语法,这些语法可以被插件理解和转换。
  • 是 CSS 生态系统的重要参与者

Workflow 工作流程

image.png

  1. 写入一个包含字符串到 AST 转换的文件

  2. 将其拆分为词法分析/解析步骤(源字符串 → 标记 → AST)

    1. 这是我们在 PostCSS 中的做法,也是最流行的一种。很多解析器如@babel/parser (Babel 背后的解析器)CSSTree都是这样编写的。将标记化与解析步骤分开的主要原因是性能和抽象复杂性。
    2. 首先,因为字符串到标记步骤比解析步骤花费更多时间。我们对大型源字符串进行操作并逐个字符地处理它,这就是为什么它在性能方面是非常低效的操作,我们应该只执行一次。
    3. 但从 token 到 AST 的转换在逻辑上更加复杂,因此通过这种分离,我们可以编写非常快的 tokenizer(但有时难以阅读代码)和易于阅读(但速度慢)的解析器。

分词器lib/tokenize.js

它接受 CSS 字符串并返回标记列表。

.className { color: #FFF; }

PostCSS 的相应标记将是

[    ["word", ".className", 1, 1, 1, 10]
    ["space", " "]
    ["{", "{", 1, 12]
    ["space", " "]
    ["word", "color", 1, 14, 1, 18]
    [":", ":", 1, 19]
    ["space", " "]
    ["word", "#FFF" , 1, 21, 1, 23]
    [";", ";", 1, 24]
    ["space", " "]
    ["}", "}", 1, 26]
]

让我们更仔细地看看像word这样的单个标记。正如所说,每个token都代表一个列表并遵循这种模式。

const token = [
     // represents token type
    'word',

    // represents matched word
    '.className',

    // This two numbers represent start position of token.
    // It is optional value as we saw in the example above,
    // tokens like `space` don't have such information.

    // Here the first number is line number and the second one is corresponding column.
    1, 1,

    // Next two numbers also optional and represent end position for multichar tokens like this one. Numbers follow same rule as was described above
    1, 10
]

解析器lib/parse.js , lib/parser.js

解析器生成一个称为抽象语法树(AST)的结构,稍后可以通过插件对其进行转换。

处理器lib/processor.js

处理器是一个非常简单的结构,用于初始化插件并运行语法转换

Stringifier lib/stringify.js , lib/stringifier.js

Stringifier 是一个基类,它将修改后的 AST 转换为纯 CSS 字符串。 Stringifier 从提供的 Node 开始遍历 AST,并调用相应的方法生成它的原始字符串表示形式。

PostCSS插件

TailwindCSS本质上是一个PostCSS插件,这里看下plugin.

修改PostCSS给我们的token.

const plugin = () => {
  return {
    postcssPlugin: 'to-red',
    Rule (rule) {
      console.log(rule.toString())
    },
    Declaration (decl) {
      console.log(decl.toString())
      decl.value = 'red'
    }
  }
}
plugin.postcss = true

await postcss([plugin]).process('a { color: black }', { from })
// => a { color: black }
// => color: black
// => a { color: red }
// => color: red

postcss.config.js

/** @type {import('postcss-load-config').Config} */
module.exports = {
  plugins: {
    'tailwindcss/nesting': {},
    'tailwindcss': {},
    'autoprefixer': {},
  },
};
  • /** @type {import('postcss-load-config').Config} */

类型注释:使用 TypeScript 提供类型检查,帮助开发者在 IDE 中获得更好的代码补全和提示。

导入三个插件

  • tailwindcss/nesting

    • 支持 CSS 嵌套规则,类似 SCSS 中的嵌套语法。
    • 将嵌套的 CSS 转换为标准的平铺样式,便于浏览器解析。
.btn {
  color: white;

  &:hover {
    color: blue;
  }
}
// 转换后
.btn {
  color: white;
}
.btn:hover {
  color: blue;
}

  • tailwindcss

    • 核心插件,负责解析 Tailwind CSS 的类名(如 text-center, bg-red-500)并生成相应的 CSS 样式。

      • 扫描配置文件tailwind.config.js 中的配置,获取颜色、字体、间距等设计系统的自定义信息
    • 提取类名:从 content 指定的文件中提取所有使用的 Tailwind 类名

    • 生成 CSS

    • 未被使用的类名会被剔除,生成的 CSS 文件体积更小。

执行流程

  1. tailwindcss/nesting:先处理嵌套规则,生成平铺的 CSS。
  2. tailwindcss:解析 Tailwind 的类名,并生成对应的 CSS。
  3. autoprefixer:最后处理生成的 CSS,为需要的属性添加浏览器前缀。

工作整体流程:

@tailwind base;
@tailwind components;
@tailwind utilities;

.btn {
  @apply bg-blue-500 text-white px-4 py-2;

  &:hover {
    background-color: blue;
  }
}

  1. tailwindcss/nesting
.btn {
  @apply bg-blue-500 text-white px-4 py-2;
}
.btn:hover {
  background-color: blue;
}

  1. tailwindcss
.bg-blue-500 {
  background-color: #3b82f6;
}
.text-white {
  color: #ffffff;
}
.px-4 {
  padding-left: 1rem;
  padding-right: 1rem;
}
.py-2 {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
.btn {
  background-color: #3b82f6;
  color: #ffffff;
  padding-left: 1rem;
  padding-right: 1rem;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
.btn:hover {
  background-color: blue;
}
  1. 添加浏览器前缀
.btn:hover {
  -webkit-background-color: blue;
  background-color: blue;
}

小结:TailwindCSS作为一个PostCss的插件,将TailwindCSS定义的样式输出

tailwindcss.config.js

import type { Config } from 'tailwindcss';
// import { fontFamily } from 'tailwindcss/defaultTheme';

const config = {
  darkMode: 'class',
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}', // Note the addition of the `app` directory.
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    // Or if using `src` directory:
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  prefix: '',
  theme: {
    container: {
      center: true,
      padding: '2rem',
      screens: {
        '2xl': '1400px',
      },
    },
    extend: {
      fontFamily: {
        inter: ['var(--font-inter)'],
        lexend: ['var(--font-lexend)'],
        lexendBold: ['var(--font-lexend-bold)'],
      },
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
      colors: {
        border: 'hsl(var(--border))',
        input: 'hsl(var(--input))',
        ring: 'hsl(var(--ring))',
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        primary: {
          DEFAULT: 'hsl(var(--primary))',
          foreground: 'hsl(var(--primary-foreground))',
        },
      },
      borderRadius: {
        lg: 'var(--radius)',
        md: 'calc(var(--radius) - 2px)',
        sm: 'calc(var(--radius) - 4px)',
      },
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
      typography: {
        DEFAULT: {
          css: {
            'h2, h3, h4, h5, ul, ol': {
              'margin-top': '1em',
              'margin-bottom': '0.6em',
            },
          },
        },
      },
    },
  },
  plugins: [
    require('tailwindcss-animate'),
    require('@tailwindcss/typography'),
    require('daisyui'),
  ],
  variants: {
    extend: {
      display: ['group-hover'],
    },
  },
} satisfies Config;

export default config;


darkMode: 'class'

  • 作用:启用暗黑模式,基于类名切换(class)。

    • 默认情况下,Tailwind 支持的值为:

      • 'media':根据系统偏好自动切换。
      • 'class':需要手动添加 dark 类名来启用暗黑模式

content

content: [
  './app/**/*.{js,ts,jsx,tsx,mdx}',
  './pages/**/*.{js,ts,jsx,tsx,mdx}',
  './components/**/*.{js,ts,jsx,tsx,mdx}',
  './src/**/*.{js,ts,jsx,tsx,mdx}',
],


指定 Tailwind 的内容扫描范围,告诉其从这些文件中提取使用的类名,以便生成对应的 CSS。

prefix

设置 Tailwind 的类名前缀

若有冲突,可自定义前缀。例如

prefix: 'tw-',

使用时需写成 tw-bg-red-500

theme.container

container: {
  center: true,
  padding: '2rem',
  screens: {
    '2xl': '1400px',
  },
},

作用:配置 Tailwind 的 .container 类。

  • center: true:让容器居中对齐。
  • padding: '2rem':为容器内内容添加 2rem 的内边距。
  • screens:设置不同屏幕大小下容器的宽度,示例中限制了 2xl 的最大宽度为 1400px。

plugins

plugins: [
  require('tailwindcss-animate'),
  require('@tailwindcss/typography'),
  require('daisyui'),
],

作用:添加 Tailwind 的插件扩展。

  • tailwindcss-animate:提供预定义的动画类。
  • @tailwindcss/typography:为富文本内容(如 Markdown)提供默认样式。
  • daisyui:一个组件库,提供丰富的 Tailwind 风格化组件。



一些基于Tailwindcss的UI库也会往这个配置文件输入内容,比如ui.shadcn.com/docs/compon…

比如安装accordion

pnpm dlx shadcn@latest add accordion

1.会把accordion组件的源代码输入到components/ui文件夹下

"use client"

import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"

import { cn } from "~/lib/utils"

const Accordion = AccordionPrimitive.Root

const AccordionItem = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
  <AccordionPrimitive.Item
    ref={ref}
    className={cn("border-b", className)}
    {...props}
  />
))
AccordionItem.displayName = "AccordionItem"

const AccordionTrigger = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <AccordionPrimitive.Header className="flex">
    <AccordionPrimitive.Trigger
      ref={ref}
      className={cn(
        "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
        className
      )}
      {...props}
    >
      {children}
      <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
    </AccordionPrimitive.Trigger>
  </AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName

const AccordionContent = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <AccordionPrimitive.Content
    ref={ref}
    className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
    {...props}
  >
    <div className={cn("pb-4 pt-0", className)}>{children}</div>
  </AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }


2.会把tailwindcss需要的样式输入到 tailwindcss.config.js


      keyframes: {
        'accordion-down': {
          from: {
            height: '0',
          },
          to: {
            height: 'var(--radix-accordion-content-height)',
          },
        },
        'accordion-up': {
          from: {
            height: 'var(--radix-accordion-content-height)',
          },
          to: {
            height: '0',
          },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },

global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

最后看看这三句

@tailwind 是 Tailwind CSS 提供的内置指令,用于在样式表中插入 Tailwind 的预定义样式。

@tailwind base;

  • 作用:插入 Tailwind 的全局基础样式(Base Styles)。

    • 包括现代浏览器的 CSS reset(即消除浏览器默认样式的影响)。
    • 提供一致的基础样式和排版规则(如字体大小、行高等)。

@tailwind components;

  • 作用:插入 Tailwind 的预定义组件样式(Component Styles)。

    • 提供可复用的、半抽象的样式(例如按钮样式、表单样式等)。
    • 这些样式的复杂度介于基础样式和工具类之间。

@tailwind utilities;

  • 作用:插入 Tailwind 的工具类样式(Utility Classes)。

    • 工具类 是 Tailwind 的核心,例如 text-centerbg-red-500flex 等。
    • 这些类的职责单一,通常只修改某个具体的 CSS 属性值。



总结

TailwindCSS作为一个PostCSS的插件,使用PostCss生成css的能力:将className对应的样式生成为css,生成的过程中提供配置的能力。

TailwindCSS内置了自己的原子样式,会把这些样式输出。

提供了配置的能力,自定义样式。

具体的配置的能力平时按照文档查阅就行。