Next.js 项目写 Tailwind CSS 基本都会遇到的两个问题

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

目前 Tailwind CSS 在 GitHub 有 80k Stars、Npm 周下载量 733W,已经成为前端主流的 CSS 框架。

而 Next.js 脚手架默认集成 Tailwind CSS,创建项目后便可直接使用 Tailwind CSS。

Tailwind CSS 看似使用简单,其实也有一些"门道"在其中。本篇我们就来聊聊 Next.js 项目写 Tailwind CSS 时会遇到的一些问题以及如何解决。

最后我会分享一些帮助大家写 Tailwind CSS 的网站和工具,希望能大幅提高大家的写代码效率。

  1. 本篇已收录到掘金专栏《Next.js 开发指北》

  2. 系统学习 Next.js,欢迎入手小册《Next.js 开发指南》。基础篇、实战篇、源码篇、面试篇四大篇章带你系统掌握 Next.js!

初始化项目

为了方便演示,我们创建一个空的 Next.js 项目:

bash 复制代码
npx create-next-app@latest

注意勾选 Tailwind CSS、App Router,其他选项选项随意:

问题 1:动态类名问题

1. 问题复现

修改 app/page.js,代码如下:

jsx 复制代码
'use client'

export default function Home() {
  return (
    <button type="submit" className="bg-indigo-600 disabled:bg-gray-500 py-2 px-4 rounded text-white w-1/2 m-2" >
      提交
    </button>
  );
}

代码正常,此时浏览器效果如下:

现在我们将 app/page.js 修改为:

jsx 复制代码
'use client'
import { useState } from "react";

export default function Home() {
  const [color, setColor] = useState("indigo");

  return (
    <button type="submit" className={`bg-${color}-600 disabled:bg-gray-500 py-2 px-4 rounded text-white w-1/2 m-2`} >
      提交
    </button>
  );
}

注意:这里需要重新运行 npm run dev,或者将颜色改为其他颜色,比如 blue

初学者可能会以为代码依然正常,按钮颜色会与之前一样,但其实浏览器效果如下:

按钮元素虽然还在,类名里也有 bg-indigo-600,但是样式表里并没有 bg-indigo-600 的样式代码。因为没有设置背景颜色,且文字为白色,所以页面显得"一片空白"。

这是为什么呢?

2. 原因解释

首先,根据这个辅助书写 Tailwind CSS 的网站介绍,Tailwind CSS 有 37080 个工具类名,如果全部打包到样式表中,CSS 文件会很大,所以将全部类名打包到样式表并不现实。

更为实际的做法是提取出项目中用到的类名,所以 Tailwind CSS 的配置文件 tailwind.config.js 有一个 content 选项,就是用来配置所有 HTML 模板、JavaScript 组件以及包含 Tailwind 类名的任何其他源文件的路径的位置:

那 Tailwind CSS 是怎么匹配提取的呢?其实非常简单,直接扫描源码,使用正则表达式来提取可能是类名的每个字符串。

换句话说,不管你是不是写在了 class 中,源码中只要出现了,那就算!

假如你是这样写的:

jsx 复制代码
'use client'
import { useState } from "react";
// 注意这里
const temp = "bg-indigo-600";

export default function Home() {
  const [color, setColor] = useState("indigo");
  return (
    <button type="submit" className={`bg-${color}-600 disabled:bg-gray-500 py-2 px-4 rounded text-white w-1/2 m-2`} >
      提交
    </button>
  );
}

尽管 temp 变量你都没用到,但这样写是可以的,bg-indigo-600 会打包到样式表中,按钮就成功设置了颜色:

再假如你是这样写的:

jsx 复制代码
'use client'
import { useState } from "react";

export default function Home() {
  const [color, setColor] = useState("indigo");
  return (
    <button type="submit" className={`bg-${color}-600 disabled:bg-gray-500 py-2 px-4 rounded text-white w-1/2 m-2`} >
      提交 bg-indigo-600
    </button>
  );
}

尽管 bg-indigo-600 是写在了按钮文字上,但这样写也是可以的,bg-indigo-600 同样会打包到样式表中:

3. 如何解决

所以写 Tailwind CSS 类名的时候,不能动态构建类名:

javascript 复制代码
// 这样写是错误的
<div class="text-{{ error ? 'red' : 'green' }}-600"></div>

需要保证类名完整存在:

javascript 复制代码
// 这样写是可以的
<div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>

或者这样写:

jsx 复制代码
// 这样写也是可以的
function Button({ color, children }) {
  const colorVariants = {
    blue: 'bg-blue-600 hover:bg-blue-500 text-white',
    red: 'bg-red-500 hover:bg-red-400 text-white',
    yellow: 'bg-yellow-300 hover:bg-yellow-400 text-black',
  }

  return (
    <button className={`${colorVariants[color]} ...`}>
      {children}
    </button>
  )
}

如果前面的方法都不行,也有一个兜底方案。tailwind.config.js 中有 safelist配置项:

javascript 复制代码
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{html,js}',
    './components/**/*.{html,js}',
  ],
  safelist: [
    'bg-indigo-600'
  ]
  // ...
}

配置在 safelist 中的类名会被打包到样式文件中。

tailwind.config.js 中也有 blocklist配置项:

javascript 复制代码
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{html,js}',
    './components/**/*.{html,js}',
  ],
  blocklist: [
    'container',
    'collapse',
  ],
  // ...
}

blocklist 中的类名不会被打包到样式文件中。比如文章中的文字包含了 container,Tailwind CSS 就会打包 container 类名,但其实没有需要,或者你自定义了自己的 container 类名,不希望使用 Tailwind CSS 的 container 类名,那此时就可以配置 blocklist

问题 2:类名优先级问题

1. 问题复现

修改 app/page.js,代码如下:

jsx 复制代码
function Button({className}) {
  return (
    <button type="submit" className={`bg-red-600 w-1/2 p-2 m-2 rounded text-white ${className}`} >
      提交
    </button>
  )
}

export default function Home() {
  return (
    <Button className="bg-blue-600" />
  );
}

因为我们用到了两个背景颜色类名,它们会发生冲突,但最终按钮的颜色是什么颜色呢?

答案是红色:

虽然 className 变量声明在了后面,我们理所当然的会希望后者会覆盖前者,但其实不会。

2. 原因解释

这是因为 HTML 元素的类名书写顺序并不影响类的优先级,类的优先级取决于样式文件中出现的先后顺序,越晚出现,优先级越高。

所以按钮最后是什么颜色,取决于 Tailwind CSS 生成的样式表中的文件的类名先后顺序,但这是不可控的,这就可能会造成错误。

3. 如何解决

3.1. tailwind-merge

所以需要 tailwind-merge,它可以解决样式冲突问题。安装 tailwind-merge:

javascript 复制代码
npm i tailwind-merge

修改 app/page.js,代码如下:

jsx 复制代码
import { twMerge } from 'tailwind-merge'

function Button({className}) {
  return (
    <button type="submit" className={twMerge("bg-red-60 w-1/2 p-2 m-2 rounded text-white", className)} >
      提交
    </button>
  )
}

export default function Home() {
  return (
    <Button className="bg-blue-600" />
  );
}

twMerge() 函数支持传入多个参数,如果发生冲突,后传入的类名优先级更高,会覆盖之前的类名。

此时按钮会如期变成蓝色:

查看按钮元素的类名,你会发现当发生冲突的时候,并没有 bg-red-600 类名,表明 tailwind-merge 根据先后顺序做了优先级处理。

3.2. clsx

现在让我们再看一个常会遇到的问题 ------ 条件语句。

jsx 复制代码
'use client'

import { useState } from 'react';
import { twMerge } from 'tailwind-merge'

function Button({className}) {
  const [submiting, setSubmit] = useState(false)
  return (
    <button type="submit" className={twMerge("bg-red-60 w-1/2 p-2 m-2 rounded text-white", className, submiting && 'bg-amber-600')} onClick={() => {
      setSubmit(true)
    }}>
      提交
    </button>
  )
}

export default function Home() {
  return (
    <Button className="bg-blue-600" />
  );
}

这段代码运行并没有什么问题,按钮本身是蓝色,点击的时候会变成黄色:

麻烦的地方在于我们是这样写样式的:

javascript 复制代码
twMerge("bg-red-600 rounded text-white w-1/2 p-2 m-2", submiting && 'bg-blue-600')

如果只有一个状态倒还好,如果有多个状态呢?难道就不能这样写吗?

javascript 复制代码
twMerge("bg-red-600 rounded text-white w-1/2 p-2 m-2", {
  "bg-blue-600": submiting,
  "text-white": loading,
  "border border-black": disabled
  // ...
})

twMerge 并不支持这样写,但是 clsx 支持!(实际上 clsx 比 twMerge 出现的更早、用的人更多),于是就有人想到这样混合使用:

运行:

bash 复制代码
npm i clsx

新建 app/page.js,代码如下:

jsx 复制代码
'use client'

import { useState } from 'react';
import { twMerge } from 'tailwind-merge'
import { clsx } from "clsx"

function cn(...inputs) {
  return twMerge(clsx(inputs))
}

function Button({className}) {
  const [submiting, setSubmit] = useState(false)
  return (
    <button type="submit" className={cn("bg-red-60 w-1/2 p-2 m-2 rounded text-white", className, {
      "bg-amber-600": submiting
    })} onClick={() => {
      setSubmit(true)
    }}>
      提交
    </button>
  )
}

export default function Home() {
  return (
    <Button className="bg-blue-600" />
  );
}

如果你用过 Shadcn UI,在 Next.js 项目中运行 npx shadcn-ui@latest init的时候,会创建一个 lib/utils.js文件,这个文件中只有一个工具函数,这个函数就是 cn

javascript 复制代码
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs) {
  return twMerge(clsx(inputs))
}

实际上,这是一个非常实用的处理 Tailwind CSS 类名的函数,我们日常也需要用到。

3.3. cva

cn 函数已经可以解决很多问题了,但当项目变得复杂,尤其是要处理组件的多种样式的时候,cn 就显得有些不够用了......

我们以 Ant-Design 的 Button 组件为例,一个 Button 有大、中、小三种尺寸,有五种类型:主按钮、次按钮、虚线按钮、文本按钮和链接按钮:

如果我们的项目中写这种组件,代码很可能会变成这样:

jsx 复制代码
"use client";

import { twMerge } from "tailwind-merge";
import { clsx } from "clsx";

function cn(...inputs) {
  return twMerge(clsx(inputs));
}

function Button({ type = "default", size = "middle" }) {
  return (
    <button
      type="submit"
      className={cn("rounded p-2", {
        "bg-blue-600 text-white": type === "default",
        "border border-black bg-white text-black": type === "primary",
        "border border-dashed border-black bg-white": type === "dashed",
        "text-blue-600": type === "link",
        "text-black": type === "text",
        "px-2 py-2": size === "small",
        "px-4 py-2": size === "middle",
        "px-6 py-2": size === "large",
      })}
      >
      Default Button
    </button>
  );
}

export default function Home() {
  return (
    <div className="p-4">
      <Button />
    </div>
  );
}

可以看到,代码并不直观,且随着样式越来越多,className 的代码会变得臃肿难以维护。这个时候就需要 cva(Class Variance Authority)了。

安装依赖项:

bash 复制代码
npm i class-variance-authority

修改 app/page.js

jsx 复制代码
"use client";

import { twMerge } from "tailwind-merge";
import { clsx } from "clsx";
import { cva } from "class-variance-authority";

function cn(...inputs) {
  return twMerge(clsx(inputs));
}

const button = cva("rounded p-2", {
  variants: {
    intent: {
      default: ["bg-blue-600", "text-white"],
      primary: ["border", "border-black", "bg-white", "text-black"],
      dashed: ["border", "border-dashed", "border-black", "bg-white"],
      link: ["text-blue-600"],
      text: ["text-black"],
    },
    size: {
      small: ["px-2", "py-2"],
      middle: ["px-4", "py-2"],
      large: ["px-6", "py-2"],
    },
  },
  defaultVariants: {
    intent: "default",
    size: "middle",
  },
});

function Button({ type, size }) {
  return (
    <button type="submit" className={button(type, size)}>
      Default Button
    </button>
  );
}

export default function Home() {
  return (
    <div className="p-4">
      <Button />
    </div>
  );
}

在这段代码中,我们借助 cva 声明了组件的不同变体(variants),并且通过 defaultVariants 设置了默认变体,最后调用 button(type, size),cva 就会算出最终的 className。

浏览器效果同之前:

网站和工具

最后我们聊聊写 Tailwind CSS 时会用到的一些网站和工具,希望对大家书写 Tailwind CSS 代码有帮助。

1. 辅助网站

Tailwind CSS 工具类名众多,如果你经常忘记怎么写,可以在这两个网站搜索查看:

  1. tailwindcomponents.com/cheatsheet/
  2. tailwind.spacet.me/

如果你需要将 CSS 转换成 Tailwind CSS:

  1. www.divmagic.com/zh-CN/tools...

2. VSCode 插件

如果你使用 VScode,这有一些不错的插件可以使用:

2.1. Tailwind CSS IntelliSense

这是 Tailwind CSS 官方提供的插件,可用于自动补全、Lint、悬浮预览等:

2.2. Tailwind DocumentationTailwind Docs

这两个都是帮助你快速查询文档的插件,主要区别在于 Tailwind Documentation 在编辑器打开,Tailwind Docs 在浏览器打开。

使用 Tailwind Documentation:

使用 Tailwind Docs:

2.3. Tailwind Fold

是不是感觉 Tailwind CSS 总是写的太长,影响你看代码了?这个插件帮你折叠代码!

2.4. prettier 排序插件

Tailwind CSS 有一个建议的排序顺序,比如首先是基础层(base layer)中的类名,然后是组件层中的类名,再然后是工具层中的类名,又比如高影响的类名如布局放在前面,装饰类的放在后面,再比如 hover、focus 这种放在普通工具类名的后面等等。

当然你不需要自己手动去排序,Tailwind CSS 提供了 prettier-plugin-tailwindcss 这个插件来实现自动排序。

安装依赖项:

javascript 复制代码
npm install -D prettier prettier-plugin-tailwindcss

根目录新建 .prettierrc

javascript 复制代码
{
  "plugins": ["prettier-plugin-tailwindcss"]
}

如果 VScode 安装了 Prettier 插件,使用 Prettier 格式化代码的时候,就会将 Tailwind CSS 类名重新排序:

注:上图中是配置了保存时自动使用 Prettier 格式化,settings.json 中配置:

javascript 复制代码
{
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true,
}

参考链接

  1. tailwindcss.com/docs/conten...
  2. tailwindcss.com/blog/automa...
  3. www.youtube.com/watch?v=re2...
  4. www.youtube.com/watch?v=guh...
相关推荐
旧味清欢|9 分钟前
关注分离(Separation of Concerns)在前端开发中的实践演进:从 XMLHttpRequest 到 Fetch API
javascript·http·es6
热爱编程的小曾26 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin37 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
-代号95271 小时前
【JavaScript】十四、轮播图
javascript·css·css3
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
QTX187302 小时前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员