普通视图

发现新文章,点击刷新页面。
昨天以前首页

微前端入门:qiankun + Vue 3 + Vite 从0搭建第一个微前端应用

作者 CodeAI
2026年4月16日 07:28

前言

随着业务规模不断扩大,前端应用体积越来越大,单一巨石应用的开发维护成本越来越高:

  • 多人协作开发,代码冲突频繁
  • 整体编译打包时间越来越长
  • 技术栈陈旧,无法引入新技术
  • 发布一个小改动需要整个应用重新上线

微前端架构应运而生,通过将巨石应用拆分为多个独立可交付的微应用,实现团队自治、独立开发、独立部署,从架构层面解决这些问题。

目前社区中比较成熟的微前端方案里,qiankun 是蚂蚁集团开源的企业级方案,经过大量业务验证,生态成熟,api 友好,是落地微前端最稳妥的选择。

本文带你一步步从 0 搭建第一个 qiankun 微前端应用,基于最新的 Vue 3 + Vite 技术栈,解决了网上大部分教程配置错误的问题。读完本文你就能跑通一个可工作的微前端应用。

技术选型

层级 选型 理由
微前端框架 qiankun 成熟稳定,社区案例多,坑少
前端框架 Vue 3 主流稳定,性能优秀,生态完善
构建工具 Vite 开发启动快,热更新体验好,是当前主流趋势
路由 Vue Router 4 官方标准,配合 qiankun 路由联动方案成熟

整体架构

graph TD
    A[主应用/MainApp] --> B[微应用1/App1]
    A --> C[微应用2/App2]
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:1px
    style C fill:#bbf,stroke:#333,stroke-width:1px
  • 主应用(基座): 负责微应用注册与生命周期管理、全局导航布局、公共依赖加载。
  • 微应用: 各个业务模块独立开发、独立运行、独立部署。
  • 最终效果:用户在浏览器中切换不同业务模块,感觉就像在同一个应用里,实际上每个模块都是独立的。

项目初始化

我们需要创建三个项目:一个主应用,两个微应用。

# 创建主应用
npm create vite@latest main-app -- --template vue

# 创建微应用一
npm create vite@latest micro-app-vue1 -- --template vue

# 创建微应用二
npm create vite@latest micro-app-vue2 -- --template vue

最终目录结构:

micro-front/
├── main-app/          # 主应用(基座)
├── micro-app-vue1/    # 微应用1
├── micro-app-vue2/    # 微应用2
└── README.md

本文档对应的完整代码已经开源在 github.com/wenbiyou/mi…,你可以直接克隆运行。

第一步:主应用配置 qiankun

安装依赖

cd main-app
npm install qiankun vue-router@4

qiankun 注册与启动

修改 src/main.js

import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { registerMicroApps, start } from 'qiankun'
import App from './App.vue'

const app = createApp(App)
const router = createRouter({
  history: createWebHistory('/'),
  routes: [
    { path: '/', redirect: '/app1' },
    { path: '/:pathMatch(.*)*', component: () => import('./views/NotFound.vue') }
  ]
})

app.use(router)
app.mount('#app')

// 微应用配置
const microApps = [
  {
    name: 'micro-app-vue1',
    entry: '//localhost:7100', // 开发环境入口
    activeRule: '/app1', // 激活规则:路径以 /app1 开头时激活
    container: '#micro-container' // 挂载容器
  },
  {
    name: 'micro-app-vue2',
    entry: '//localhost:7101',
    activeRule: '/app2',
    container: '#micro-container'
  }
]

// 注册微应用
registerMicroApps(microApps, {
  beforeLoad: [app => console.log('before load', app.name)],
  beforeMount: [app => console.log('before mount', app.name)],
  afterMount: [app => console.log('after mount', app.name)],
  afterUnmount: [app => console.log('after unmount', app.name)]
})

// 启动 qiankun
start({
  sandbox: {
    strictStyleIsolation: false,
    experimentalStyleIsolation: true // 开启实验性样式隔离,兼容性更佳
  },
  prefetch: 'all' // 预加载微应用静态资源
})

添加主应用布局

修改 src/App.vue

<template>
  <div id="main-app">
    <header class="main-header">
      <div class="logo">
        <h1>微前端主应用</h1>
      </div>
      <nav class="nav">
        <router-link to="/app1">应用一</router-link>
        <router-link to="/app2">应用二</router-link>
      </nav>
    </header>
    <!-- qiankun 挂载容器 -->
    <div id="micro-container" class="micro-container"></div>
  </div>
</template>

<style scoped>
.main-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 20px;
  background: #2c3e50;
  color: white;
}
.nav {
  display: flex;
  gap: 20px;
}
.nav a {
  color: white;
  text-decoration: none;
}
.nav a.router-link-exact-active {
  color: #42b983;
  font-weight: bold;
}
.micro-container {
  padding: 20px;
}
</style>

Vite 配置

修改 vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 7099, // 主应用端口,避免和微应用冲突
    cors: true // 开启跨域,允许加载微应用资源
  }
})

主应用配置完成!接下来配置微应用。

第二步:微应用适配 qiankun

micro-app-vue1 为例,micro-app-vue2 只需要修改端口和名称即可。

安装依赖

cd micro-app-vue1
npm install vite-plugin-qiankun

修改入口文件适配生命周期

修改 src/main.js

import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/es/helper'
import App from './App.vue'
import './style.css'

let app = null

function render(props = {}) {
  const { container } = props
  const mountNode = container ? container.querySelector('#app') : '#app'

  // 每次渲染新建 router,避免状态污染
  const router = createRouter({
    history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/app1' : '/'),
    routes: [
      { path: '/', component: () => import('./views/Home.vue') },
      { path: '/about', component: () => import('./views/About.vue') }
    ]
  })

  app = createApp(App)
  app.use(router)
  app.mount(mountNode)
}

// 使用 renderWithQiankun 包裹生命周期
renderWithQiankun({
  bootstrap() {
    console.log('[micro-app-vue1] bootstrap')
  },
  mount(props) {
    console.log('[micro-app-vue1] mount', props)
    render(props)
  },
  unmount() {
    console.log('[micro-app-vue1] unmount')
    app?.unmount()
    app = null
  }
})

// 独立运行
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render()
}

关键 Vite 配置

修改 vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  base: '/app1/', //  开发环境基路径,生产环境请根据部署路径动态设置
  plugins: [
    vue(),
    qiankun('micro-app-vue1', {
      useDevMode: true // 开发模式必须开启
    })
  ],
  server: {
    port: 7100, // 微应用端口
    cors: true, // 必须开启跨域,主应用才能访问
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
    origin: 'http://localhost:7100' // 必须配置完整的 origin,qiankun 才能正确获取资源
  },
  build: {
    // 生产构建必须配置 UMD 格式供 qiankun 加载
    lib: {
      entry: './src/main.js',
      name: 'micro-app-vue1',
      formats: ['umd'],
      fileName: () => 'index.js'
    }
  }
})

关键提示: 这是网上大多数教程配置错误的地方!

  1. server.origin 必须是完整 URL,否则 qiankun 无法正确加载微应用资源
  2. base 必须和 activeRule 保持一致
  3. 生产构建必须配置 build.lib 输出 UMD 格式

第二个微应用 micro-app-vue2 配置类似,只需要修改:

  • 端口改为 7101
  • base 改为 /app2/
  • name 改为 micro-app-vue2

本地运行验证

我们需要三个终端分别启动:

# 终端一:主应用
cd main-app && npm install && npm run dev
# 访问 http://localhost:7099

# 终端二:微应用一
cd micro-app-vue1 && npm install && npm run dev
# 监听 http://localhost:7100

# 终端三:微应用二
cd micro-app-vue2 && npm install && npm run dev
# 监听 http://localhost:7101

打开浏览器访问 http://localhost:7099,你应该能看到:

  1. 主应用头部导航显示
  2. 微应用一正常加载3.点击导航切换到微应用二,微应用二正常加载
  3. 切换回微应用一,微应用一再次正常加载

如果看到这个效果,恭喜你!你的第一个 qiankun 微前端应用已经跑通了!

FAQ

Q: 按照步骤配置后,微应用还是加载不出来,怎么办?

A: 先检查这几点:

  1. micro-app-vue1vite.config.jsserver.origin 是否配置了完整 URL (http://localhost:7100)
  2. 端口是否被占用,三个服务都正常启动了吗?
  3. 浏览器控制台有没有报错?常见报错我们在第三篇文章会详细讲解。

Q: 微应用可以独立运行吗?

A: 可以!直接访问 http://localhost:7100 就能独立运行微应用一,这是最佳实践——每个微应用必须能够独立运行,方便开发调试。

Q: 为什么每次 mount 都要重新创建路由?

A: 为了避免状态污染。如果路由只创建一次重复使用,上次的状态会残留到下次挂载,重新创建可以保证每次挂载都是干净的状态。

Q: 生产环境部署和开发环境有什么不同?

A: 打包优化、部署方案、常见坑点我们放在第三篇文章详细讲解。第二篇我们先把核心概念讲清楚。

本章小结

你已经完成了:

  1. 创建了主应用和两个微应用的项目结构
  2. 主应用注册并启动了 qiankun
  3. 微应用适配了 qiankun 生命周期
  4. 配置了正确的 Vite 配置(解决了大多数教程的错误)
  5. 本地运行验证可以正常切换

下一篇我们深入讲解 qiankun 核心概念:路由联动、样式隔离、跨应用通信

TailwindCSS 核心概念与实用技巧:从传统CSS到Utility-First迁移指南

作者 CodeAI
2026年4月14日 17:48

引言:为什么越来越多人用Tailwind?

你是否还在为CSS命名发愁?

.container .header .button-primary 想破脑袋还是避免不了命名冲突。

传统CSS开发中,我们常常遇到这些痛点:

1. CSS文件越来越臃肿 项目迭代一段时间后,你会发现写了大量重复样式,却不敢删除旧代码,怕哪里出问题。最后CSS文件几千行,大部分都是无用代码。

2. 命名是永恒的难题 使用BEM命名规范?button button--primary button--large 虽然规范,但写起来冗长又繁琐。稍微复杂点的组件,命名就变成了玄学。

3. 样式和组件分离 写React/Vue组件时,JSX/模板里写了结构,还要跑到另一个CSS文件写样式,来回切换上下文,开发效率被打断。

4. 改样式要改多个文件 调整个间距颜色,要找到对应的CSS类,修改完还要回来检查,一不小心影响其他地方样式。

TailwindCSS 为什么能在近几年迅速流行?因为它从根本上解决了这些问题。

它把CSS带回到你的HTML中,用原子化的Utility类让你不用再写CSS,同时保持代码整洁可维护。

据统计,npm 下载量已经突破百万,Vue、React、Next.js 等主流框架都官方支持,越来越多团队开始全面采用。


什么是Utility-First?和传统CSS/BEM/CSS-in-JS的区别

先搞懂核心思想:Utility-First就是原子化CSS

简单说,Tailwind提供了大量功能单一的工具类,比如 text-center 代表文字居中,pt-4 代表上内边距1rem。

你不需要再写新的CSS,只需要在HTML中组合这些工具类就能构建出任何样式。

我们来对比一下不同方案:

传统CSS写法

<!-- HTML -->
<button class="btn btn-primary">点击我</button>
/* CSS */
.btn {
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  font-weight: 500;
}

.btn-primary {
  background-color: #3b82f6;
  color: white;
}

.btn-primary:hover {
  background-color: #2563eb;
}

BEM写法

<button class="button button--primary button--medium">点击我</button>
.button {
  font-family: system-ui;
  border: none;
  outline: none;
}

.button--primary {
  background-color: var(--color-primary);
  color: white;
}

.button--medium {
  padding: 8px 16px;
  font-size: 14px;
}

BEM解决了命名冲突问题,但还是需要不断写新的CSS,类名越来越长。

Tailwind Utility-First 写法

<button
  class="px-4 py-2 font-medium text-white bg-blue-500
             hover:bg-blue-600 rounded"
>
  点击我
</button>

不需要写任何CSS!所有样式都通过组合Utility类直接在HTML中完成。

CSS-in-JS 写法(对比参考)

// Styled Components 写法
const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  font-weight: 500;
  background-color: #3b82f6;
  color: white;

  &:hover {
    background-color: #2563eb;
  }
`;

<Button>点击我</Button>;

CSS-in-JS把CSS放到JS里,解决了作用域问题,但运行时有开销,调试也相对麻烦。

Tailwind 则是纯CSS方案,构建时移除无用代码,最终产物体积很小,同时保留了CSS的原生优势。

一句话总结区别:

  • 传统CSS/BEM:语义化命名,一个类对应多个样式属性
  • Utility-First:功能单一,一个类只做一件事
  • CSS-in-JS:JS掌管样式,组件级作用域

Tailwind核心概念详解

1. 配置文件 tailwind.config.js

安装完Tailwind后,根目录会有一个 tailwind.config.js 配置文件。

这是Tailwind的神经中枢,你可以在这里自定义主题、断点、颜色、间距等等。

基础配置示例:

/** @type {import('tailwindcss').Config} */
module.exports = {
  // 扫描所有项目文件,找出用到的类,用于Tree Shaking
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue,html}"],
  theme: {
    // 扩展默认主题,不会覆盖
    extend: {
      // 自定义颜色
      colors: {
        primary: "#165DFF",
        secondary: "#6b7280",
      },
      // 自定义字体
      fontFamily: {
        sans: ["Inter", "system-ui", "sans-serif"],
      },
      // 自定义断点
      screens: {
        "3xl": "1920px",
      },
    },
  },
  // 第三方插件
  plugins: [],
};

如果你想完全覆盖默认主题,可以直接在 theme 里定义,不使用 extend

theme: {
  // 完全自定义颜色,会替换Tailwind默认颜色
  colors: {
    blue: {
      50: '#f0f9ff',
      100: '#e0f2fe',
      // ... 一直到 900
      600: '#2563eb',
    }
  }
}

对于中文开发者,建议在配置中加入中文字体优化:

theme: {
  extend: {
    fontFamily: {
      chinese: ['PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif'],
    },
  },
}

使用的时候直接:

<body class="font-chinese"></body>

2. @layer 分层机制

Tailwind 用 @layer 把样式分成三层:basecomponentsutilities

这个分层机制帮你正确排序CSS优先级,避免特异性冲突。

/* 在你的style.css中 */
@tailwind base;
@tailwind components;
@tailwind utilities;

我们分别解释:

@layer base - 基础样式层

用于重置浏览器默认样式,或者给HTML标签添加默认样式。

@layer base {
  h1 {
    @apply text-3xl font-bold mb-4;
  }
  h2 {
    @apply text-2xl font-semibold mb-3;
  }
  a {
    @apply text-blue-600 hover:underline;
  }
}

Base层优先级最低,后面的classes和utilities可以覆盖它。

@layer components - 组件层

用来提取可复用的组件样式,优先级高于base。

@layer components {
  .btn {
    @apply px-4 py-2 rounded font-medium;
  }
  .btn-primary {
    @apply bg-primary text-white hover:bg-primary/90;
  }
  .card {
    @apply bg-white rounded-lg shadow p-6;
  }
}

然后就可以在HTML中直接使用:

<button class="btn btn-primary">提交</button>
<div class="card">内容</div>

@layer utilities - 工具类层

优先级最高,如果你需要添加自定义工具类,放在这里。

@layer utilities {
  .text-shadow {
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  .content-auto {
    content-visibility: auto;
  }
}

放在 @layer utilities 里的自定义工具类,优先级比Tailwind自带的工具类还要高吗?不,它和Tailwind自带的utilities同级,后面写的会覆盖前面的。

记住这个优先级顺序:base < components < utilities,这样就不会出现奇怪的样式覆盖问题。

3. Purge / Tree-shaking 工作原理

Tailwind v3 默认就开启了Tree-shaking,它会扫描你所有的模板文件,只保留实际用到的Utility类。

工作流程:

  1. 你在 content 配置里指定了要扫描的文件路径
  2. 构建时,Tailwind 从这些文件中提取出所有用到的class名称
  3. 只生成这些class对应的CSS,没有用到的全部移除

举个例子,你的项目只用到了 px-4 py-2 bg-blue-500,那Tailwind就只会生成这几个类对应的CSS,其他所有没用到的padding、margin、颜色都不会出现在最终CSS文件中。

所以即使Tailwind默认包含了几千个Utility类,最终打包出来的CSS通常只有几KB到十几KB,比你自己写的CSS还小。

配置示例(v3标准写法):

// tailwind.config.js
module.exports = {
  content: [
    // 所有可能用到Tailwind类的文件都要写在这里
    "./index.html",
    "./src/**/*.{js,jsx,ts,tsx,vue,html}",
  ],
};

注意事项:如果你用了动态class拼接,需要用安全列表:

module.exports = {
  content: [...],
  safelist: [
    // 强制保留这些类,不会被摇掉
    'bg-red-500',
    'bg-green-500',
    'bg-yellow-500',
    // 或者用模式匹配
    {
      pattern: /bg-(red|green|yellow)-.+/,
    }
  ]
}

这在中文开发中很常见,比如后台配置返回不同状态的样式类,一定要记得加safelist,不然生产环境样式会丢。

4. 响应式断点系统

Tailwind的响应式设计非常简单,默认提供了五个断点:

断点 最小值 对应设备
sm 640px 手机横屏
md 768px 平板
lg 1024px 小桌面
xl 1280px 大桌面
2xl 1536px 超大桌面

使用方法非常简单:在类名前加上断点前缀就是了。

示例:移动端单列,平板双列,桌面三列

<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  <div>卡片1</div>
  <div>卡片2</div>
  <div>卡片3</div>
</div>

解释一下:

  • grid-cols-1:默认(小于640px)单列
  • md:grid-cols-2:宽度 ≥768px 变成双列
  • lg:grid-cols-3:宽度 ≥1024px 变成三列

更实际的导航栏示例:移动端汉堡菜单,桌面端全链接

<nav class="bg-white shadow fixed w-full">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div class="flex justify-between h-16">
      <div class="flex">Logo</div>
      <!-- 桌面端菜单 -->
      <div class="hidden md:flex items-center space-x-4">
        <a href="#" class="text-gray-700 hover:text-blue-600">首页</a>
        <a href="#" class="text-gray-700 hover:text-blue-600">产品</a>
        <a href="#" class="text-gray-700 hover:text-blue-600">关于</a>
      </div>
      <!-- 移动端汉堡按钮 -->
      <div class="md:hidden flex items-center">
        <button>🍔</button>
      </div>
    </div>
  </div>
</nav>

hidden md:flex 的意思是:默认隐藏,大于等于md(768px)才显示,完美实现响应式切换。

自定义断点也很简单,在配置里加就行:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      screens: {
        xs: "480px", // 比sm更小的断点
        "3xl": "1920px", // 更大屏幕
      },
    },
  },
};

实用开发技巧

1. 提取组件(@apply) vs 保持纯utility

这是Tailwind开发中最常见的问题:什么时候该提取组件,什么时候直接堆Utility类?

两种方式都可以,我们来看具体例子。

直接保持纯Utility写法

<button
  class="px-4 py-2 text-sm font-medium text-white bg-blue-600
             hover:bg-blue-700 rounded-lg focus:outline-none
             focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
  提交
</button>

优点:所有样式都在这里,一目了然,不用跳去别的文件看。适合一次性、不重复使用的按钮。

使用 @apply 提取为可复用组件

@layer components {
  .btn-primary {
    @apply px-4 py-2 text-sm font-medium text-white bg-blue-600
           hover:bg-blue-700 rounded-lg focus:outline-none
           focus:ring-2 focus:ring-blue-500 focus:ring-offset-2;
  }
}

然后HTML就很简洁:

<button class="btn-primary">提交</button>
<button class="btn-primary">保存</button>

优点:复用方便,统一修改只改一处。适合项目中多处使用的组件。

在Vue/React组件中提取

这其实是更推荐的方式,因为你已经在使用组件化框架了,为什么不直接用组件呢?

React示例:

function Button({ children, ...props }) {
  return (
    <button
      className="px-4 py-2 text-sm font-medium text-white bg-blue-600
                 hover:bg-blue-700 rounded-lg focus:outline-none
                 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      {...props}
    >
      {children}
    </button>
  );
}

// 使用
<Button>点击我</Button>;

Vue示例:

<template>
  <button
    class="px-4 py-2 text-sm font-medium text-white bg-blue-600
                hover:bg-blue-700 rounded-lg focus:outline-none
                focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
  >
    <slot />
  </button>
</template>

我的建议

  • 如果用组件化框架(React/Vue),优先用JSX/Vue组件提取,不要用@apply写到CSS里
  • 如果是纯HTML项目或者需要配合后端模板引擎,用@layer components提取
  • 不要过度提取,只提取真正会复用的组件,一次性的代码直接堆Utility就好

2. 暗色模式实现

Tailwind v3 内置暗色模式支持,开箱即用。

先在配置中开启:

// tailwind.config.js
module.exports = {
  darkMode: "class", // 或者 'media' 跟随系统
  // ...
};

darkMode: 'media' 会自动根据系统暗色切换,darkMode: 'class' 适合手动切换(用户点击按钮切换)。

使用方式:加上 dark: 前缀。

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
  <h1>你好,世界</h1>
  <p>这是一段文字</p>
</div>

htmlbody 标签加上 dark class 后,暗色模式就激活了:

<html class="dark">
  <!-- 所有dark:前缀的样式都会生效 -->
</html>

实现手动切换的JS代码:

// 检查用户偏好
if (
  localStorage.theme === "dark" ||
  (!("theme" in localStorage) &&
    window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
  document.documentElement.classList.add("dark");
} else {
  document.documentElement.classList.remove("dark");
}

// 切换函数
function toggleDarkMode() {
  if (document.documentElement.classList.contains("dark")) {
    document.documentElement.classList.remove("dark");
    localStorage.theme = "light";
  } else {
    document.documentElement.classList.add("dark");
    localStorage.theme = "dark";
  }
}

卡片带暗色的完整示例:

<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
  <h3 class="text-gray-900 dark:text-white font-semibold">标题</h3>
  <p class="text-gray-600 dark:text-gray-300 mt-2">
    这是描述文字,在暗色模式下会变浅。
  </p>
  <button
    class="mt-4 px-4 py-2 bg-blue-600 dark:bg-blue-500
                 text-white rounded"
  >
    按钮
  </button>
</div>

3. Hover/Focus 等交互状态

Tailwind给所有交互状态都提供了变体前缀,直接用就行。

基础示例:

<!-- Hover -->
<button class="bg-blue-500 hover:bg-blue-600 text-white">Hover我变色</button>

<!-- Focus -->
<input
  class="border focus:outline-none focus:ring-2
             focus:ring-blue-500 border-gray-300 rounded px-3 py-2"
  placeholder="点击我看看"
/>

多个状态可以叠加:

<button
  class="bg-green-500 hover:bg-green-600
             focus:ring-2 focus:ring-green-500 focus:ring-offset-2
             active:bg-green-700
             disabled:opacity-50 disabled:cursor-not-allowed
             text-white px-4 py-2 rounded"
>
  按钮
</button>

常用状态变体列表:

  • hover: 鼠标悬停
  • focus: 获得焦点
  • active: 鼠标按下
  • disabled: 禁用状态
  • first: 第一个子元素
  • last: 最后一个子元素
  • odd: 奇数行
  • even: 偶数行
  • hover:dark: / dark:hover: 暗色模式下的hover

响应式和状态可以组合,顺序没关系:md:hover:bg-blue-500hover:md:bg-blue-500 效果一样。

4. group-hover 群组变体

很多时候我们希望鼠标悬停在父元素上,改变子元素的样式,这就需要 group-hover

使用分两步:

  1. 给父元素加上 group class
  2. 给子元素加上 group-hover: 前缀

卡片示例:鼠标悬停卡片时,让按钮背景变色。

<div class="group card border rounded-lg p-6 hover:shadow-lg">
  <h3 class="group-hover:text-blue-600">卡片标题</h3>
  <p>卡片内容...</p>
  <button
    class="bg-gray-200 group-hover:bg-blue-600
                 group-hover:text-white mt-4 px-4 py-2 rounded"
  >
    查看详情
  </button>
</div>

导航栏下拉菜单示例:

<div class="group relative inline-block">
  <button class="group-hover:text-blue-600">产品菜单 ▼</button>
  <div class="absolute hidden group-hover:block w-48 bg-white shadow">
    <a href="#" class="block px-4 py-2 hover:bg-gray-100">产品1</a>
    <a href="#" class="block px-4 py-2 hover:bg-gray-100">产品2</a>
    <a href="#" class="block px-4 py-2 hover:bg-gray-100">产品3</a>
  </div>
</div>

完美!不需要写任何JS,纯CSS实现悬停显示下拉菜单。

还有 group-focusgroup-active,用法一样,针对focus和active状态。

5. 任意值方括号语法

Tailwind v3 最香的功能就是任意值语法,用方括号 [] 直接写任意值。

什么时候用?当你需要一个Tailwind默认没提供的值,不用去改配置文件,直接写:

/* 自定义宽度 */
<div class="w-[310px]">
  /* 自定义定位 */
  <div class="top-[13px] left-[7px]">
    /* 自定义颜色 */
    <div class="bg-[#165DFF] text-[#fff]">
      /* 自定义字体大小 */
      <h1 class="text-[32px]">
        /* 自定义间距 */
        <div class="m-[14px] p-[8px]"></div>
      </h1>
    </div>
  </div>
</div>

组合响应式也没问题:

<div class="w-[300px] md:w-[500px] lg:w-[720px]"></div>

甚至可以写CSS自定义属性:

<div class="bg-[--primary-color]"></div>

这解决了什么问题?以前你想要一个特殊尺寸,必须去tailwind.config.js里扩展,现在直接方括号搞定,非常方便。

但是注意:不要滥用,能用上默认值就用默认值,比如 px-4 能满足就别写 px-[16px]。只有默认值满足不了的时候再用任意值语法。


常见迁移误区

误区一:过早提取组件

很多人从传统CSS转过来,习惯了一切都抽成组件,刚写了一个按钮就想着提取出来。

错误示例

项目才刚开始,按钮只用到一次,就急着提取:

@layer components {
  .header-button {
    @apply ... /* 只在头部用到一次 */;
  }
  .sidebar-item {
    @apply ... /* 只用到一次 */;
  }
}

问题:需求一变,这个组件就不用了,你白提了,而且还要维护CSS。

正确做法

重复出现第二次的时候再提取。

第一次写,直接堆Utility,第二次碰到一样的,复制过去,第三次还碰到,这时候你知道它真的需要复用,再提取也不迟。

误区二:混乱的class顺序

很多人写Tailwind,class顺序乱排,读起来非常费劲。

混乱示例

<button
  class="text-white hover:bg-blue-600 px-4 bg-blue-500 py-2 rounded"
></button>

顺序乱了,你很难快速读懂这个按钮有哪些样式。

推荐的排序思路

按这个顺序排列,可读性大大提高:

  1. 定位布局类:position, top/right/bottom/left, z-index, display, flex/grid, flex-wrap, justify-, items-, gap, w, h, m, p
  2. 边框阴影:border, rounded, shadow
  3. 背景文字颜色:bg, text
  4. 字体样式:font-, text-
  5. 交互状态:hover:, focus:, active:, disabled:, group-hover:
  6. 响应式变体:sm:, md:, lg:, xl:

整理之后

<button
  class="px-4 py-2 bg-blue-500 text-white rounded
             hover:bg-blue-600"
></button>

舒服多了对不对?

很多编辑器有Tailwind插件,可以自动排序,推荐开启。如果你用VSCode,安装 bradlc.vscode-tailwindcss 插件,开启 editor.codeActionsOnSave 自动排序。

误区三:不知道什么时候用自定义CSS

很多人转了Tailwind之后,觉得什么都能用Utility搞定,其实不是。

Tailwind不排斥自定义CSS,该用的时候就要用。

适合用自定义CSS的场景

场景一:复杂的媒体查询和关键帧动画

/* 自定义动画,这用Utility不好写,放在全局CSS就行 */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-fadeIn {
  animation: fadeIn 0.3s ease-out;
}

然后把它放在 @layer utilities 里,就能在HTML中用了:

@layer utilities {
  .animate-fadeIn {
    animation: fadeIn 0.3s ease-out;
  }
}

场景二:复杂伪元素

/* 比如清除浮动 */
@layer utilities {
  .clearfix::after {
    content: "";
    display: table;
    clear: both;
  }
}

场景三:你就是需要写原生CSS的时候

Tailwind只是工具,不是宗教,如果你觉得写原生CSS更清晰更简洁,那就直接写。

错误做法

用一堆方括号拼出一个复杂CSS,可读性极差:

<div class="[&:nth-child(2n+1)]:mr-0 [&>span]:absolute [...]"></div>

这种情况不如抽出来写自定义CSS。

误区四:覆盖Tailwind默认样式时优先级错了

如果你没有用 @layer,直接写在全局CSS,会出现优先级问题。

错误示例

/* 没有加@layer,这个样式会被Tailwind utilities覆盖 */
.btn-primary {
  background-color: red !important; /* 被迫加important */
}

正确做法

@layer components {
  /* components层优先级在utilities之前,不需要important */
  .btn-primary {
    background-color: red;
  }
}

记住:只要是自定义的Tailwind相关样式,都放到 @layer 里面,让Tailwind帮你处理优先级。


总结:给传统CSS开发者的迁移建议

从传统CSS转到Utility-First开发思维,需要一个适应过程,这里给大家几个实用建议:

1. 不要一开始就全量迁移

如果你有一个成熟的老项目,不用一下子全部改成Tailwind。可以配合着用,新组件用Tailwind写,旧组件慢慢迁移。

Tailwind和传统CSS可以和平共处。

2. 不要害怕HTML变"脏"

刚转过来会觉得一堆class写在HTML里很脏,这不符和"表现与结构分离"的思想啊?

这是思维转换中最关键的一步。其实,当你适应了Utility-First,你会发现这样反而更直观,不用来回跳文件找样式。

3. 优先用默认配置,少自定义

Tailwind默认的设计系统已经非常完善了,颜色、间距、断点都有了,能满足90%的场景。不要一开始就去推翻默认配置重写一遍。

默认值够用就用默认值,不够用了再用方括号或者配置扩展。

4. 善用编辑器插件提升开发效率

VSCode的Tailwind CSS IntelliSense 插件一定要装,自动补全class名称,提示颜色,非常好用。

5. 记住这个决策树

遇到问题不知道该怎么做,问自己:

  • 这个会复用吗?不会 → 直接堆Utility
  • 会复用吗?会 → 用React/Vue组件提取(如果用框架)
  • 框架也不好处理 → 用@layer components提取
  • Utility搞不定 → 写自定义CSS,放到@layer utilities

Tailwind不是银弹,但它确实解决了CSS开发中长期存在的很多问题。对于从传统CSS转过来的开发者,只要适应了Utility-First的思维,开发效率会提升很多。

开始动手试试吧,从一个小组件开始,慢慢你就会爱上这种开发方式。


❌
❌