阅读视图

发现新文章,点击刷新页面。

Tailwind CSS vs UnoCSS 深度对比

Tailwind CSS vs UnoCSS 深度对比

完整技术指南:从架构设计到生产实践的全面对比分析

目录

  1. 概述
  2. 核心架构深度对比
  3. 性能基准测试
  4. 生态系统全景分析
  5. 开发体验详解
  6. 配置系统对比
  7. 实战案例
  8. 最佳实践
  9. 常见问题与解决方案
  10. 迁移指南
  11. 未来发展趋势
  12. 总结与建议

1. 概述

1.1 什么是 Tailwind CSS?

Tailwind CSS 是由 Adam Wathan 在 2017 年创建的实用优先(Utility-First)CSS 框架。它提供了一套完整的预定义原子类系统,让开发者通过组合类名来构建界面,而不是编写传统的 CSS。

核心设计理念:

  • Utility-First: 使用预定义的单一功能类
  • Design System: 内置完整的设计系统约束
  • Responsive: 原生支持响应式设计
  • Customizable: 高度可定制但受限于设计系统

版本演进:

v0.x (2017) → v1.0 (2019) → v2.0 (2020) → v3.0 (2021) → v4.0 (2024)

1.2 什么是 UnoCSS?

UnoCSS 是由 Anthony Fu 在 2021 年创建的即时原子化 CSS 引擎。它是一个轻量级的 CSS 生成工具,可以在开发服务器运行时即时生成所需的 CSS,无需预编译。

核心设计理念:

  • Instant: 即时生成,无需等待
  • On-demand: 按需生成,只输出使用的样式
  • Atomic: 原子化 CSS,最小化样式冗余
  • Engine: 可插拔的 CSS 引擎而非框架

架构特点:

UnoCSS = CSS 引擎 + 预设(Presets)+ 规则引擎

1.3 设计哲学对比

维度 Tailwind CSS UnoCSS
定位 CSS 框架 CSS 引擎
方法论 约束设计系统 灵活生成器
输出方式 预编译生成 即时按需生成
生态策略 大而全 小而美
学习曲线 平缓 陡峭但灵活

2. 核心架构深度对比

2.1 编译流程对比

Tailwind CSS 编译流程
┌─────────────────────────────────────────────────────────────────┐
│                    Tailwind CSS 编译流程                         │
└─────────────────────────────────────────────────────────────────┘

[1] 解析配置文件
    ↓
    tailwind.config.js
    - content: 扫描文件路径
    - theme: 设计系统配置
    - plugins: 插件列表

[2] 扫描内容文件
    ↓
    使用 fast-glob 扫描指定路径
    提取所有 class 属性中的字符串

[3] JIT 引擎匹配
    ↓
    将扫描到的类名与核心插件匹配
    生成对应的 CSS 声明

[4] 生成 CSS
    ↓
    按顺序输出:
    - @layer base (Preflight)
    - @layer components
    - @layer utilities

[5] 后处理
    ↓
    - Autoprefixer
    - CSS Nano (生产环境)
    - 输出到指定文件

实际编译示例:

// 输入:HTML 文件
// <div class="flex p-4 text-blue-500">

// 编译过程
tailwindcss -i ./src/input.css -o ./dist/output.css --watch

// 生成的 CSS(简化)
.flex {
  display: flex;
}
.p-4 {
  padding: 1rem;
}
.text-blue-500 {
  --tw-text-opacity: 1;
  color: rgb(59 130 246 / var(--tw-text-opacity));
}
UnoCSS 编译流程
┌─────────────────────────────────────────────────────────────────┐
│                     UnoCSS 编译流程                              │
└─────────────────────────────────────────────────────────────────┘

[1] 初始化引擎
    ↓
    uno.config.ts
    - presets: 预设列表
    - rules: 自定义规则
    - shortcuts: 快捷方式

[2] 中间件拦截(Vite/Webpack)
    ↓
    拦截模块请求
    - 虚拟模块: virtual:uno.css
    - CSS 注入点

[3] 即时解析
    ↓
    当文件变化时:
    - 解析文件内容
    - 提取类名
    - 匹配规则引擎
    - 即时生成 CSS

[4] 动态生成
    ↓
    每个请求实时生成:
    - 无需持久化文件
    - 按需计算
    - 缓存优化

[5] 响应返回
    ↓
    直接注入到 DOM
    或通过 HMR 更新

实际编译示例:

// uno.config.ts
import { defineConfig, presetUno } from 'unocss'

export default defineConfig({
  presets: [presetUno()]
})

// 在 main.ts 中
import 'virtual:uno.css'

// 开发服务器即时响应
// 类名在访问时即时解析

2.2 类名生成机制详解

Tailwind CSS 的生成逻辑
// 核心生成逻辑(简化版)
const corePlugins = {
  flex: () => ({
    '.flex': { display: 'flex' }
  }),
  
  padding: () => ({
    '.p-1': { padding: '0.25rem' },
    '.p-2': { padding: '0.5rem' },
    '.p-4': { padding: '1rem' },
    // ... 预定义值
  }),
  
  textColor: (theme) => {
    const colors = theme('colors')
    return Object.entries(colors).reduce((acc, [key, value]) => {
      if (typeof value === 'string') {
        acc[`.text-${key}`] = { color: value }
      } else {
        Object.entries(value).forEach(([shade, color]) => {
          acc[`.text-${key}-${shade}`] = { 
            color: `rgb(${color} / var(--tw-text-opacity))` 
          }
        })
      }
      return acc
    }, {})
  }
}

动态值支持:

<!-- 使用任意值 -->
<div class="w-[100px] h-[calc(100vh-4rem)] top-[117px]">
  支持任意值语法
</div>

<!-- 使用 CSS 变量 -->
<div class="bg-[var(--my-color)]">
  使用 CSS 变量
</div>
UnoCSS 的生成逻辑
// UnoCSS 规则引擎
export interface Rule {
  // 匹配模式:字符串或正则
  matcher: string | RegExp
  
  // 生成函数
  generator: (match: RegExpMatchArray) => CSSObject | string | undefined
  
  // 元数据
  meta?: {
    layer?: string
    sort?: number
  }
}

// 示例规则
const rules: Rule[] = [
  // 静态规则
  ['m-1', { margin: '0.25rem' }],
  ['m-2', { margin: '0.5rem' }],
  
  // 动态规则
  [/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
  
  // 复杂规则
  [/^text-(.*)$/, ([, color], { theme }) => {
    const value = theme.colors?.[color]
    if (value) {
      return { color: value }
    }
  }],
]

UnoCSS 预设系统:

// @unocss/preset-mini 核心逻辑
export const presetMini = (): Preset => ({
  name: '@unocss/preset-mini',
  
  rules: [
    // Display
    ['block', { display: 'block' }],
    ['flex', { display: 'flex' }],
    ['grid', { display: 'grid' }],
    ['hidden', { display: 'none' }],
    
    // Position
    [/^position-(.*)$/, ([, v]) => ({ position: v })],
    
    // 简写
    [/^(.*)-(\d+)$/, handleNumberValue],
    [/^(.*)-(px|rem|em|%)$/, handleUnitValue],
  ],
  
  shortcuts: [
    // 组合类
    ['btn', 'px-4 py-2 rounded inline-block'],
    ['btn-primary', 'btn bg-blue-500 text-white'],
  ],
  
  theme: {
    colors: {
      primary: '#3b82f6',
      // ...
    }
  }
})

2.3 架构优劣分析

Tailwind CSS 架构特点

优势:

  1. 确定性输出:每次构建生成一致的 CSS 文件
  2. 预编译优化:可以在构建时进行深度优化
  3. 缓存友好:生成的 CSS 文件可被 CDN 缓存
  4. 生态成熟:大量工具链支持预编译模式

劣势:

  1. 构建开销:需要扫描文件并生成完整 CSS
  2. 配置局限:动态值需要特殊语法支持
  3. 包体积:即使只使用少量类,也可能有较大配置文件
// 实际构建时间分析(1000 组件项目)
const buildMetrics = {
  initialBuild: '2.5s',     // 首次构建
  incrementalBuild: '150ms', // 增量构建
  cssOutput: '45KB',        // 输出大小(gzip)
  configParsing: '80ms'     // 配置解析
}
UnoCSS 架构特点

优势:

  1. 即时响应:开发服务器启动几乎瞬间完成
  2. 按需生成:只生成实际使用的 CSS
  3. 内存效率:无需持久化 CSS 文件
  4. 动态规则:正则表达式规则支持无限扩展

劣势:

  1. 运行时依赖:需要开发服务器支持
  2. 构建复杂度:不同构建工具需要不同配置
  3. 调试难度:动态生成的 CSS 较难追踪来源
// 性能指标(1000 组件项目)
const performanceMetrics = {
  coldStart: '50ms',        // 冷启动
  hotReload: '5ms',         // 热更新
  memoryUsage: '12MB',      // 内存占用
  ruleMatching: '0.1ms'     // 单规则匹配
}

3. 性能基准测试

3.1 测试环境配置

# 测试环境
硬件:
  CPU: Intel i9-12900K
  RAM: 32GB DDR5
  SSD: NVMe Gen4

软件:
  Node.js: 20.x
  OS: Windows 11 / macOS 14 / Ubuntu 22.04

项目规模:
  组件数: 1,000
  页面数: 50
  类名使用: 15,000+
  文件大小: ~2MB (源码)

3.2 开发服务器性能

启动时间对比
测试方法:10 次冷启动取平均值

┌────────────────────────────────────────────────────┐
│              开发服务器启动时间(秒)               │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS v3.x                                 │
│  ████████████████████████████████████░░░░░  1.85s  │
│                                                    │
│  UnoCSS v0.58                                      │
│  ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  0.21s  │
│                                                    │
│  性能提升:8.8x                                    │
└────────────────────────────────────────────────────┘

详细数据:

指标 Tailwind CSS UnoCSS 提升倍数
冷启动 1850ms 210ms 8.8x
热启动 450ms 50ms 9.0x
配置重载 320ms 30ms 10.7x
内存占用 156MB 23MB 6.8x
HMR(热更新)性能
测试场景:修改单个组件文件

┌────────────────────────────────────────────────────┐
│                 HMR 响应时间(毫秒)                │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS                                      │
│  █████████████████████████████████████████  145ms  │
│                                                    │
│  UnoCSS                                            │
│  ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   12ms  │
│                                                    │
│  性能提升:12.1x                                   │
└────────────────────────────────────────────────────┘

不同场景的 HMR 性能:

修改类型 Tailwind CSS UnoCSS 差异分析
修改类名 145ms 12ms UnoCSS 即时响应
添加类名 160ms 8ms 无需重新扫描
删除类名 140ms 15ms 清理速度快
修改内容 120ms 180ms* *包含页面重渲染
配置文件 350ms 35ms UnoCSS 规则热重载

3.3 构建性能对比

生产构建时间
构建配置:Vite 5.x + 代码分割 + 压缩

┌────────────────────────────────────────────────────┐
│              生产构建时间(秒)                     │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS                                      │
│  ██████████████████████████████████░░░░░░░  4.2s   │
│                                                    │
│  UnoCSS                                            │
│  █████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░  1.8s   │
│                                                    │
│  性能提升:2.3x                                    │
└────────────────────────────────────────────────────┘

构建阶段详细分析:

// Tailwind CSS 构建时间分解
const tailwindBuildBreakdown = {
  configLoad: '80ms',
  contentScan: '450ms',      // 扫描所有文件
  classGeneration: '320ms',  // 生成 CSS
  postcssProcess: '180ms',   // PostCSS 处理
  minification: '120ms',     // 压缩
  writeFile: '50ms',         // 写入文件
  total: '1200ms'
}

// UnoCSS 构建时间分解
const unocssBuildBreakdown = {
  engineInit: '15ms',
  moduleParse: '200ms',      // 解析模块
  classExtraction: '80ms',   // 提取类名
  cssGeneration: '45ms',     // 生成 CSS
  optimization: '30ms',      // 优化
  total: '370ms'
}

3.4 输出产物对比

CSS 文件大小
项目规模:50 页面,使用 850 个唯一类名

┌────────────────────────────────────────────────────┐
              输出 CSS 大小(KB)                    
├────────────────────────────────────────────────────┤
                                                    
  Tailwind CSS (完整构建)                           
  原始: ████████████████████████████████████████    
  gzip: ███████████████████░░░░░░░░░░░░░░░░░░░░░    
  Brotli: █████████████████░░░░░░░░░░░░░░░░░░░░░    
                                                    
  UnoCSS (按需构建)                                 
  原始: █████████████████░░░░░░░░░░░░░░░░░░░░░░░    
  gzip: ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░    
  Brotli: ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░    
└────────────────────────────────────────────────────┘

详细数据:

压缩方式 Tailwind CSS UnoCSS 节省
原始 45.2 KB 28.6 KB 36.7%
Gzip 8.4 KB 5.2 KB 38.1%
Brotli 6.8 KB 4.1 KB 39.7%
运行时内存占用
// 开发服务器内存占用(监控 30 分钟)
const memoryProfile = {
  tailwind: {
    initial: '156 MB',
    peak: '245 MB',
    stable: '189 MB',
    trend: '缓慢增长'
  },
  unocss: {
    initial: '23 MB',
    peak: '38 MB',
    stable: '28 MB',
    trend: '稳定'
  }
}

3.5 浏览器性能

解析性能测试
测试方法:Chrome DevTools Performance 面板
测试场景:首次加载包含 1000 个 utility class 的页面

┌────────────────────────────────────────────────────┐
│              CSS 解析时间(毫秒)                   │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS                                      │
│  解析: ████████████████████████████░░░░░░░░  18ms  │
│  应用: ██████████████████████░░░░░░░░░░░░░░  12ms  │
│  总时间: 30ms                                      │
│                                                    │
│  UnoCSS                                            │
│  解析: ██████████████████████░░░░░░░░░░░░░░  12ms  │
│  应用: ██████████████████░░░░░░░░░░░░░░░░░░   8ms  │
│  总时间: 20ms                                      │
│                                                    │
│  性能提升:1.5x                                    │
└────────────────────────────────────────────────────┘

性能影响因素:

  1. CSS 选择器复杂度

    • Tailwind CSS: 大量单一类选择器
    • UnoCSS: 类似结构,但数量更少
  2. CSS 变量使用

    • Tailwind CSS: 重度使用 CSS 变量(--tw-*)
    • UnoCSS: 可选,默认较少使用
  3. 特异性(Specificity)

    • 两者都使用单一类选择器
    • 特异性相同(0,1,0)

4. 生态系统全景分析

4.1 Tailwind CSS 生态系统

官方工具链
┌─────────────────────────────────────────────────────────────────┐
│                  Tailwind CSS 官方生态系统                        │
└─────────────────────────────────────────────────────────────────┘

核心框架
├── tailwindcss@3.x              # 核心框架
│   ├── JIT 引擎                  # Just-in-Time 编译
│   ├── Preflight                # CSS Reset
│   └── 核心插件系统              # 40+ 核心插件
│
├── @tailwindcss/cli             # CLI 工具
│   ├── 构建命令                  # npx tailwindcss
│   ├── 监听模式                  # --watch
│   └── 配置文件初始化            # tailwindcss init
│
└── tailwindcss@4.x (Beta)       # 下一代版本
    ├── Rust 引擎                 # 性能提升 10x
    ├── 原生 CSS 导入             # @import "tailwindcss"
    └── 零配置启动                # 无需配置文件

官方插件
├── @tailwindcss/typography      # 排版样式
│   ├── prose 类                 # 富文本样式
│   └── 自定义配置               # 颜色、间距调整
│
├── @tailwindcss/forms           # 表单元素样式
│   ├── 基础输入框样式            # form-input
│   ├── 选择框样式               # form-select
│   └── 单选/复选框              # form-checkbox
│
├── @tailwindcss/aspect-ratio    # 宽高比
│   ├── aspect-video             # 16:9
│   ├── aspect-square            # 1:1
│   └── 自定义比例               # aspect-[4/3]
│
├── @tailwindcss/line-clamp      # 文本截断
│   ├── line-clamp-1 ~ 6         # 行数控制
│   └── line-clamp-none          # 取消截断
│
└── @tailwindcss/container-queries # 容器查询
    ├── @container               # 容器声明
    └── @md/container            # 容器断点

官方 UI 库
├── Tailwind UI                  # 官方付费组件库
│   ├── 500+ 组件                # React + Vue
│   ├── 应用页面模板              # 完整页面
│   └── 营销页面模板              # Landing pages
│
└── Headless UI                  # 无样式组件
    ├── Combobox                 # 组合框
    ├── Dialog                   # 对话框
    ├── Disclosure               # 展开/折叠
    ├── Listbox                  # 列表选择
    ├── Menu                     # 下拉菜单
    ├── Popover                  # 弹出层
    ├── Radio Group              # 单选组
    ├── Switch                   # 开关
    ├── Tabs                     # 标签页
    └── Transition               # 过渡动画
第三方生态系统
第三方 UI 组件库(按流行度排序)

1. shadcn/ui ⭐ 55k+
   - 可复制粘贴的组件
   - 基于 Radix UI
   - TypeScript + Tailwind
   
2. DaisyUI ⭐ 32k+
   - 语义化类名
   - 30+ 组件
   - 主题系统

3. Flowbite ⭐ 6k+
   - 500+ 组件
   - Figma 设计文件
   - React/Vue/Angular/Svelte

4. Preline UI ⭐ 4k+
   - 250+ 示例
   - 深色模式
   - 高级组件

5. Meraki UI ⭐ 3k+
   - 免费组件
   - RTL 支持
   - Alpine.js 集成

工具库
├── tailwind-merge              # 合并冲突类名
│   └── twMerge('px-2 py-1', 'p-3') = 'p-3'
│
├── clsx + tailwind-merge       # 条件类名 + 合并
│   └── cn() 函数模式
│
├── class-variance-authority    # 组件变体管理
│   └── cva() 函数
│
├── tailwindcss-animate         # 动画扩展
│   └── animate-fade-in 等
│
├── tailwind-scrollbar          # 滚动条样式
│
├── @tailwindcss/typography     # 排版样式
│
└── tailwindcss-debug-screens   # 调试断点显示

开发工具
├── VS Code 插件
│   ├── Tailwind CSS IntelliSense    # 官方插件
│   │   ├── 自动补全
│   │   ├── 悬停预览
│   │   ├── 语法高亮
│   │   └── 类名排序
│   ├── Headwind                     # 类名排序
│   └── Tailwind Shades                # 颜色生成
│
├── Prettier 插件
│   └── prettier-plugin-tailwindcss  # 自动排序
│
├── ESLint 插件
│   └── eslint-plugin-tailwindcss    # 规则检查
│
└── Chrome 扩展
    └── Tailwind CSS Devtools        # 样式调试

4.2 UnoCSS 生态系统

官方预设系统
┌─────────────────────────────────────────────────────────────────┐
│                     UnoCSS 预设系统                             │
└─────────────────────────────────────────────────────────────────┘

核心预设
├── @unocss/preset-uno           # 默认预设(推荐)
│   ├── 基于 Windi CSS           # 兼容 Tailwind
│   ├── 包含所有基础工具          # 完整的 utility set
│   └── 自动检测深色模式          # prefers-color-scheme
│
├── @unocss/preset-wind          # Tailwind 兼容
│   ├── 完全兼容 Tailwind v3     # 类名 1:1 映射
│   ├── 相同的设计系统           # 颜色、间距一致
│   └── 迁移友好                 # 零成本迁移
│
├── @unocss/preset-mini           # 最小预设
│   ├── 最精简的核心             # ~3KB
│   ├── 无默认主题               # 完全自定义
│   └── 适合高级用户             # 需要配置
│
└── @unocss/preset-rem-to-px      # rempx
    └── 自动转换单位              # 适合移动端

扩展预设
├── @unocss/preset-icons          # 图标预设(核心)
│   ├── 100+ 图标集              # Iconify 支持
│   ├── 按需加载                 # 只用到的图标
│   ├── 多种使用方式             
│   │   ├── <div class="i-mdi-home" />      # CSS 图标
│   │   ├── <div i-mdi-home />            # Attributify
│   │   └── <div class="i-[mdi--home]" />  # 动态
│   └── 自定义图标集             
│       └── collections: { custom: {...} }
│
├── @unocss/preset-attributify    # 属性化模式
│   ├── <div m-4 p-2 bg-blue />  # 类名作为属性
│   ├── 前缀支持                 
│   │   └── <div uno-m-4 />      # 避免冲突
│   └── 布尔属性                 
│       └── <button disabled bg-gray-500 />
│
├── @unocss/preset-typography     # 排版预设
│   └── prose 类                 # 类似 @tailwindcss/typography
│
├── @unocss/preset-web-fonts      # Web 字体
│   ├── Google Fonts             # 内置支持
│   ├── Bunny Fonts              # 隐私友好
│   └── 自定义字体提供商          
│
├── @unocss/preset-tagify         # 标签化
│   └── <tag-red-500 />          # 组件化类名
│
└── @unocss/preset-scrollbar       # 滚动条
    └── 类似 tailwind-scrollbar   

社区预设
├── @unocss/preset-daisy          # DaisyUI 兼容
├── @unocss/preset-forms          # 表单预设
├── @unocss/preset-chinese        # 中文排版
└── @unocss/preset-autoprefixer   # Autoprefixer
工具与集成
┌─────────────────────────────────────────────────────────────────┐
│                     UnoCSS 工具链                               │
└─────────────────────────────────────────────────────────────────┘

构建工具集成
├── Vite (官方)
│   └── npm i -D unocss
│   import UnoCSS from 'unocss/vite'
│   plugins: [UnoCSS()]
│
├── Webpack
│   └── npm i -D @unocss/webpack
│
├── Rollup
│   └── npm i -D @unocss/rollup
│
├── Nuxt (官方模块)
│   └── npm i -D @unocss/nuxt
│   modules: ['@unocss/nuxt']
│
├── Astro
│   └── npm i -D @unocss/astro
│   integrations: [UnoCSS()]
│
├── Svelte/SvelteKit
│   └── npm i -D @unocss/svelte-scoped
│
└── 其他
    ├── @unocss/esbuild
    ├── @unocss/rspack
    └── @unocss/farm

CLI 工具
├── @unocss/cli
│   ├── npx unocss "src/**/*" -o output.css
│   ├── --watch 模式
│   └── --minify 压缩
│
├── @unocss/eslint-plugin
│   └── ESLint 规则检查
│
├── @unocss/runtime
│   └── 浏览器运行时生成(CDN 使用)
│
└── @unocss/inspector
    └── 可视化调试工具

VS Code 扩展
├── UnoCSS (官方)
│   ├── 自动补全
│   ├── 悬停预览
│   ├── 颜色预览
│   └── 跳转到定义
│
└── UnoCSS  snippets
    └── 代码片段

4.3 生态系统对比矩阵

类别 Tailwind CSS UnoCSS 胜出
UI 组件库 ⭐⭐⭐⭐⭐ ⭐⭐ Tailwind
官方插件 ⭐⭐⭐⭐⭐ ⭐⭐⭐ Tailwind
工具链成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ Tailwind
IDE 支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐ Tailwind
图标集成 ⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS
配置灵活性 ⭐⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS
现代工具链支持 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS
预设丰富度 ⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS

5. 开发体验详解

5.1 IDE 支持对比

VS Code 功能对比
┌─────────────────────────────────────────────────────────────────┐
│                     VS Code 功能对比                            │
└─────────────────────────────────────────────────────────────────┘

Tailwind CSS IntelliSense (官方)
├── 功能完整度: ⭐⭐⭐⭐⭐
├── 
│   ✅ 自动补全(上下文感知)
│   ✅ 悬停预览(CSS 代码)
│   ✅ 颜色预览(内联方块)
│   ✅ 类名排序(自动)
│   ✅ 语法高亮
│   ✅ 错误提示
│   ✅ 配置文件跳转
│   ✅ 自定义值支持
│   
└── 安装量: 8M+

UnoCSS (官方)
├── 功能完整度: ⭐⭐⭐⭐
├── 
│   ✅ 自动补全
│   ✅ 悬停预览
│   ✅ 颜色预览
│   ✅ 跳转到预设
│   ✅ 快捷方式支持
│   
│   ❌ 类名排序(需配合 Prettier)
│   ❌ 自定义规则预览(有限)
│   
└── 安装量: 800K+

实际使用体验对比:

功能 Tailwind UnoCSS 差异说明
补全速度 ~50ms ~30ms UnoCSS 更快
补全精度 极高 Tailwind 更智能
悬停信息 完整 基本 Tailwind 显示更多
颜色预览 优秀 良好 两者都很好
自定义值 完整支持 部分支持 Tailwind 更强
快捷键 Cmd+K Cmd+G Tailwind 独有
WebStorm 支持
Tailwind CSS
├── 原生支持                       # 内置插件
├── 自动配置检测                   # 开箱即用
├── 完整的代码洞察                 # 导航、重构
└── 智能补全                       # 项目感知

UnoCSS
├── 社区插件                       # 非官方
├── 基本支持                       # 有限的补全
└── 需手动配置                     # 不如 Tailwind 完善

5.2 代码示例深度对比

案例 1:卡片组件

Tailwind CSS 实现:

// Card.jsx
import { twMerge } from 'tailwind-merge'
import { clsx, type ClassValue } from 'clsx'

// 工具函数(常用模式)
function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export function Card({ 
  children, 
  className,
  variant = 'default',
  size = 'md',
  interactive = false,
  ...props 
}) {
  return (
    <div
      className={cn(
        // 基础样式
        'rounded-lg border bg-card text-card-foreground shadow-sm',
        
        // 尺寸变体
        size === 'sm' && 'p-3',
        size === 'md' && 'p-6',
        size === 'lg' && 'p-8',
        
        // 颜色变体
        variant === 'default' && 'border-border bg-white',
        variant === 'outline' && 'border-2 border-dashed',
        variant === 'ghost' && 'border-transparent bg-transparent',
        variant === 'destructive' && 'border-red-500 bg-red-50',
        
        // 交互状态
        interactive && [
          'cursor-pointer',
          'transition-all duration-200',
          'hover:shadow-md hover:border-gray-300',
          'active:scale-[0.98]',
          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2'
        ],
        
        // 传入的类名覆盖className
      )}
      {...props}
    >
      {children}
    </div>
  )
}

UnoCSS 实现:

// Card.jsx
// 使用 Attributify 预设 + 快捷方式

// uno.config.ts 中定义
const shortcuts = {
  'card': 'rounded-lg border bg-card text-card-foreground shadow-sm',
  'card-sm': 'card p-3',
  'card-md': 'card p-6',
  'card-lg': 'card p-8',
  'card-interactive': 'cursor-pointer transition-all duration-200 hover:shadow-md hover:border-gray-300 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
}

// 组件使用
export function Card({ 
  children, 
  className,
  variant = 'default',
  size = 'md',
  interactive = false,
  ...props 
}) {
  const variantStyles = {
    default: 'border-border bg-white',
    outline: 'border-2 border-dashed',
    ghost: 'border-transparent bg-transparent',
    destructive: 'border-red-500 bg-red-50'
  }
  
  return (
    <div
      class={[
        `card-${size}`,
        variantStyles[variant],
        interactive && 'card-interactive',
        className
      ].filter(Boolean).join(' ')}
      {...props}
    >
      {children}
    </div>
  )
}

// 或者使用 Attributify 模式
export function CardAttributify({ children, ...props }) {
  return (
    <div 
      p-6 rounded-lg border bg-white shadow-sm
      hover:shadow-md transition-shadow
      {...props}
    >
      {children}
    </div>
  )
}
案例 2:表单输入组件

Tailwind CSS 实现:

// Input.jsx
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'

export const Input = forwardRef(({
  className,
  type = 'text',
  error,
  disabled,
  ...props
}, ref) => {
  return (
    <input
      type={type}
      className={cn(
        // 基础样式
        'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2',
        'text-sm ring-offset-background file:border-0 file:bg-transparent',
        'file:text-sm file:font-medium placeholder:text-muted-foreground',
        
        // 焦点状态
        'focus-visible:outline-none focus-visible:ring-2',
        'focus-visible:ring-ring focus-visible:ring-offset-2',
        
        // 禁用状态
        disabled && 'cursor-not-allowed opacity-50',
        
        // 错误状态
        error && [
          'border-red-500',
          'focus-visible:ring-red-500',
          'placeholder:text-red-300'
        ],
        
        // 过渡动画
        'transition-colors duration-200',
        
        className
      )}
      disabled={disabled}
      ref={ref}
      {...props}
    />
  )
})
Input.displayName = 'Input'

UnoCSS 实现(使用 @unocss/preset-forms):

// Input.jsx
// 使用 preset-forms 预设

export const Input = forwardRef(({
  className,
  type = 'text',
  error,
  disabled,
  ...props
}, ref) => {
  return (
    <input
      type={type}
      class={cn(
        // 使用预设的表单样式
        'form-input',
        
        // 自定义覆盖
        'w-full h-10 px-3 py-2',
        'rounded-md border border-gray-300',
        'text-sm placeholder-gray-400',
        'focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20',
        'transition-all duration-200',
        
        // 状态
        disabled && 'opacity-50 cursor-not-allowed',
        error && 'border-red-500 focus:border-red-500 focus:ring-red-500/20',
        
        className
      )}
      disabled={disabled}
      ref={ref}
      {...props}
    />
  )
})
案例 3:响应式导航栏

Tailwind CSS 实现:

// Navbar.jsx
export function Navbar() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <nav className="bg-white shadow-md">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between h-16">
          {/* Logo */}
          <div className="flex items-center">
            <a href="/" className="text-xl font-bold text-gray-800">
              Logo
            </a>
          </div>
          
          {/* Desktop Menu */}
          <div className="hidden md:flex items-center space-x-4">
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium transition-colors"
              >
                {item}
              </a>
            ))}
            <button className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
              登录
            </button>
          </div>
          
          {/* Mobile Menu Button */}
          <div className="flex items-center md:hidden">
            <button
              onClick={() => setIsOpen(!isOpen)}
              className="text-gray-600 hover:text-gray-900 p-2"
            >
              <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                {isOpen ? (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                ) : (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
                )}
              </svg>
            </button>
          </div>
        </div>
        
        {/* Mobile Menu */}
        <div className={`md:hidden ${isOpen ? 'block' : 'hidden'}`}>
          <div className="px-2 pt-2 pb-3 space-y-1">
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                className="text-gray-600 hover:text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium"
              >
                {item}
              </a>
            ))}
          </div>
        </div>
      </div>
    </nav>
  )
}

UnoCSS 实现:

// Navbar.jsx
export function Navbar() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <nav bg-white shadow-md>
      <div max-w-7xl mx-auto px-4 sm:px-6 lg:px-8>
        <div flex justify-between h-16>
          {/* Logo */}
          <div flex items-center>
            <a href="/" text-xl font-bold text-gray-800>
              Logo
            </a>
          </div>
          
          {/* Desktop Menu */}
          <div hidden md:flex items-center space-x-4>
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium transition-colors
              >
                {item}
              </a>
            ))}
            
            <button 
              bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium 
              hover:bg-blue-700 transition-colors
            >
              登录
            </button>
          </div>
          
          {/* Mobile Menu Button */}
          <div flex items-center md:hidden>
            <button
              onClick={() => setIsOpen(!isOpen)}
              text-gray-600 hover:text-gray-900 p-2
            >
              <div className={isOpen ? 'i-mdi-close' : 'i-mdi-menu'} text-2xl>
              </div>
            </button>
          </div>
        </div>
        
        {/* Mobile Menu */}
        <div md:hidden block={isOpen}>
          <div px-2 pt-2 pb-3 space-y-1>
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                text-gray-600 hover:text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium
              >
                {item}
              </a>
            ))}
          </div>
        </div>
      </div>
    </nav>
  )
}

5.3 Attributify 模式详解

UnoCSS 的 Attributify 预设是其独特功能,可以将类名作为 HTML 属性使用。

传统写法 vs Attributify:

<!-- 传统 Tailwind/UnoCSS -->
<div class="m-4 p-4 bg-blue-500 text-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
  传统写法
</div>

<!-- UnoCSS Attributify 模式 -->
<div
  m-4
  p-4
  bg-blue-500
  text-white
  rounded-lg
  shadow-md
  hover:shadow-lg
  transition-shadow
>
  Attributify 写法
</div>

<!-- 分组写法(更清晰) -->
<div
  m="4"
  p="4"
  bg="blue-500"
  text="white"
  rounded="lg"
  shadow="md hover:lg"
  transition="shadow"
>
  分组写法
</div>

<!-- 复杂示例 -->
<button
  flex items-center justify-center
  gap-2
  px-6 py-3
  bg="blue-600 hover:blue-700"
  text="white"
  font="medium"
  rounded="md"
  transition="all duration-200"
  disabled:opacity-50
  cursor="pointer disabled:not-allowed"
>
  提交
</button>

Attributify 配置:

// uno.config.ts
import { defineConfig, presetUno, presetAttributify } from 'unocss'

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify({
      // 前缀(可选)
      prefix: 'uno-',
      
      // 前缀(可选)
      prefixedOnly: false,
      
      // 忽略的属性
      ignoreAttributes: ['label']
    })
  ]
})

5.4 图标集成对比

Tailwind CSS 图标方案
// 方案 1:使用 SVG 图标
import { HomeIcon } from '@heroicons/react/24/outline'

function IconDemo() {
  return (
    <div className="flex items-center gap-2">
      <HomeIcon className="w-6 h-6 text-blue-500" />
      <span>首页</span>
    </div>
  )
}

// 方案 2:使用图标字体(如 Font Awesome)
// 需要单独引入 CSS
function FontDemo() {
  return (
    <div className="flex items-center gap-2">
      <i className="fas fa-home text-blue-500 text-xl"></i>
      <span>首页</span>
    </div>
  )
}

// 方案 3:内联 SVG
function InlineSvgDemo() {
  return (
    <div className="flex items-center gap-2">
      <svg className="w-6 h-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
      </svg>
      <span>首页</span>
    </div>
  )
}
UnoCSS 图标方案
// 使用 @unocss/preset-icons(推荐)

// 基础用法
function IconDemo() {
  return (
    <div flex items-center gap-2>
      <div className="i-mdi-home w-6 h-6 text-blue-500" />
      <span>首页</span>
    </div>
  )
}

// Attributify 写法
function AttributifyIconDemo() {
  return (
    <div flex items-center gap-2>
      <div i-mdi-home w-6 h-6 text-blue-500 />
      <span>首页</span>
    </div>
  )
}

// 使用不同图标集
function MultiIconDemo() {
  return (
    <div flex gap-4>
      {/* Material Design */}
      <div i-mdi-home w-6 h-6 />
      
      {/* Phosphor Icons */}
      <div i-ph-house w-6 h-6 />
      
      {/* Heroicons */}
      <div i-heroicons-home w-6 h-6 />
      
      {/* Lucide */}
      <div i-lucide-home w-6 h-6 />
      
      {/* Tabler */}
      <div i-tabler-home w-6 h-6 />
    </div>
  )
}

// 动态图标
function DynamicIcon({ name, iconSet = 'mdi' }) {
  return (
    <div className={`i-${iconSet}-${name} w-6 h-6`} />
  )
}

// 使用自定义图标
function CustomIconDemo() {
  return (
    <div i-custom-logo w-8 h-8 />
  )
}

UnoCSS 图标配置:

// uno.config.ts
import { defineConfig, presetUno, presetIcons } from 'unocss'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'

export default defineConfig({
  presets: [
    presetUno(),
    presetIcons({
      // 缩放比例
      scale: 1.2,
      
      // 额外 CSS 属性
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle'
      },
      
      // 自定义图标集
      collections: {
        // 从文件系统加载
        custom: FileSystemIconLoader('./assets/icons'),
        
        // 内联 SVG
        inline: {
          'logo': '<svg viewBox="0 0 24 24">...',
          'arrow': '<svg viewBox="0 0 24 24">...'
        }
      },
      
      // 自动安装图标集(开发模式)
      autoInstall: true,
      
      // 警告未找到的图标
      warn: true
    })
  ]
})

支持的图标集(100+):

图标集 前缀 数量
Material Design Icons i-mdi-* 7000+
Phosphor Icons i-ph-* 7000+
Heroicons i-heroicons-* 300+
Lucide i-lucide-* 800+
Tabler Icons i-tabler-* 4000+
Carbon Icons i-carbon-* 2000+
Simple Icons i-simple-icons-* 2500+
Flag Icons i-flag-* 250+


6. 配置系统深度对比

6.1 Tailwind CSS 4.0 配置详解

Tailwind CSS 4.0 引入了 CSS 优先的配置方式,这是与 UnoCSS 最大的区别之一。

CSS 配置文件结构
/* styles.css */
@import "tailwindcss";

/* 主题配置 */
@theme {
  /* 颜色 */
  --color-brand-50: #f0f9ff;
  --color-brand-100: #e0f2fe;
  --color-brand-500: #0ea5e9;
  --color-brand-900: #0c4a6e;
  
  /* 字体 */
  --font-display: "Inter", sans-serif;
  --font-mono: "Fira Code", monospace;
  
  /* 间距 */
  --spacing-18: 4.5rem;
  --spacing-88: 22rem;
  
  /* 断点 */
  --breakpoint-3xl: 1920px;
  
  /* 动画 */
  --animate-fade-up: fade-up 0.5s ease-out;
  
  @keyframes fade-up {
    0% { opacity: 0; transform: translateY(10px); }
    100% { opacity: 1; transform: translateY(0); }
  }
}

/* 基础层 */
@layer base {
  html {
    @apply antialiased;
  }
  
  body {
    @apply bg-gray-50 text-gray-900;
  }
}

/* 组件层 */
@layer components {
  .btn {
    @apply px-4 py-2 rounded-md font-medium transition-colors;
  }
  
  .btn-primary {
    @apply btn bg-brand-500 text-white hover:bg-brand-600;
  }
}

/* 工具层 */
@layer utilities {
  .text-shadow {
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  
  .scrollbar-hide {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
  
  .scrollbar-hide::-webkit-scrollbar {
    display: none;
  }
}
与 JavaScript 配置的对比
特性 CSS 配置 (v4) JS 配置 (v3) 说明
配置位置 @theme 指令 tailwind.config.js v4 更直观
主题继承 自动继承默认主题 需手动 extend v4 更智能
变量类型 CSS 自定义属性 JS 对象 v4 原生支持
运行时修改 支持 不支持 v4 可动态调整
构建工具 更轻量 需要 PostCSS v4 更快速

6.2 UnoCSS 配置系统

UnoCSS 使用 TypeScript/JavaScript 配置,提供了极高的灵活性。

配置文件结构
// uno.config.ts
import { 
  defineConfig, 
  presetUno, 
  presetAttributify, 
  presetIcons,
  presetTypography,
  presetWebFonts,
  transformerDirectives,
  transformerVariantGroup,
  extractorSplit
} from 'unocss'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'

export default defineConfig({
  // 内容扫描配置
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}',
      // 排除某些文件
      '!src/**/*.test.{js,ts}'
    ],
    // 内联内容
    inline: [
      '<div class="p-4 m-2">',
    ]
  },
  
  // 预设列表
  presets: [
    // 核心预设
    presetUno({
      dark: 'class',  // 或 'media'
      attributifyPseudo: true,
    }),
    
    // 属性化模式
    presetAttributify({
      prefix: 'uno-',
      prefixedOnly: false,
    }),
    
    // 图标预设
    presetIcons({
      scale: 1.2,
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle',
      },
      collections: {
        custom: FileSystemIconLoader('./assets/icons'),
        // 内联图标
        inline: {
          logo: '<svg viewBox="0 0 24 24">...</svg>',
        }
      },
      autoInstall: true,
    }),
    
    // 排版预设
    presetTypography({
      cssExtend: {
        'code': {
          color: '#476582',
          backgroundColor: '#f3f4f6',
        }
      }
    }),
    
    // Web 字体
    presetWebFonts({
      provider: 'google',  // 或 'bunny'
      fonts: {
        sans: 'Inter:400,600,800',
        mono: 'Fira Code:400,600',
      }
    }),
  ],
  
  // 自定义规则
  rules: [
    // 静态规则
    ['m-1', { margin: '0.25rem' }],
    ['m-2', { margin: '0.5rem' }],
    
    // 动态规则
    [/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
    [/^p-(\d+)$/, ([, d]) => ({ padding: `${d / 4}rem` })],
    
    // 复杂规则 - 圆角
    [/^rounded-([\w-]+)$/, ([, s]) => {
      const map: Record<string, string> = {
        'sm': '0.125rem',
        'md': '0.375rem',
        'lg': '0.5rem',
        'xl': '0.75rem',
        '2xl': '1rem',
        '3xl': '1.5rem',
        'full': '9999px',
      }
      if (map[s]) {
        return { 'border-radius': map[s] }
      }
    }],
    
    // 使用主题
    [/^text-brand-(\d+)$/, ([, d], { theme }) => {
      const color = theme.colors?.brand?.[d]
      if (color) {
        return { color }
      }
    }],
  ],
  
  // 快捷方式
  shortcuts: {
    // 基础组件
    'btn': 'px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2',
    'btn-primary': 'btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500',
    'btn-secondary': 'btn bg-gray-200 text-gray-800 hover:bg-gray-300',
    'btn-ghost': 'btn hover:bg-gray-100',
    'btn-danger': 'btn bg-red-600 text-white hover:bg-red-700',
    
    // 布局
    'flex-center': 'flex items-center justify-center',
    'flex-between': 'flex items-center justify-between',
    'flex-col-center': 'flex flex-col items-center justify-center',
    
    // 卡片
    'card': 'bg-white rounded-lg shadow-md overflow-hidden',
    'card-hover': 'card hover:shadow-lg transition-shadow',
    
    // 表单
    'input': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
    'input-error': 'input border-red-500 focus:ring-red-500',
    
    // 响应式容器
    'container-fluid': 'w-full px-4 sm:px-6 lg:px-8',
    'container-prose': 'max-w-prose mx-auto px-4',
  },
  
  // 主题配置
  theme: {
    colors: {
      brand: {
        50: '#f0f9ff',
        100: '#e0f2fe',
        200: '#bae6fd',
        300: '#7dd3fc',
        400: '#38bdf8',
        500: '#0ea5e9',
        600: '#0284c7',
        700: '#0369a1',
        800: '#075985',
        900: '#0c4a6e',
        950: '#082f49',
      },
      // 语义化颜色
      primary: 'var(--color-primary)',
      secondary: 'var(--color-secondary)',
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
    },
    spacing: {
      '18': '4.5rem',
      '88': '22rem',
      '128': '32rem',
    },
    breakpoints: {
      'xs': '480px',
      '3xl': '1920px',
      '4xl': '2560px',
    },
    animation: {
      'fade-up': 'fade-up 0.5s ease-out',
      'fade-in': 'fade-in 0.3s ease-out',
      'slide-in': 'slide-in 0.3s ease-out',
    },
    keyframes: {
      'fade-up': {
        '0%': { opacity: '0', transform: 'translateY(10px)' },
        '100%': { opacity: '1', transform: 'translateY(0)' },
      },
      'fade-in': {
        '0%': { opacity: '0' },
        '100%': { opacity: '1' },
      },
      'slide-in': {
        '0%': { transform: 'translateX(-100%)' },
        '100%': { transform: 'translateX(0)' },
      },
    },
  },
  
  // 变体(类似 Tailwind 的 modifiers)
  variants: [
    // 自定义变体
    (matcher) => {
      if (!matcher.startsWith('hover:')) return matcher
      return {
        matcher: matcher.slice(6),
        selector: s => `${s}:hover`,
      }
    },
  ],
  
  // 提取器
  extractors: [
    extractorSplit,
    // 自定义提取器
    {
      name: 'custom',
      extract({ code }) {
        // 自定义类名提取逻辑
        return [...code.matchAll(/class\(['"`]([^'"`]+)['"`]\)/g)]
          .map(m => m[1].split(/\s+/))
          .flat()
      }
    }
  ],
  
  // 安全列表
  safelist: [
    'bg-red-500',
    'text-3xl',
    'lg:text-4xl',
    'animate-fade-up',
    // 动态安全列表
    ...Array.from({ length: 10 }, (_, i) => `p-${i}`),
  ],
  
  // 预检(CSS Reset)
  preflights: [
    {
      getCSS: () => `
        *, *::before, *::after {
          box-sizing: border-box;
          margin: 0;
          padding: 0;
        }
        
        html {
          -webkit-text-size-adjust: 100%;
          -moz-tab-size: 4;
          tab-size: 4;
        }
        
        body {
          line-height: inherit;
        }
      `
    }
  ],
  
  // 后处理
  postprocess: [
    // 自定义后处理器
    (util) => {
      // 修改生成的 CSS
      if (util.selector.includes('important')) {
        util.entries.forEach((entry) => {
          entry[1] = `${entry[1]} !important`
        })
      }
      return util
    }
  ],
  
  // 转换器(Transformers)
  transformers: [
    transformerDirectives(),      // @apply 等指令
    transformerVariantGroup(),    // 变体组 (hover:(bg-red text-white))
  ],
  
  // 配置合并策略
  configDeps: [
    './config/colors.ts',
    './config/spacing.ts',
  ],
})

6.3 配置系统能力对比

动态规则对比

Tailwind CSS 4.0(有限)

/* 使用任意值 */
<div class="w-[123px] h-[calc(100vh-4rem)]">

/* 但无法自定义规则逻辑 */

UnoCSS(完全灵活)

// 完全自定义规则逻辑
rules: [
  // 动态间距
  [/^gap-(\d+)-(\d+)$/, ([, x, y]) => ({
    gap: `${x}px ${y}px`
  })],
  
  // 复杂计算
  [/^grid-cols-fit-(\d+)$/, ([, min]) => ({
    'grid-template-columns': `repeat(auto-fit, minmax(${min}px, 1fr))`
  })],
  
  // 条件规则
  [/^if-(\w+):(.*)$/, ([, condition, className], { theme }) => {
    if (theme.conditions?.[condition]) {
      return { [className]: theme.conditions[condition] }
    }
  }],
]
快捷方式对比
特性 Tailwind v4 UnoCSS
定义位置 @layer components shortcuts 配置
参数支持 有限(@apply) 完整(函数支持)
嵌套能力 一层 无限嵌套
动态生成 不支持 支持

UnoCSS 高级快捷方式

shortcuts: [
  // 静态快捷方式
  ['btn', 'px-4 py-2 rounded font-medium'],
  
  // 动态快捷方式
  [/^btn-(.*)$/, ([, c], { theme }) => {
    if (theme.colors[c]) {
      return `bg-${c}-500 text-white hover:bg-${c}-600`
    }
  }],
  
  // 嵌套快捷方式
  {
    'card': 'bg-white rounded-lg shadow-md',
    'card-interactive': 'card hover:shadow-lg transition-shadow cursor-pointer',
    'card-interactive-primary': 'card-interactive border-2 border-blue-500',
  },
]

7. 实战案例

7.1 企业级设计系统构建

使用 Tailwind CSS 4.0 构建
// design-system/index.ts
// 基于 Tailwind CSS 4.0 的设计系统

export const designTokens = {
  colors: {
    brand: {
      50: '#f0f9ff',
      500: '#0ea5e9',
      900: '#0c4a6e',
    },
    semantic: {
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
      info: '#3b82f6',
    }
  },
  spacing: {
    '4.5': '1.125rem',
    '18': '4.5rem',
  },
  borderRadius: {
    '4xl': '2rem',
  }
} as const

// components/Button.tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg' | 'xl'
  loading?: boolean
  disabled?: boolean
  children: React.ReactNode
}

export function Button({
  variant = 'primary',
  size = 'md',
  loading,
  disabled,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        // 基础样式
        'inline-flex items-center justify-center gap-2',
        'font-medium transition-all duration-200',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
        'disabled:opacity-50 disabled:cursor-not-allowed',
        
        // 尺寸
        size === 'sm' && 'h-8 px-3 text-sm rounded-md',
        size === 'md' && 'h-10 px-4 text-base rounded-lg',
        size === 'lg' && 'h-12 px-6 text-lg rounded-lg',
        size === 'xl' && 'h-14 px-8 text-xl rounded-xl',
        
        // 变体
        variant === 'primary' && [
          'bg-brand-500 text-white',
          'hover:bg-brand-600',
          'focus-visible:ring-brand-500',
          'active:scale-[0.98]',
        ],
        variant === 'secondary' && [
          'bg-gray-100 text-gray-900',
          'hover:bg-gray-200',
          'focus-visible:ring-gray-500',
        ],
        variant === 'ghost' && [
          'text-gray-700',
          'hover:bg-gray-100',
          'focus-visible:ring-gray-500',
        ],
        variant === 'danger' && [
          'bg-red-500 text-white',
          'hover:bg-red-600',
          'focus-visible:ring-red-500',
        ],
        
        // 加载状态
        loading && 'opacity-70 cursor-wait',
      )}
      disabled={disabled || loading}
      {...props}
    >
      {loading && <Spinner className="w-4 h-4 animate-spin" />}
      {children}
    </button>
  )
}
使用 UnoCSS 构建
// uno.config.ts
// 企业级设计系统配置

import { defineConfig, presetUno, presetAttributify, presetIcons } from 'unocss'

const buttonShortcuts = {
  // 基础按钮
  'btn': 'inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
  
  // 尺寸变体
  'btn-sm': 'btn h-8 px-3 text-sm rounded-md',
  'btn-md': 'btn h-10 px-4 text-base rounded-lg',
  'btn-lg': 'btn h-12 px-6 text-lg rounded-lg',
  'btn-xl': 'btn h-14 px-8 text-xl rounded-xl',
  
  // 颜色变体
  'btn-primary': 'btn-md bg-brand-500 text-white hover:bg-brand-600 focus-visible:ring-brand-500 active:scale-[0.98]',
  'btn-secondary': 'btn-md bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500',
  'btn-ghost': 'btn-md text-gray-700 hover:bg-gray-100 focus-visible:ring-gray-500',
  'btn-danger': 'btn-md bg-red-500 text-white hover:bg-red-600 focus-visible:ring-red-500',
  
  // 状态变体
  'btn-loading': 'opacity-70 cursor-wait',
}

export default defineConfig({
  presets: [
    presetUno({
      dark: 'class',
    }),
    presetAttributify(),
    presetIcons(),
  ],
  
  shortcuts: {
    ...buttonShortcuts,
    
    // 输入框
    'input': 'w-full h-10 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent transition-all',
    'input-error': 'input border-red-500 focus:ring-red-500',
    'input-success': 'input border-green-500 focus:ring-green-500',
    
    // 卡片
    'card': 'bg-white rounded-lg shadow-md overflow-hidden',
    'card-bordered': 'card border border-gray-200',
    'card-hoverable': 'card hover:shadow-lg transition-shadow cursor-pointer',
    
    // 布局
    'page-container': 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8',
    'section': 'py-12 md:py-16 lg:py-20',
    
    // 排版
    'heading-1': 'text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight',
    'heading-2': 'text-3xl md:text-4xl font-bold tracking-tight',
    'heading-3': 'text-2xl md:text-3xl font-semibold',
    'text-body': 'text-base text-gray-600 leading-relaxed',
    'text-small': 'text-sm text-gray-500',
  },
  
  theme: {
    colors: {
      brand: {
        50: '#f0f9ff',
        100: '#e0f2fe',
        200: '#bae6fd',
        300: '#7dd3fc',
        400: '#38bdf8',
        500: '#0ea5e9',
        600: '#0284c7',
        700: '#0369a1',
        800: '#075985',
        900: '#0c4a6e',
        950: '#082f49',
      },
    },
    
    animation: {
      'spin-slow': 'spin 3s linear infinite',
      'bounce-slow': 'bounce 2s infinite',
      'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
    },
  },
})

// components/Button.tsx - 使用 Attributify
export function Button({ variant = 'primary', size = 'md', loading, children, ...props }) {
  const variantClass = `btn-${variant}`
  const sizeClass = size !== 'md' ? `btn-${size}` : ''
  
  return (
    <button
      class={[variantClass, sizeClass, loading && 'btn-loading'].filter(Boolean).join(' ')}
      disabled={loading}
      {...props}
    >
      {loading && <div i-svg-spinners-90-ring-with-bg text-lg animate-spin />}
      {children}
    </button>
  )
}

7.2 性能优化实战

Tailwind CSS 优化策略
/* 1. 使用 CSS 层控制优先级 */
@layer utilities {
  /* 高性能动画 */
  .gpu-accelerated {
    transform: translateZ(0);
    will-change: transform;
  }
  
  /* 减少重绘 */
  .content-visibility {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px;
  }
}

/* 2. 容器查询优化 */
@layer components {
  .card-grid {
    @apply grid gap-4;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  }
  
  @container (min-width: 768px) {
    .card-grid {
      grid-template-columns: repeat(3, 1fr);
    }
  }
}
// tailwind.config.js - 优化配置
module.exports = {
  // 精确控制扫描范围
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    // 明确排除测试文件
    '!./src/**/*.test.{js,ts}',
    '!./src/**/*.spec.{js,ts}',
    '!./src/**/__tests__/**',
  ],
  
  // 仅启用需要的核心插件
  corePlugins: {
    container: false,  // 使用自定义容器
    float: false,      // 使用 flex/grid
    clear: false,
    objectFit: true,
    objectPosition: true,
    // ... 按需启用
  },
  
  // 自定义提取器
  content: {
    files: ['./src/**/*.{js,ts,jsx,tsx}'],
    extract: {
      tsx: (content) => {
        // 更精确的类名提取
        return [...content.matchAll(/className=(?:["']([^"']+)["']|\{`([^`]+)`\})/g)]
          .flatMap(match => (match[1] || match[2]).split(/\s+/))
          .filter(Boolean)
      }
    }
  },
}
UnoCSS 优化策略
// uno.config.ts - 性能优化配置

export default defineConfig({
  // 1. 精确的内容匹配
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte}',
    ],
    // 自定义提取逻辑
    pipeline: {
      include: [/\.vue$/, /\.tsx?$/],
      exclude: [/node_modules/, /\.git/, /test/],
    }
  },
  
  // 2. 选择器合并优化
  mergeSelectors: true,
  
  // 3. 最小化输出
  minify: process.env.NODE_ENV === 'production',
  
  // 4. 安全列表优化 - 仅保留必要的
  safelist: [
    // 动态类名
    ...Array.from({ length: 5 }, (_, i) => `col-span-${i + 1}`),
    // 主题切换
    'dark',
    'light',
  ],
  
  // 5. 后处理优化
  postprocess: [
    // 移除无用的前缀
    (util) => {
      util.entries = util.entries.filter(([key]) => 
        !key.startsWith('-webkit-') || key === '-webkit-appearance'
      )
      return util
    }
  ],
  
  // 6. 提取器优化
  extractors: [
    {
      name: 'optimized',
      order: 0,
      extract({ code }) {
        // 预过滤,减少正则匹配次数
        if (!code.includes('class') && !code.includes('className')) {
          return []
        }
        // 高效的提取逻辑
        return [...code.matchAll(/(?:class|className)=(?:["']([^"']+)["']|\{`([^`]+)`\})/g)]
          .flatMap(m => (m[1] || m[2]).split(/\s+/))
          .filter(c => c.length > 0 && !c.includes('${'))
      }
    }
  ],
})

7.3 大型项目架构对比

Tailwind CSS 项目结构

project-tailwind/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   └── index.ts
│   ├── styles/
│   │   ├── globals.css       # @import "tailwindcss"
│   │   ├── components.css    # @layer components
│   │   └── utilities.css     # @layer utilities
│   ├── app/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── lib/
│       └── utils.ts          # cn() 函数
├── tailwind.config.ts        # 主题配置
└── package.json

UnoCSS 项目结构

project-unocss/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   └── index.ts
│   ├── app/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── lib/
│       └── utils.ts
├── uno.config.ts             # 核心配置(包含主题、规则、快捷方式)
├── presets/
│   ├── shortcuts.ts          # 快捷方式定义
│   ├── rules.ts              # 自定义规则
│   └── theme.ts              # 主题配置
└── package.json

8. 最佳实践

8.1 代码组织

Tailwind CSS 推荐模式

// components/ui/Button.tsx
import { cn } from '@/lib/utils'
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 whitespace-nowrap',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 px-3',
        lg: 'h-11 px-8',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

UnoCSS 推荐模式

// 1. 配置集中管理
// uno.config.ts
shortcuts: {
  // 使用语义化命名
  'btn': 'inline-flex items-center justify-center gap-2',
  'btn-primary': 'btn bg-blue-600 text-white hover:bg-blue-700',
  'btn-secondary': 'btn bg-gray-200 text-gray-800 hover:bg-gray-300',
}

// 2. 组件中使用
// components/Button.tsx
export function Button({ variant = 'primary', size = 'md', children }) {
  return (
    <button class={`btn-${variant} btn-${size}`}>
      {children}
    </button>
  )
}

// 3. Attributify 模式(可选)
// components/Card.tsx
export function Card({ title, children }) {
  return (
    <div 
      bg="white dark:gray-800"
      rounded="lg"
      shadow="md hover:lg"
      p="6"
      transition="shadow"
    >
      <h3 text-xl font-bold mb-4>{title}</h3>
      {children}
    </div>
  )
}

8.2 团队协作规范

Tailwind CSS 团队规范

// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindFunctions": ["cn", "cva"]
}

// .eslintrc
{
  "plugins": ["tailwindcss"],
  "rules": {
    "tailwindcss/classnames-order": "error",
    "tailwindcss/enforces-negative-arbitrary-values": "error",
    "tailwindcss/enforces-shorthand": "error",
    "tailwindcss/migration-from-tailwind-2": "error",
    "tailwindcss/no-arbitrary-value": "off",
    "tailwindcss/no-custom-classname": "off"
  }
}

UnoCSS 团队规范

// uno.config.ts - 团队共享配置
import { defineConfig } from 'unocss'

export default defineConfig({
  // 使用预设确保一致性
  presets: [
    presetUno(),
    presetAttributify({
      prefix: 'uno-',  // 避免冲突
    }),
  ],
  
  // 团队约定的快捷方式
  shortcuts: {
    // 命名规范
    // 1. 组件:单数名词
    'btn': '...',
    'card': '...',
    'input': '...',
    
    // 2. 变体:[组件]-[变体名]
    'btn-primary': '...',
    'btn-danger': '...',
    'card-hover': '...',
    
    // 3. 工具:动词或形容词
    'flex-center': '...',
    'text-truncate': '...',
    'visually-hidden': '...',
  },
  
  // 主题锁定
  theme: {
    colors: {
      // 只允许使用这些颜色
      brand: {
        50: '#f0f9ff',
        500: '#0ea5e9',
        900: '#0c4a6e',
      },
      // 禁止直接使用 tailwind 颜色
      // red: null,
      // blue: null,
    },
  },
})

8.3 深色模式最佳实践

Tailwind CSS v4 实现

/* styles.css */
@import "tailwindcss";

@theme {
  --color-bg-primary: var(--bg-primary);
  --color-text-primary: var(--text-primary);
}

@layer base {
  :root {
    --bg-primary: #ffffff;
    --text-primary: #1f2937;
  }
  
  .dark {
    --bg-primary: #111827;
    --text-primary: #f9fafb;
  }
}

/* 使用 */
<div class="bg-bg-primary text-text-primary">

UnoCSS 实现

// uno.config.ts
export default defineConfig({
  presets: [
    presetUno({
      dark: 'class',  // 或 'media'
    }),
  ],
  
  shortcuts: {
    'bg-primary': 'bg-white dark:bg-gray-900',
    'text-primary': 'text-gray-900 dark:text-gray-100',
    'border-primary': 'border-gray-200 dark:border-gray-800',
  },
})

// 组件中使用
<div class="bg-primary text-primary border-primary">

// 或 Attributify 模式
<div 
  bg="white dark:gray-900"
  text="gray-900 dark:gray-100"
  border="gray-200 dark:gray-800"
>

9. 常见问题与解决方案

9.1 类名冲突问题

问题: Tailwind 和 UnoCSS 类名冲突

解决方案:

// uno.config.ts
export default defineConfig({
  presets: [
    presetUno({
      // 添加前缀避免冲突
      prefix: 'u-',
    }),
  ],
})

// 使用
<div class="u-flex u-p-4 tailwind-class">

9.2 动态类名问题

Tailwind CSS(需要配置)

// tailwind.config.js
module.exports = {
  safelist: [
    // 明确列出动态类名
    'bg-red-500',
    'bg-blue-500',
    'text-lg',
    'text-xl',
    // 使用模式
    { pattern: /bg-(red|blue|green)-(100|500|900)/ },
  ],
}

// 使用
function getColorClass(color) {
  return `bg-${color}-500`  // 可能被 tree-shake
}

UnoCSS(自动处理)

// uno.config.ts
export default defineConfig({
  // 提取器会自动处理
  // 只需确保内容扫描包含动态类名
  content: {
    filesystem: ['src/**/*.{js,ts,jsx,tsx}'],
    // 如果需要,添加安全列表
    safelist: [
      ...['red', 'blue', 'green'].flatMap(c => 
        [100, 500, 900].map(n => `bg-${c}-${n}`)
      ),
    ],
  },
})

// 使用
function getColorClass(color) {
  return `bg-${color}-500`  // 会被自动检测
}

9.3 VS Code 智能提示失效

Tailwind CSS

// .vscode/settings.json
{
  "tailwindCSS.includeLanguages": {
    "plaintext": "html",
    "vue": "html",
    "svelte": "html"
  },
  "tailwindCSS.experimental.classRegex": [
    ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
    ["cva\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)", "(?:'|\"|`)([^']*)(?:'|\"|`)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ]
}

UnoCSS

// .vscode/settings.json
{
  "unocss.root": "./uno.config.ts",
  "unocss.include": [
    "src/**/*.{html,js,ts,jsx,tsx,vue,svelte}"
  ]
}

9.4 构建失败问题

Tailwind CSS 常见问题

# 错误:Content 路径配置错误
# 解决:检查 tailwind.config.js 中的 content 配置

# 错误:PostCSS 配置问题
# 解决:确保 postcss.config.js 正确配置
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

# 错误:找不到 CSS 文件
# 解决:确保在入口文件导入 CSS
import './styles/globals.css'

UnoCSS 常见问题

# 错误:虚拟模块未找到
# 解决:确保导入虚拟模块
import 'virtual:uno.css'

# 错误:Vite 配置问题
# 解决:确保插件顺序正确
import { defineConfig } from 'vite'
import UnoCSS from 'unocss/vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    UnoCSS(),  // 放在框架插件之后
  ],
})

10. 迁移指南

10.1 从 Tailwind CSS v3 迁移到 v4

# 1. 升级依赖
npm install tailwindcss@latest

# 2. 更新配置文件
# v3: tailwind.config.js
# v4: styles.css (CSS 优先)

# 3. 迁移步骤
/* v3: tailwind.config.js */
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          500: '#0ea5e9',
        }
      }
    }
  }
}

/* v4: styles.css */
@import "tailwindcss";

@theme {
  --color-brand-500: #0ea5e9;
}

10.2 从 Tailwind CSS 迁移到 UnoCSS

迁移检查清单:

- [ ] 安装 UnoCSS 依赖
- [ ] 配置 UnoCSS(使用 preset-wind 保持兼容)
- [ ] 迁移自定义配置到 uno.config.ts
- [ ] 检查第三方插件兼容性
- [ ] 测试所有组件
- [ ] 优化性能

详细步骤:

// 1. 安装依赖
// npm install -D unocss @unocss/preset-wind

// 2. 配置兼容模式
// uno.config.ts
import { defineConfig, presetWind, presetAttributify } from 'unocss'

export default defineConfig({
  presets: [
    // 使用 Wind 预设保持 100% 兼容
    presetWind(),
    // 可选:启用 Attributify
    presetAttributify(),
  ],
  
  // 3. 迁移主题配置
  theme: {
    // 从 tailwind.config.js 复制
    colors: {
      brand: {
        50: '#f0f9ff',
        500: '#0ea5e9',
      }
    },
    extend: {
      spacing: {
        '18': '4.5rem',
      }
    }
  },
  
  // 4. 迁移自定义类
  shortcuts: {
    // 从 @layer components
    'btn-primary': 'bg-brand-500 text-white hover:bg-brand-600',
  },
})

// 3. 更新构建配置
// vite.config.ts
import UnoCSS from 'unocss/vite'

export default {
  plugins: [
    UnoCSS(),
  ]
}

// 4. 更新入口文件
// main.ts
import 'virtual:uno.css'  // 替换掉 tailwind.css

10.3 从 UnoCSS 迁移到 Tailwind CSS

# 这种情况较少见,通常是团队要求统一技术栈

# 1. 安装 Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# 2. 配置 Tailwind
# tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      // 从 uno.config.ts 迁移
    },
  },
}

# 3. 创建 CSS 文件
# src/styles/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;

# 4. 更新所有组件
# 将 UnoCSS 快捷方式转换为 Tailwind 类名

11. 未来发展趋势

11.1 Tailwind CSS 路线图

v4.1+ 预期功能:

  • 更完善的 CSS 原生配置支持
  • 更好的容器查询集成
  • 增强的动画工具
  • 改进的深色模式切换

长期方向:

  • 与原生 CSS 标准更深度的集成
  • 零 JavaScript 运行时依赖
  • 更好的性能优化

11.2 UnoCSS 路线图

近期功能:

  • 更多官方预设
  • 改进的 VS Code 体验
  • 更强的类型安全

长期方向:

  • 成为构建工具的默认选择
  • 更广泛的框架集成
  • 社区预设生态扩张

11.3 技术趋势预测

2024-2025 年趋势:
├── CSS 原生能力提升
│   ├── @property 更广泛支持
│   ├── color-mix() 普及
│   └── 容器查询标准化
│
├── 构建工具演进
│   ├── Rspack/SWC 更普及
│   ├── 更快的构建速度
│   └── 更智能的 tree-shaking
│
└── 原子化 CSS 主流化
    ├── 更多框架采用
    ├── 标准化工具链
    └── 更好的开发体验

12. 总结与建议

12.1 决策矩阵

项目特征 推荐选择 理由
企业级大型项目 Tailwind CSS 生态完善、团队熟悉度高
初创/小型项目 UnoCSS 启动快速、配置简单
追求极致性能 UnoCSS 构建速度快、运行时高效
需要丰富 UI 组件 Tailwind CSS shadcn/ui 等生态成熟
高度定制化需求 UnoCSS 动态规则、灵活配置
团队技术栈现代 UnoCSS 与现代工具链集成好
长期维护考虑 Tailwind CSS 稳定性高、社区活跃
快速原型开发 两者皆可 都支持快速开发

12.2 混合使用策略

渐进式迁移方案:

阶段 1:新项目直接使用 UnoCSS
阶段 2:旧项目逐步引入 UnoCSS
阶段 3:统一技术栈

具体做法:
1. 使用 preset-wind 保持兼容
2. 逐步迁移组件
3. 统一配置管理

12.3 最终建议

选择 Tailwind CSS 4.0,如果你:

  • 需要稳定、成熟的解决方案
  • 团队对 Tailwind 已有经验
  • 依赖丰富的 UI 组件生态
  • 需要长期的项目维护支持

选择 UnoCSS,如果你:

  • 追求极致的开发体验
  • 需要高度定制化的配置
  • 使用现代构建工具(Vite 等)
  • 愿意尝试新技术

无论选择哪个,都要:

  • 建立团队规范
  • 使用类型安全工具
  • 关注性能优化
  • 保持配置的一致性

TCP 与 UDP 核心差异及面试高分指南

在计算机网络传输层(Transport Layer),TCP 与 UDP 是两大基石协议。理解二者的底层差异,不仅是网络编程的基础,更是后端架构选型的关键依据。本文剥离冗余表述,直击技术核心。

第一部分:协议本质

  • TCP (Transmission Control Protocol) :一种面向连接的、可靠的、基于字节流的传输层通信协议。
  • UDP (User Datagram Protocol) :一种无连接的、不可靠的、基于数据报的传输层通信协议。

第二部分:核心差异拆解

1. 连接机制

  • TCP面向连接。通信前必须通过三次握手建立连接,结束时需通过四次挥手释放连接。这种机制确保了通信双方的状态同步。
  • UDP无连接。发送数据前不需要建立连接,发送结束也无需关闭。发送端想发就发,接收端有数据就收。

2. 传输模式(核心底层差异)

  • TCP面向字节流 (Byte Stream)

    • TCP 将应用层数据看作一连串无结构的字节流。
    • 无边界:TCP 不保留应用层数据的边界。发送方连续发送两次数据,接收方可能一次收到(粘包),也可能分多次收到(拆包)。因此,应用层必须自行处理粘包/拆包问题(如定义消息长度或分隔符)。
  • UDP面向数据报 (Datagram)

    • UDP 对应用层交下来的报文,不合并、不拆分,保留这些报文的边界
    • 有边界:发送方发一次,接收方就收一次。只要数据包大小不超过 MTU(最大传输单元),UDP 就能保证应用层数据的完整性。

3. 可靠性保障

  • TCP强可靠性。通过以下机制确保数据无差错、不丢失、不重复、按序到达:

    • 序列号 (Sequence Number)  与 确认应答 (ACK)
    • 超时重传机制。
    • 流量控制 (滑动窗口) 与 拥塞控制 (慢启动、拥塞避免、快重传、快恢复)。
  • UDP不可靠性

    • 只负责尽最大努力交付 (Best Effort Delivery)。
    • 不保证数据包顺序,不保证不丢包。
    • 无拥塞控制,网络拥堵时也不会降低发送速率(这对实时应用是优势也是风险)。

4. 头部开销

  • TCP开销大

    • 头部最小长度为 20 字节(不含选项字段),最大可达 60 字节。包含源/目的端口、序列号、确认号、窗口大小、校验和等复杂信息。
  • UDP开销极小

    • 头部固定仅 8 字节。仅包含源端口、目的端口、长度、校验和。这使得 UDP 在网络带宽受限或对传输效率要求极高的场景下更具优势。

5. 传输效率与并发

  • TCP:仅支持点对点 (Unicast) 通信。每条 TCP 连接只能有两个端点。
  • UDP:支持一对一一对多多对一多对多交互通信。原生支持广播 (Broadcast) 和多播 (Multicast)。

第三部分:场景选择

TCP 典型场景

适用于对数据准确性要求高、不能容忍丢包、对速度相对不敏感的场景:

  • HTTP/HTTPS (网页浏览)
  • FTP (文件传输)
  • SMTP/POP3 (邮件传输)
  • SSH (远程登录)

UDP 典型场景

适用于对实时性要求高、能容忍少量丢包、网络开销要求低的场景:

  • DNS (域名解析,要求快速)
  • 直播/视频会议/VoIP (RTP/RTCP,实时性优先)
  • DHCP/SNMP (局域网服务)
  • QUIC/HTTP3 (基于 UDP 实现可靠传输的下一代 Web 协议)

第四部分:面试回答范式

当面试官问到“TCP 和 UDP 的区别”时,建议采用结构化具备演进思维的回答策略。

回答模板:

  1. 先下定义(定基调)
    “TCP 是面向连接的、可靠的字节流协议;而 UDP 是无连接的、不可靠的数据报协议。”

  2. 细述差异(展示底层功底)
    “具体区别主要体现在三个维度:

    • 连接与开销:TCP 需要三次握手,头部最小 20 字节;UDP 无需连接,头部仅 8 字节
    • 数据模式:TCP 是字节流,没有边界,应用层需要处理粘包问题;UDP 是报文,保留边界。
    • 可靠性机制:TCP 有序列号、ACK、拥塞控制来保证有序传输;UDP 则是尽最大努力交付,不保证顺序和完整性。”
  3. 升华主题(架构师视角 - 加分项)
    “值得注意的是,虽然 TCP 可靠,但在弱网环境下存在TCP 队头阻塞(Head-of-Line Blocking)问题(即一个包丢失导致后续所有包等待)。
    这也是为什么最新的 HTTP/3 (QUIC)  协议选择基于 UDP 来构建。QUIC 在应用层实现了可靠性和拥塞控制,既利用了 UDP 的低延迟和无队头阻塞优势,又保证了数据的可靠传输。这是当前传输层协议演进的一个重要趋势。”

第五部分:总结对比表

维度 TCP UDP
连接性 面向连接 (三次握手/四次挥手) 无连接
可靠性 高 (无差错、不丢失、不重复、有序) 低 (尽最大努力交付)
传输模式 字节流 (无边界,需处理粘包) 数据报 (有边界)
头部开销 20 ~ 60 字节 固定 8 字节
传输效率 较低 (需维护连接状态、拥塞控制) 很高 (无连接、无控制)
并发支持 仅点对点 支持广播、多播、单播
拥塞控制 有 (慢启动、拥塞避免等)

HTTP 协议演进史:从 1.0 到 2.0

HTTP 协议的演进本质是追求传输效率与资源利用率的平衡。本文剖析从 1.0 到 2.0 的技术迭代逻辑。

第一部分:HTTP 1.0 —— 基础与瓶颈

HTTP 1.0 确立了请求-响应模型,但其设计初衷仅为传输简单的超文本内容。

核心机制

  • 短连接(Short Connection) :默认采用“一求一连”模式。浏览器每次请求资源,都需要与服务器建立一个 TCP 连接,传输完成后立即断开。
  • 无状态(Stateless) :服务器不跟踪客户端状态,每次请求都是独立的。

致命缺陷

  1. TCP 连接成本极高
    每个请求都需要经历 三次握手 和 四次挥手。在加载包含数十个资源(图片、CSS、JS)的现代网页时,连接建立的耗时甚至超过数据传输本身。
  2. 严重的队头阻塞(Head-of-Line Blocking)
    由于无法复用连接,前一个请求未处理完成前,后续请求无法发送(虽然可以通过浏览器开启多个并行连接缓解,但数量有限)。
  3. 缓存控制简陋
    主要依赖 Expires 和 Last-Modified,缺乏精细的控制策略。

第二部分:HTTP 1.1 —— 性能优化标准

HTTP 1.1 旨在解决 1.0 的连接效率问题,是当前互联网使用最广泛的协议版本。

核心改进

  1. 持久连接(Persistent Connection)

    • 引入 Keep-Alive 机制,且默认开启。
    • 允许多个 HTTP 请求复用同一个 TCP 连接,显著减少了 TCP 握手开销和慢启动(Slow Start)的影响。
  2. 管道化(Pipelining)

    • 允许客户端在收到上一个响应前发送下一个请求。
    • 痛点现状:服务器必须按请求顺序返回响应。若第一个请求处理阻塞,后续响应都会被拖延。因此,主流浏览器默认禁用此功能。
  3. 虚拟主机(Virtual Host)

    • 引入 Host 头部字段。
    • 允许在同一台物理服务器(同一 IP)上托管多个域名,是现代云主机和负载均衡的基础。
  4. 功能增强

    • 断点续传:引入 Range 头,支持只请求资源的某一部分(如 206 Partial Content)。
    • 缓存增强:引入 Cache-Control、ETag 等机制,提供更复杂的缓存策略。

遗留问题

  • 应用层队头阻塞:虽然 TCP 连接复用了,但 HTTP 请求依然是串行的。一旦某个请求发生阻塞,整个管道停滞。
  • 头部冗余:Cookie 和 User-Agent 等头部信息在每次请求中重复传输,且未经压缩,浪费带宽。
  • 文本协议解析低效:基于文本的解析容易出错且效率低于二进制解析。

第三部分:HTTP 2.0 —— 架构级变革

HTTP 2.0 并非简单的功能修补,而是对传输层的重新设计,旨在突破 HTTP 1.x 的性能天花板。

核心技术

  1. 二进制分帧(Binary Framing)

    • 机制:抛弃 ASCII 文本,将所有传输信息分割为更小的消息和帧,并采用二进制编码。
    • 价值:计算机解析二进制数据的效率远高于文本,且容错率更高。
  2. 多路复用(Multiplexing)

    • 机制:基于二进制分帧,允许在同一个 TCP 连接中同时发送多个请求和响应。数据流(Stream)被打散为帧(Frame)乱序发送,接收端根据帧首部的流标识(Stream ID)进行重组。
    • 价值:彻底解决了 应用层的队头阻塞 问题,实现了真正的并发传输。
  3. 头部压缩(HPACK)

    • 机制:通信双方维护一张静态字典和动态字典。
    • 价值:传输时仅发送索引号或差异数据,极大减少了 Header 的传输体积(尤其是 Cookie 较大的场景)。
  4. 服务端推送(Server Push)

    • 服务器可在客户端请求 HTML 时,主动推送后续可能需要的 CSS 或 JS 资源,减少往返延迟(RTT)。

第四部分:总结对比

维度 HTTP 1.0 HTTP 1.1 HTTP 2.0
连接管理 短连接(每请求新建 TCP) 长连接(Keep-Alive 复用) 多路复用(单 TCP 连接并发)
数据格式 文本 文本 二进制(帧)
并发机制 管道化(常被禁用,存在阻塞) 多路复用(真正并发)
头部处理 原文传输 原文传输 HPACK 算法压缩
主机支持 单一主机 虚拟主机(Host 头) 虚拟主机
内容获取 完整获取 断点续传(Range) 断点续传

构建无障碍组件之Accordion Pattern

Accordion Pattern 详解:构建垂直堆叠的展开收起组件

Accordion(手风琴)是一种常见的交互组件,由垂直堆叠的可交互标题组成,每个标题包含一个内容部分的标题、摘要或缩略图。本文基于 W3C WAI-ARIA Accordion Pattern 规范,详解如何构建无障碍的 Accordion 组件。

一、Accordion 的定义与核心概念

Accordion 是一组垂直堆叠的交互式标题,每个标题都包含一个内容部分的标题、摘要或缩略图。标题作为控件,允许用户显示或隐藏其关联的内容部分。

Accordion 常用于在单个页面上呈现多个内容部分时减少滚动需求。

1.1 核心术语

  • Accordion Header(手风琴标题):内容部分的标签或缩略图,同时作为显示(在某些实现中也包括隐藏)内容部分的控件
  • Accordion Panel(手风琴面板):与手风琴标题关联的内容部分

在某些 Accordion 中,手风琴标题旁边始终可见额外的元素。例如,每个手风琴标题可能伴随一个菜单按钮,用于提供适用于该部分的操作访问。

二、WAI-ARIA 角色与属性

2.1 基本角色

每个手风琴标题的内容包含在具有 role="button" 的元素中。

2.2 标题层级

每个手风琴标题按钮包装在具有 role="heading" 的元素中,并设置适合页面信息架构的 aria-level 值:

  • 如果原生宿主语言具有隐式标题和 aria-level 的元素(如 HTML 标题标签),可以使用原生宿主语言元素
  • 按钮元素是标题元素内部的唯一元素
<!-- 手风琴标题 -->
<h3>
  <button aria-expanded="true" aria-controls="panel-1" id="accordion-header-1">
    第一部分标题
  </button>
</h3>

<!-- 手风琴面板 -->
<div id="panel-1" role="region" aria-labelledby="accordion-header-1">
  <p>第一部分的内容...</p>
</div>

2.3 状态属性

  • aria-expanded:如果与手风琴标题关联的面板可见,设置为 true;如果面板不可见,设置为 false
  • aria-controls:设置为包含手风琴面板内容的元素的 ID
  • aria-disabled:如果与手风琴标题关联的面板可见,且手风琴不允许折叠该面板,则设置为 true

2.4 区域角色(可选)

每个作为面板内容容器的元素可以具有 role="region"aria-labelledby,其值引用控制面板显示的按钮:

  • 避免在会创建过多地标区域的情况下使用 region 角色,例如在可以同时展开超过约 6 个面板的手风琴中
  • 当面板包含标题元素或嵌套手风琴时,region 角色对屏幕阅读器用户感知结构特别有帮助
<!-- 手风琴标题按钮 -->
<h3>
  <button aria-expanded="true" aria-controls="panel-1" id="header-1">
    面板标题
  </button>
</h3>

<!-- 手风琴面板内容 -->
<div role="region" aria-labelledby="header-1" id="panel-1">
  <p>面板内容...</p>
</div>

三、键盘交互规范

3.1 基本键盘操作

按键 功能
Enter 或 Space 当焦点位于折叠面板的手风琴标题上时,展开关联面板。如果实现只允许一个面板展开,且另一个面板已展开,则折叠该面板
Tab 将焦点移动到下一个可聚焦元素;手风琴中的所有可聚焦元素都包含在页面 Tab 序列中
Shift + Tab 将焦点移动到上一个可聚焦元素;手风琴中的所有可聚焦元素都包含在页面 Tab 序列中

3.2 可选键盘操作

按键 功能
Down Arrow 如果焦点在手风琴标题上,将焦点移动到下一个手风琴标题。如果焦点在最后一个手风琴标题上,要么不执行任何操作,要么将焦点移动到第一个手风琴标题
Up Arrow 如果焦点在手风琴标题上,将焦点移动到上一个手风琴标题。如果焦点在第一个手风琴标题上,要么不执行任何操作,要么将焦点移动到最后一个手风琴标题
Home 当焦点在手风琴标题上时,将焦点移动到第一个手风琴标题
End 当焦点在手风琴标题上时,将焦点移动到最后一个手风琴标题

四、实现方式

4.1 基础结构

<div class="accordion">
  <!-- 第一部分 -->
  <h3>
    <button 
      aria-expanded="true" 
      aria-controls="section1"
      id="accordion-header-1">
      第一部分标题
    </button>
  </h3>
  <div 
    id="section1" 
    role="region" 
    aria-labelledby="accordion-header-1">
    <p>第一部分的内容...</p>
  </div>

  <!-- 第二部分 -->
  <h3>
    <button 
      aria-expanded="false" 
      aria-controls="section2"
      id="accordion-header-2">
      第二部分标题
    </button>
  </h3>
  <div 
    id="section2" 
    role="region" 
    aria-labelledby="accordion-header-2"
    hidden>
    <p>第二部分的内容...</p>
  </div>
</div>

4.2 单展开模式

在单展开模式下,一次只能展开一个面板:

<div class="accordion" data-accordion-single>
  <h3>
    <button 
      aria-expanded="true" 
      aria-controls="panel-1"
      aria-disabled="true">
      始终展开的面板
    </button>
  </h3>
  <div id="panel-1" role="region">
    <p>此面板无法折叠...</p>
  </div>
  
  <h3>
    <button 
      aria-expanded="false" 
      aria-controls="panel-2">
      可切换的面板
    </button>
  </h3>
  <div id="panel-2" role="region" hidden>
    <p>点击上方标题可展开此面板...</p>
  </div>
</div>

4.3 多展开模式

在多展开模式下,可以同时展开多个面板:

<div class="accordion" data-accordion-multiple>
  <h3>
    <button aria-expanded="true" aria-controls="multi-1">
      第一个面板
    </button>
  </h3>
  <div id="multi-1" role="region">
    <p>第一个面板内容...</p>
  </div>
  
  <h3>
    <button aria-expanded="true" aria-controls="multi-2">
      第二个面板(也可同时展开)
    </button>
  </h3>
  <div id="multi-2" role="region">
    <p>第二个面板内容...</p>
  </div>
</div>

4.4 使用原生 HTML <details> + name 实现

HTML5.2 起,<details> 元素支持 name 属性,可以实现原生的单展开模式(Accordion 效果),无需 JavaScript:

<details name="accordion-group" open>
  <summary>第一部分标题</summary>
  <p>第一部分的内容...</p>
</details>

<details name="accordion-group">
  <summary>第二部分标题</summary>
  <p>第二部分的内容...</p>
</details>

<details name="accordion-group">
  <summary>第三部分标题</summary>
  <p>第三部分的内容...</p>
</details>
关键点说明
特性 说明
name 属性 相同 name 值的 <details> 元素会互斥,实现单展开
open 属性 指定默认展开的面板
浏览器支持 Chrome 120+, Firefox, Safari 17.1+
增强版实现(添加 heading 结构)

⚠️ 注意<details> 元素的实现方式与 W3C Accordion Pattern 的 DOM 结构要求不完全一致。W3C 标准要求按钮元素必须是 heading 元素内部的唯一子元素(<h3><button>...</button></h3>),而 <details> 使用 <summary> 作为交互元素。

如果需要更好的无障碍支持,可以在 <summary> 内添加标题:

<details name="accordion-group" open>
  <summary>
    <h3 style="display: inline; font-size: inherit;">第一部分标题</h3>
  </summary>
  <p>第一部分的内容...</p>
</details>

重要提示:这种结构虽然添加了 heading,但仍然是 heading 在 summary 内部,与 W3C 要求的 button 在 heading 内部 的结构相反。因此,这种方式:

  • ✅ 提供了基本的标题层级信息
  • ❌ 不完全符合 W3C Accordion Pattern 的 DOM 结构规范
  • ❌ 可能不被某些屏幕阅读器正确识别为手风琴组件
适用场景

推荐使用 <details name>

  • 简单的 FAQ 页面
  • 不需要复杂样式的场景
  • 追求原生、轻量实现
  • 现代浏览器环境

推荐使用 W3C 模式:

  • 需要多展开模式
  • 需要箭头键导航
  • 需要精确的标题层级(SEO/屏幕阅读器)
  • 需要复杂的自定义样式

五、常见应用场景

5.1 表单分步填写

将长表单分成多个部分,用户逐步填写:

<div class="accordion">
  <h3>
    <button aria-expanded="true" aria-controls="step-1">
      步骤 1:个人信息
    </button>
  </h3>
  <div id="step-1" role="region">
    <label>姓名 <input type="text" /></label>
    <label>邮箱 <input type="email" /></label>
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="step-2">
      步骤 2:地址信息
    </button>
  </h3>
  <div id="step-2" role="region" hidden>
    <label>城市 <input type="text" /></label>
    <label>邮编 <input type="text" /></label>
  </div>
</div>

5.2 FAQ 页面

常见问题解答页面,每个问题作为一个可展开的部分:

<div class="accordion">
  <h3>
    <button aria-expanded="false" aria-controls="faq-1">
      如何注册账户?
    </button>
  </h3>
  <div id="faq-1" role="region" hidden>
    <p>点击页面右上角的"注册"按钮,填写必要信息...</p>
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="faq-2">
      如何重置密码?
    </button>
  </h3>
  <div id="faq-2" role="region" hidden>
    <p>点击登录页面的"忘记密码"链接...</p>
  </div>
</div>

5.3 设置面板

应用程序的设置页面,将相关设置分组:

<div class="accordion">
  <h3>
    <button aria-expanded="true" aria-controls="settings-general">
      通用设置
    </button>
  </h3>
  <div id="settings-general" role="region">
    <label><input type="checkbox" /> 启用通知</label>
    <label><input type="checkbox" /> 自动保存</label>
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="settings-privacy">
      隐私设置
    </button>
  </h3>
  <div id="settings-privacy" role="region" hidden>
    <label><input type="checkbox" /> 公开个人资料</label>
    <label><input type="checkbox" /> 允许搜索</label>
  </div>
</div>

六、最佳实践

6.1 语义化标记

  • 使用适当的标题层级(h1-h6)包装手风琴标题按钮
  • 为每个面板添加 role="region" 以增强结构感知(面板数量较少时)
  • 确保按钮元素是标题元素内部的唯一元素

6.2 键盘导航

  • 实现基本的 Enter/Space 和 Tab 导航
  • 可选实现箭头键导航以提升用户体验
  • 确保所有手风琴标题都包含在 Tab 序列中

6.3 视觉指示

  • 使用清晰的视觉指示器表示展开/折叠状态
  • 为当前聚焦的标题提供明显的焦点样式
  • 考虑使用动画过渡提升用户体验

6.4 状态管理

  • 明确区分单展开和多展开模式
  • 在单展开模式中,考虑是否允许所有面板同时折叠
  • 使用 aria-disabled 表示不允许折叠的面板

6.5 嵌套考虑

  • 避免过深的嵌套层级
  • 嵌套手风琴时,确保每个层级有清晰的视觉区分
  • 考虑使用不同的标题层级表示嵌套关系

七、Accordion 与 Disclosure 的区别

特性 Accordion Disclosure
内容组织 多个垂直堆叠的面板 单个内容块
展开模式 支持单展开或多展开 独立控制
标题结构 使用 heading + button 结构 简单按钮或 summary
导航支持 支持箭头键导航 基本 Tab 导航
用途 表单分步、设置面板、FAQ 详细信息展示

八、总结

构建无障碍的 Accordion 组件需要关注三个核心:正确的语义化标记(heading + button 结构)、完整的键盘交互支持(包括可选的箭头键导航)、清晰的状态管理(aria-expanded、aria-controls、aria-disabled)。与简单的 Disclosure 不同,Accordion 强调多个面板的组织和管理,适用于更复杂的内容展示场景。

遵循 W3C Accordion Pattern 规范,我们能够创建既美观又包容的手风琴组件,为不同能力的用户提供一致的体验。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

Dnf Command in Linux: Package Management Guide

dnf (Dandified YUM) is the default package manager on Fedora, RHEL, AlmaLinux, Rocky Linux, and other RPM-based distributions. It replaces the older yum package manager and provides faster dependency resolution, better performance, and a cleaner command interface.

Most dnf commands require root privileges, so you need to run them with sudo .

This guide explains the most common dnf commands for day-to-day package management.

Checking for Updates (dnf check-update)

Before installing or upgrading packages, check which packages have updates available:

Terminal
dnf check-update

The command lists all packages with available updates and returns exit code 100 if updates are available, or 0 if the system is up to date. This makes it useful in scripts.

Upgrading Packages (dnf upgrade)

To upgrade all installed packages to their latest available versions, run:

Terminal
sudo dnf upgrade

To refresh the repository metadata and upgrade in one step:

Terminal
sudo dnf upgrade --refresh

To upgrade a single package:

Terminal
sudo dnf upgrade package_name

To apply only security updates:

Terminal
sudo dnf upgrade --security

Installing Packages (dnf install)

To install a package, run:

Terminal
sudo dnf install package_name

To install multiple packages at once, specify them as a space-separated list:

Terminal
sudo dnf install package1 package2

To install a local RPM file, provide the full path:

Terminal
sudo dnf install /full/path/package.rpm

dnf automatically resolves and installs all required dependencies.

To reinstall a package (for example, to restore corrupted files):

Terminal
sudo dnf reinstall package_name

Removing Packages (dnf remove)

To remove an installed package:

Terminal
sudo dnf remove package_name

You can specify multiple packages separated by spaces:

Terminal
sudo dnf remove package1 package2

The remove command also removes packages that depend on the one being removed.

Removing Unused Dependencies (dnf autoremove)

When a package is removed, its dependencies may no longer be needed by any other package. To remove these orphaned dependencies:

Terminal
sudo dnf autoremove

Searching for Packages (dnf search)

To search for a package by name or description:

Terminal
dnf search package_name

The command searches both package names and summaries. To search only in package names:

Terminal
dnf search --names-only package_name

Package Information (dnf info)

To display detailed information about a package, including its version, repository, size, and description:

Terminal
dnf info package_name

This works for both installed and available packages.

Finding Which Package Provides a File (dnf provides)

To find which package provides a specific file or command:

Terminal
dnf provides /usr/bin/curl

This is useful when a command is missing and you need to know which package to install.

Listing Packages (dnf list)

To list all installed packages:

Terminal
dnf list installed

To list all available packages from enabled repositories:

Terminal
dnf list available

To check whether a specific package is installed:

Terminal
dnf list installed | grep package_name

To list packages that have updates available:

Terminal
dnf list updates

Package Groups (dnf group)

DNF organizes related packages into groups. To list all available groups:

Terminal
dnf group list

To install a group (for example, “Development Tools”):

Terminal
sudo dnf group install "Development Tools"

To remove a group:

Terminal
sudo dnf group remove "Development Tools"

Module Streams (dnf module)

DNF modules allow you to install specific versions (streams) of software. For example, you can choose between Node.js 18 or 20.

To list available modules:

Terminal
dnf module list

To enable a specific module stream:

Terminal
sudo dnf module enable nodejs:20

To install a module with its default profile:

Terminal
sudo dnf module install nodejs:20

To reset a module to its default state:

Terminal
sudo dnf module reset nodejs

Managing Repositories (dnf config-manager)

The config-manager command is provided by the dnf-plugins-core package. Install it first if the subcommand is missing:

Terminal
sudo dnf install dnf-plugins-core

To list all enabled repositories:

Terminal
dnf repolist

To list all repositories, including disabled ones:

Terminal
dnf repolist all

To enable a repository:

Terminal
sudo dnf config-manager --set-enabled repo_id

To disable a repository:

Terminal
sudo dnf config-manager --set-disabled repo_id

To show detailed information about a repository:

Terminal
dnf repoinfo repo_id

Cleaning the Cache (dnf clean)

DNF caches repository metadata and downloaded packages locally. To clear all cached data:

Terminal
sudo dnf clean all

To rebuild the metadata cache:

Terminal
sudo dnf makecache

Cleaning the cache is useful when you encounter stale metadata errors or want to free disk space.

Transaction History (dnf history)

DNF records every transaction (install, upgrade, remove) in a history log. To view the transaction history:

Terminal
dnf history

To see the details of a specific transaction:

Terminal
dnf history info 25

To undo a transaction (revert the changes it made):

Terminal
sudo dnf history undo 25

This is useful when an upgrade causes problems and you need to roll back.

Quick Reference

Task Command
Check for available updates dnf check-update
Upgrade all packages sudo dnf upgrade
Install a package sudo dnf install package_name
Install a local RPM file sudo dnf install /path/file.rpm
Remove a package sudo dnf remove package_name
Remove unused dependencies sudo dnf autoremove
Search for a package dnf search keyword
Show package details dnf info package_name
Find which package provides a file dnf provides /path/to/file
List installed packages dnf list installed
List enabled repositories dnf repolist
Clear cached data sudo dnf clean all
View transaction history dnf history
Undo a transaction sudo dnf history undo ID

For a printable quick reference, see the DNF cheatsheet .

Troubleshooting

“Error: Failed to download metadata for repo”
The repository metadata is stale or the mirror is unreachable. Run sudo dnf clean all followed by sudo dnf makecache to refresh the cache. If the problem persists, check your network connection and the repository URL in /etc/yum.repos.d/.

“No match for argument: package_name”
The package does not exist in any enabled repository. Verify the package name with dnf search and check that the correct repository is enabled with dnf repolist.

Dependency conflict during upgrade
If a dependency conflict prevents an upgrade, review the error message carefully. You can retry with sudo dnf upgrade --allowerasing, but only after confirming which packages will be removed.

GPG key verification failed
The repository GPG key is not imported. DNF prompts you to accept the key during the first install from a new repository. If you need to import it manually, use sudo rpm --import KEY_URL.

Transaction undo fails
Not all transactions can be undone. If packages have been updated by later transactions, the undo may conflict. Check dnf history info ID for details and consider a manual rollback.

FAQ

What is the difference between dnf and yum?
dnf is the successor to yum. It uses the same repository format and configuration files, but provides faster dependency resolution, better memory usage, and a more consistent command interface. On modern Fedora and RHEL systems, yum is a symlink to dnf.

Is dnf update the same as dnf upgrade?
Yes. dnf update is an alias for dnf upgrade. Both commands upgrade all installed packages to the latest available versions.

How do I install a specific version of a package?
Specify the version with a dash: sudo dnf install package_name-1.2.3. To list all available versions, use dnf --showduplicates list package_name.

How do I prevent a package from being upgraded?
Use the versionlock plugin: sudo dnf install dnf-plugin-versionlock, then sudo dnf versionlock add package_name. To remove the lock later, use sudo dnf versionlock delete package_name.

What is the equivalent of apt autoremove in dnf?
The equivalent is sudo dnf autoremove. It removes packages that were installed as dependencies but are no longer required by any installed package.

Conclusion

dnf is the standard package manager for Fedora, RHEL, and RPM-based distributions. It handles installing, upgrading, removing, and searching packages, as well as managing repositories and module streams. To learn more, run man dnf in your terminal.

If you have any questions, feel free to leave a comment below.

IP Command Cheatsheet

Basic Syntax

Use this structure for most ip operations.

Command Description
ip [OPTIONS] OBJECT COMMAND General ip command syntax
ip -br a Show addresses in brief format
ip -c a Show colorized output
ip -4 a Show only IPv4 addresses
ip -6 a Show only IPv6 addresses

Show Interfaces and Addresses

Inspect links and assigned IP addresses.

Command Description
ip link show Show all network interfaces
ip link show dev eth0 Show one interface
ip addr show Show all IP addresses
ip addr show dev eth0 Show addresses on one interface
ip -br addr Brief interface and address overview

Add and Remove IP Addresses

Assign or remove IP addresses on interfaces.

Command Description
sudo ip addr add 192.168.1.50/24 dev eth0 Add IPv4 address
sudo ip addr del 192.168.1.50/24 dev eth0 Remove IPv4 address
sudo ip addr add 2001:db8::50/64 dev eth0 Add IPv6 address
sudo ip addr flush dev eth0 Remove all addresses from interface
ip addr show dev eth0 Verify interface addresses

Bring Interfaces Up or Down

Enable, disable, or rename network links.

Command Description
sudo ip link set dev eth0 up Bring interface up
sudo ip link set dev eth0 down Bring interface down
sudo ip link set dev eth0 mtu 9000 Change MTU
sudo ip link set dev eth0 name lan0 Rename interface
ip -br link Show link state quickly

Routing Table

Inspect and manage network routes.

Command Description
ip route show Show IPv4 routing table
ip -6 route show Show IPv6 routing table
ip route get 8.8.8.8 Show route used for destination
sudo ip route add default via 192.168.1.1 Add default gateway
sudo ip route del default Remove default gateway
sudo ip route add 10.10.0.0/16 via 192.168.1.254 dev eth0 Add static route

Neighbor (ARP/NDP) Table

View and manage neighbor cache entries.

Command Description
ip neigh show Show neighbor table
ip neigh show dev eth0 Show neighbors for one interface
sudo ip neigh flush dev eth0 Clear neighbor entries on interface
sudo ip neigh del 192.168.1.10 dev eth0 Remove a neighbor entry
ip -s neigh Show neighbor statistics

Policy Routing

Work with multiple routing tables and rules.

Command Description
ip rule show List policy routing rules
sudo ip rule add from 192.168.10.0/24 table 100 Route source subnet using table 100
sudo ip route add default via 10.0.0.1 table 100 Add default route to custom table
sudo ip rule del from 192.168.10.0/24 table 100 Remove policy rule
ip route show table 100 Show routes in table 100

Network Namespaces

Inspect or run commands inside network namespaces.

Command Description
ip netns list List network namespaces
sudo ip netns add ns1 Create namespace
sudo ip netns exec ns1 ip a Run ip a inside namespace
sudo ip netns del ns1 Delete namespace
ip -n ns1 route Show routes in namespace

Troubleshooting

Fast checks for common network issues.

Issue Check
No IP assigned to interface ip addr show dev eth0
Interface is down ip link show dev eth0 then sudo ip link set dev eth0 up
Wrong default route ip route show and verify default via ...
Cannot reach destination ip route get DESTINATION_IP
Stale ARP/neighbor entry sudo ip neigh flush dev eth0

Related Guides

Use these articles for detailed networking workflows.

Guide Description
Linux ip Command with Examples Complete ip command guide
How to Find Your IP Address in Linux Public and private IP lookup methods
Traceroute Command in Linux Path and hop diagnostics
UFW Cheatsheet Firewall rules quick reference

Understanding the /etc/fstab File in Linux

The /etc/fstab file (filesystem table) is a system configuration file that defines how filesystems, partitions, and storage devices are mounted at boot time. The system reads this file during startup and mounts each entry automatically.

Understanding /etc/fstab is essential when you need to add a new disk, create a swap file , mount a network share , or change mount options for an existing filesystem.

This guide explains the /etc/fstab file format, what each field means, common mount options, and how to add new entries safely.

/etc/fstab Format

The /etc/fstab file is a plain text file with one entry per line. Each line defines a filesystem to mount. Lines beginning with # are comments and are ignored by the system.

To view the contents of the file safely, use less :

Terminal
less /etc/fstab

A typical /etc/fstab file looks like this:

output
# <file system> <mount point> <type> <options> <dump> <pass>
UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 / ext4 errors=remount-ro 0 1
UUID=b2c3d4e5-f6a7-8901-bcde-f12345678901 /home ext4 defaults 0 2
UUID=c3d4e5f6-a7b8-9012-cdef-123456789012 none swap sw 0 0
tmpfs /tmp tmpfs defaults,noatime 0 0

Each entry contains six space-separated fields:

txt
UUID=a1b2c3d4... /home ext4 defaults 0 2
[---------------] [---] [--] [------] - -
| | | | | |
| | | | | +-> 6. Pass (fsck order)
| | | | +----> 5. Dump (backup flag)
| | | +-----------> 4. Options
| | +------------------> 3. Type
| +-------------------------> 2. Mount point
+---------------------------------------------> 1. File system

Field Descriptions

  1. File system — The device or partition to mount. This can be specified as:

    • A UUID: UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    • A disk label: LABEL=home
    • A device path: /dev/sda1
    • A network path: 192.168.1.10:/export/share (for NFS)

    Using UUIDs is recommended because device paths like /dev/sda1 can change if disks are added or removed. To find the UUID of a partition, run blkid:

    Terminal
    sudo blkid
  2. Mount point — The directory where the filesystem is attached. The directory must already exist. Common mount points include /, /home, /boot, and /mnt/data. For swap entries, this field is set to none.

  3. Type — The filesystem type. Common values include:

    • ext4 — The default Linux filesystem
    • xfs — High-performance filesystem used on RHEL-based distributions
    • btrfs — Copy-on-write filesystem with snapshot support
    • swap — Swap partition or file
    • tmpfs — Temporary filesystem stored in memory
    • nfs — Network File System
    • vfat — FAT32 filesystem (USB drives, EFI partitions)
    • auto — Let the kernel detect the filesystem type automatically
  4. Options — A comma-separated list of mount options. See the Common Mount Options section below for details.

  5. Dump — Used by the dump backup utility. A value of 0 means the filesystem is not included in backups. A value of 1 means it is. Most modern systems do not use dump, so this is typically set to 0.

  6. Pass — The order in which fsck checks filesystems at boot. The root filesystem should be 1. Other filesystems should be 2 so they are checked after root. A value of 0 means the filesystem is not checked.

Common Mount Options

The fourth field in each fstab entry is a comma-separated list of mount options. The following options are the most commonly used:

  • defaults — Uses the standard default options (rw, suid, dev, exec, auto, nouser, async). Some effective behaviors can still vary by filesystem and kernel settings.
  • ro — Mount the filesystem as read-only.
  • rw — Mount the filesystem as read-write.
  • noatime — Do not update file access times. This can improve performance, especially on SSDs.
  • nodiratime — Do not update directory access times.
  • noexec — Do not allow execution of binaries on the filesystem.
  • nosuid — Do not allow set-user-ID or set-group-ID bits to take effect.
  • nodev — Do not interpret character or block special devices on the filesystem.
  • nofail — Do not report errors if the device does not exist at boot. Useful for removable drives and network shares.
  • auto — Mount the filesystem automatically at boot (default behavior).
  • noauto — Do not mount automatically at boot. The filesystem can still be mounted manually with mount.
  • user — Allow a regular user to mount the filesystem.
  • errors=remount-ro — Remount the filesystem as read-only if an error occurs. Common on root filesystem entries.
  • _netdev — The filesystem requires network access. The system waits for the network to be available before mounting. Use this for NFS, CIFS, and iSCSI mounts.
  • x-systemd.automount — Mount the filesystem on first access instead of at boot. Managed by systemd.

You can combine multiple options separated by commas:

txt
UUID=a1b2c3d4... /data ext4 defaults,noatime,nofail 0 2

Adding an Entry to /etc/fstab

Before editing /etc/fstab, always create a backup:

Terminal
sudo cp /etc/fstab /etc/fstab.bak

Step 1: Find the UUID

Identify the UUID of the partition you want to mount:

Terminal
sudo blkid /dev/sdb1
output
/dev/sdb1: UUID="d4e5f6a7-b8c9-0123-def0-123456789abc" TYPE="ext4"

Step 2: Create the Mount Point

Create the directory where the filesystem will be mounted:

Terminal
sudo mkdir -p /mnt/data

Step 3: Add the Entry

Open /etc/fstab in a text editor :

Terminal
sudo nano /etc/fstab

Add a new line at the end of the file:

/etc/fstabsh
UUID=d4e5f6a7-b8c9-0123-def0-123456789abc /mnt/data ext4 defaults,nofail 0 2

Step 4: Test the Entry

Instead of rebooting, use mount -a to mount all entries in /etc/fstab that are not already mounted:

Terminal
sudo mount -a

If the command produces no output, the entry is correct. If there is an error, fix the fstab entry before rebooting — an incorrect fstab can prevent the system from booting normally.

Verify the filesystem is mounted :

Terminal
df -h /mnt/data

Common fstab Examples

Swap File

To add a swap file to fstab:

/etc/fstabsh
/swapfile none swap sw 0 0

NFS Network Share

To mount an NFS share that requires network access:

/etc/fstabsh
192.168.1.10:/export/share /mnt/nfs nfs defaults,_netdev,nofail 0 0

CIFS/SMB Windows Share

To mount a Windows/Samba share with a credentials file:

/etc/fstabsh
//192.168.1.20/share /mnt/smb cifs credentials=/etc/samba/creds,_netdev,nofail 0 0

Set strict permissions on the credentials file so other users cannot read it:

Terminal
sudo chmod 600 /etc/samba/creds

USB or External Drive

To mount a removable drive that may not always be attached:

/etc/fstabsh
UUID=e5f6a7b8-c9d0-1234-ef01-23456789abcd /mnt/usb ext4 defaults,nofail,noauto 0 0

The nofail option prevents boot errors when the drive is not connected. The noauto option prevents automatic mounting — mount it manually with sudo mount /mnt/usb when needed.

tmpfs for /tmp

To mount /tmp as a temporary filesystem in memory:

/etc/fstabsh
tmpfs /tmp tmpfs defaults,noatime,size=2G 0 0

Quick Reference

Task Command
View fstab contents less /etc/fstab
Back up fstab sudo cp /etc/fstab /etc/fstab.bak
Find partition UUIDs sudo blkid
Mount all fstab entries sudo mount -a
Check mounted filesystems mount or df -h
Check filesystem type lsblk -f
Restore fstab from backup sudo cp /etc/fstab.bak /etc/fstab

Troubleshooting

System does not boot after editing fstab
An incorrect fstab entry can cause a boot failure. Boot into recovery mode or a live USB, mount the root filesystem, and fix or restore /etc/fstab from the backup. Always test with sudo mount -a before rebooting.

mount -a reports “wrong fs type” or “bad superblock”
The filesystem type in the fstab entry does not match the actual filesystem on the device. Use sudo blkid or lsblk -f to check the correct type.

Network share fails to mount at boot
Add the _netdev option to tell the system to wait for network availability before mounting. For systemd-based systems, x-systemd.automount can also help with timing issues.

“mount point does not exist”
The directory specified in the second field does not exist. Create it with mkdir -p /path/to/mountpoint before running mount -a.

UUID changed after reformatting a partition
Reformatting a partition assigns a new UUID. Run sudo blkid to find the new UUID and update the fstab entry accordingly.

FAQ

What happens if I make an error in /etc/fstab?
If the entry references a non-existent device without the nofail option, the system may drop to an emergency shell during boot. Always use nofail for non-essential filesystems and test with sudo mount -a before rebooting.

Should I use UUID or device path (/dev/sda1)?
Use UUID. Device paths can change if you add or remove disks, or if the boot order changes. UUIDs are unique to each filesystem and do not change unless you reformat the partition.

What does the nofail option do?
It tells the system to continue booting even if the device is not present or cannot be mounted. Without nofail, a missing device causes the system to drop to an emergency shell.

How do I remove an fstab entry?
Open /etc/fstab with sudo nano /etc/fstab, delete or comment out the line (add # at the beginning), save the file, and then unmount the filesystem with sudo umount /mount/point.

What is the difference between noauto and nofail?
noauto prevents the filesystem from being mounted automatically at boot — you must mount it manually. nofail still mounts automatically but does not cause a boot error if the device is missing.

Conclusion

The /etc/fstab file controls how filesystems are mounted at boot. Each entry specifies the device, mount point, filesystem type, options, and check order. Always back up fstab before editing, use UUIDs instead of device paths, and test changes with sudo mount -a before rebooting.

If you have any questions, feel free to leave a comment below.

DNF Cheatsheet

Basic Commands

Start with package lists and metadata.

Command Description
dnf --version Show DNF version
dnf check-update List available updates
dnf makecache Refresh repository metadata cache
dnf repolist List enabled repositories
dnf repolist all List all repositories

Search and Info

Find packages and inspect details.

Command Description
dnf search nginx Search packages by keyword
dnf info nginx Show package details
dnf provides /usr/bin/python3 Find package that provides a file
dnf list installed List installed packages
dnf list available List available packages from repos

Install and Remove

Install, remove, and reinstall packages.

Command Description
sudo dnf install nginx Install one package
sudo dnf install nginx php-fpm Install multiple packages
sudo dnf remove nginx Remove package
sudo dnf autoremove Remove unneeded dependencies
sudo dnf reinstall nginx Reinstall package

Update and Upgrade

Keep the system and packages up to date.

Command Description
sudo dnf update Update installed packages
sudo dnf upgrade Upgrade packages (same effect in most setups)
sudo dnf upgrade --refresh Refresh metadata and upgrade
sudo dnf update --security Apply security updates only
sudo dnf offline-upgrade download Prepare offline upgrade (where supported)

Groups and Modules

Work with package groups and modular streams.

Command Description
dnf group list List package groups
sudo dnf group install "Development Tools" Install package group
sudo dnf group remove "Development Tools" Remove package group
dnf module list List module streams
sudo dnf module enable nodejs:20 Enable a module stream
sudo dnf module reset nodejs Reset module stream

Repository Management

Enable, disable, and inspect repositories.

Command Description
sudo dnf config-manager --set-enabled repo_id Enable repository
sudo dnf config-manager --set-disabled repo_id Disable repository
dnf repoinfo Show repo details
dnf repoinfo repo_id Show one repository details
sudo dnf clean all Clear all cache data

Query and History

Review installed files and transaction history.

Command Description
rpm -ql nginx List files installed by package
rpm -qf /usr/sbin/nginx Find package owning a file
dnf history Show transaction history
dnf history info 25 Show details of transaction ID 25
sudo dnf history undo 25 Undo transaction ID 25

Troubleshooting

Common checks when package operations fail.

Issue Check
Metadata errors or stale cache Run sudo dnf clean all then sudo dnf makecache
Package not found Verify enabled repos with dnf repolist and use dnf search
Dependency conflicts Retry with --allowerasing only after reviewing affected packages
GPG key error Import/verify repository GPG key and retry
Slow mirror response Refresh metadata and test another mirror/repo configuration

Related Guides

Use these references for broader package management workflows.

Guide Description
How to Use apt Command Package management on Ubuntu, Debian, and derivatives
Linux Commands Cheatsheet General Linux command quick reference

DC-SDK 实战指南:基于 Cesium 的三维数字孪生大屏开发 前言 在当今数字孪生、智慧城市等领域的开发中,三维地图可视化已经成为核心需求。

DC-SDK 实战指南:基于 Cesium 的三维数字孪生大屏开发

前言

在当今数字孪生、智慧城市等领域的开发中,三维地图可视化已经成为核心需求。本文将结合一个实际的政务大屏项目,分享如何使用 DC-SDK(@dvgis/dc-sdk)快速构建高性能的三维可视化应用。

本文基于实际生产项目 databoard-ui,涉及人口、房屋、场所、事件等多维度数据的三维可视化展示。


一、DC-SDK 是什么

DC-SDK 是一款基于 Cesium 的 WebGL 三维地图可视化开发框架,它在 Cesium 原生 API 的基础上进行了封装和增强,提供了:

  • 更简洁的 API 设计
  • 丰富的图层类型(矢量图层、聚合图层、GeoJSON图层、3DTiles图层等)
  • 完善的事件交互体系
  • Vue/React 友好的集成方式

安装方式

npm install @dvgis/dc-sdk
// main.js 中引入
import * as DC from '@dvgis/dc-sdk'
import '@dvgis/dc-sdk/dist/dc.min.css'

// 挂载到全局,方便使用
window.DC = DC

二、初始化三维地球

基础配置

在 Vue 项目中,我们通常将地图实例管理放在 Vuex 中:

// store/modules/map.js
initMapInstance({ commit, state }) {
  return new Promise((resolve, reject) => {
    DC.ready().then(async () => {
      const viewer = new DC.Viewer('viewer-container', {
        sceneMode: DC.SceneMode.SCENE3D,      // 3D模式
        baseLayer: true,                       // 启用基础图层
        enableCursorStyle: false,              // 自定义鼠标样式
        scene3DOnly: true,                     // 仅3D场景
        requestRenderMode: true,               // 请求渲染模式(性能优化)
      })
      
      // 配置地球外观和相机控制
      viewer.setOptions({
        showAtmosphere: false,                 // 隐藏大气层
        showSun: false,                        // 隐藏太阳
        showMoon: false,                       // 隐藏月亮
        cameraController: {
          minimumZoomDistance: 280,            // 最小缩放距离
          maximumZoomDistance: 500000,         // 最大缩放距离
        },
        globe: {
          show: true,
          showGroundAtmosphere: false,         // 隐藏地面大气层
          enableLighting: false,               // 禁用光照
          baseColor: DC.Color.fromCssColorString("rgba(0, 23, 75, 1)"),
        },
        skyBox: { show: false }                // 隐藏天空盒
      })
      
      commit('SET_MAP_INSTANCE', viewer)
      resolve(viewer)
    })
  })
}

添加影像底图

// 创建 XYZ 瓦片底图
const baseLayer = DC.ImageryLayerFactory.createXYZImageryLayer({
  url: process.env.VUE_APP_IMAGERYLAYER_URL_XYZ,
  style: "elec",
  tilingScheme: new DC.WebMercatorTilingScheme(),
  crs: "WGS84",
  rectangle: {
    west: -180 * Math.PI / 180,
    south: -85.05 * Math.PI / 180,
    east: 180 * Math.PI / 180,
    north: 85.05 * Math.PI / 180
  }
})

await viewer.addBaseLayer([baseLayer], { brightness: 0.1 })

三、核心图层实战

1. 聚合图层 (ClusterLayer) - 海量点位展示

聚合图层是处理海量点位数据的利器,自动根据缩放级别聚合显示:

// 创建聚合图层
this.layers.clusterLayer = new DC.ClusterLayer('clusterLayer', {
  radius: 40,                                    // 聚合像素范围
  maxZoom: 25,                                   // 最大聚合缩放级别
  image: require('@/assets/img/icon-cs.png'),   // 单点图标
  gradientColors: {                              // 聚合颜色渐变
    "0.0001": DC.Color.BLUEVIOLET,
    "0.001": DC.Color.BLUEVIOLET,
    "0.01": DC.Color.BLUEVIOLET,
    "0.1": DC.Color.BLUEVIOLET
  },
  fontColor: DC.Color.WHITE,                     // 数字颜色
  style: 'circle',                               // 样式:circle/clustering/custom
  clusterSize: 16,                              // 聚合图标尺寸
})

// 设置点位数据
const positions = dataList.map(item => ({
  attr: item,           // 自定义属性,点击时可获取
  lng: item.lng,
  lat: item.lat
}))

this.layers.clusterLayer.setPoints(positions)
this.viewer.addLayer(this.layers.clusterLayer)

聚合点点击事件处理

this.layers.clusterLayer.on(DC.MouseEventType.CLICK, (movement) => {
  const { attr } = movement.overlay
  
  // 判断是聚合点还是单个点
  if (Object.keys(attr).includes('count')) {
    // 聚合点:获取聚合内的点列表
    const leaves = this.layers.clusterLayer.getLeaves(attr.properties.cluster_id)
    
    // 或者放大到展开级别
    const expansionZoom = this.layers.clusterLayer.getClusterExpansionZoom(attr.properties.cluster_id)
    this.viewer.camera.flyTo({ destination: ... })
  } else {
    // 单个点:显示详情信息
    this.showDetailPopup(attr)
  }
})

2. GeoJSON 图层 - 区域边界可视化

加载 GeoJSON 数据展示行政区划:

async renderGeoJson(geoJson) {
  // 创建 GeoJsonLayer
  this.layers.geoJsonLayer = new DC.GeoJsonLayer('geoJsonLayer', geoJson, {
    clampToGround: true,        // 贴地
    pickable: true,             // 可点击(重要!)
    fill: DC.Color.fromCssColorString("rgba(0, 0, 0, 0)"),
    stroke: DC.Color.fromCssColorString("rgba(0, 171, 255, 1)"),
    strokeWidth: 10,
  })
  
  this.viewer.addLayer(this.layers.geoJsonLayer)
  
  // 遍历每个多边形,添加拉伸效果
  const delegate = await this.layers.geoJsonLayer.delegate
  this.layers.geoJsonLayer.eachOverlay((item) => {
    if (item.polygon) {
      let polygon = DC.Polygon.fromEntity(item)
      polygon.setStyle({
        height: 198,
        material: DC.Color.fromCssColorString("rgba(0, 171, 255, 0.2)"),
        extrudedHeight: 200,    // 拉伸高度,形成3D效果
      })
      this.vectorLayer.addOverlay(polygon)
    }
  })
}

3. 3DTiles 图层 - 城市建筑白模

加载城市级 3DTiles 数据,实现建筑白模渲染:

loadWhiteTileset() {
  const tileset = new DC.Tileset(tilesetUrl, {
    // ⭐ 核心性能优化参数
    maximumScreenSpaceError: 16,           // 降低值更流畅
    maximumNumberOfLoadedTiles: 1000,      // 限制同时加载瓦片数
    skipLevelOfDetail: true,               // 跳过中间LOD
    maximumMemoryUsage: 512,               // 内存限制(MB)
    
    // 视锥体优化
    cullRequestsWhileMoving: true,         // 移动时暂停新请求
    cullRequestsWhileMovingMultiplier: 60,
    
    // 动态调整精度
    dynamicScreenSpaceError: true,
    dynamicScreenSpaceErrorDensity: 0.00278,
    dynamicScreenSpaceErrorFactor: 4.0,
  })
  
  // 自定义着色器,统一建筑颜色
  tileset.ready((tileset) => {
    tileset.customShader = new Cesium.CustomShader({
      fragmentShaderText: `
        void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
          material.diffuse = vec3(0.0/255.0, 191.0/255.0, 255.0/255.0);
        }
      `
    })
  })
  
  this.tilesetLayer.addOverlay(tileset)
  this.viewer.addLayer(this.tilesetLayer)
}

4. HTML 图层 - 自定义弹窗

使用 HtmlLayer 实现 Vue 组件弹窗:

// 创建 HTML 图层
this.layers.htmlLayer = new DC.HtmlLayer('htmlLayer')
this.viewer.addLayer(this.layers.htmlLayer)

// 在指定位置创建 HTML 图标
createHtmlIcon(options) {
  const { position, htmlLayer, props, component } = options
  const id_html = "id_" + Date.now()
  
  // 创建 DivIcon
  let htmlIcon = new DC.DivIcon(
    new DC.Position(position.lng, position.lat),
    `<div id="${id_html}"></div>`
  )
  htmlIcon.setStyle({
    className: 'custom-popup',
    zIndex: '2200'
  })
  
  htmlLayer.addOverlay(htmlIcon)
  
  // 将 Vue 组件挂载到 DOM
  const vm = mountRemote(id_html, props, component)
  return vm
}

// 使用示例
const vm = this.createHtmlIcon({
  position: { lng: 118.18, lat: 28.42 },
  htmlLayer: this.layers.htmlLayer,
  props: { title: '详情信息' },
  component: PlaceDetails,  // Vue 组件
})

vm.$on('close', () => {
  this.layers.htmlLayer.clear()
})

四、相机控制与视角管理

常用相机操作

// 1. 设置初始视角
viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(118.18, 28.42, 1500),
  orientation: {
    heading: Cesium.Math.toRadians(0),     // 正北
    pitch: Cesium.Math.toRadians(-45),     // 俯角45度
    roll: 0
  }
})

// 2. 飞行到指定位置(带动画)
viewer.camera.flyTo({
  destination: Cesium.Cartesian3.fromDegrees(lng, lat, height),
  orientation: { heading: 0, pitch: -45, roll: 0 },
  duration: 2,                              // 飞行时间
  complete: () => console.log('到达目标')
})

// 3. 飞行到边界范围(适配数据)
viewer.flyToBounds([minLon, minLat, maxLon, maxLat], {
  heading: 0,
  pitch: -90,
  roll: 0
})

// 4. 计算边界球飞行(适配大量点位)
const positions = points.map(p => 
  Cesium.Cartesian3.fromDegrees(p.lng, p.lat)
)
const boundingSphere = Cesium.BoundingSphere.fromPoints(positions)
viewer.camera.flyToBoundingSphere(boundingSphere, {
  duration: 2,
  padding: new Cesium.HeadingPitchRange(0, -0.5, boundingSphere.radius * 1.5)
})

监听相机高度变化

viewer.camera.changed.addEventListener(() => {
  const zoom = viewer.zoom
  const currentHeight = viewer.camera.positionCartographic.height
  console.log('当前相机高度: ' + currentHeight.toFixed(2) + ' 米')
})

五、性能优化技巧

1. 渲染模式优化

const viewer = new DC.Viewer('viewer-container', {
  requestRenderMode: true,        // 请求渲染模式,只在需要时渲染
  maximumRenderTimeChange: 0.5,   // 最大渲染间隔
})

2. 3DTiles 性能调优

const tileset = new DC.Tileset(url, {
  // 随相机移动动态调整精度
  maximumScreenSpaceError: 16,
  
  // 移动时降低精度
  cullRequestsWhileMoving: true,
  cullRequestsWhileMovingMultiplier: 60.0,
  
  // 相机停止后恢复精度
})

viewer.camera.moveStart.addEventListener(() => {
  tileset.maximumScreenSpaceError = 32
})
viewer.camera.moveEnd.addEventListener(() => {
  setTimeout(() => {
    tileset.maximumScreenSpaceError = 8
  }, 500)
})

3. 图层生命周期管理

// 页面离开时清理图层
beforeDestroy() {
  for (let key in this.layers) {
    this.viewer.removeLayer(this.layers[key])
  }
}

六、完整业务封装示例

将地图操作封装为业务类,便于各页面复用:

// layout/dcMap/common.js
export default class Common {
  layers = {}
  viewer = null
  vueInstance = null
  mapUtils = null

  constructor(viewer, vueInstance) {
    this.viewer = viewer
    this.vueInstance = vueInstance
    this.mapUtils = new MapUtils(this.viewer)
  }

  initCommon() {
    // 原型方法扩展
    for (const key in viewerRewrite) {
      DC.Viewer.prototype[key] = viewerRewrite[key]
    }
  }

  // 工具方法
  zoomIn() {
    this.mapUtils._zoomIn()
  }

  zoomOut() {
    this.mapUtils._zoomOut()
  }

  homeLayer() {
    const homePosition = store.state.map.currentView.homePosition
    this.viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(
        homePosition.longitude, 
        homePosition.latitude, 
        homePosition.height
      ),
      orientation: { ... }
    })
  }
}

业务类继承

// layout/dcMap/dashboard.js
import Common from './common.js'

export default class Dashboard extends Common {
  constructor(viewer, vueInstance) {
    super(viewer, vueInstance)
    this.initMap()
  }
  
  initMap() {
    this.initCommon()
    // 初始化业务图层
    this.layers.vectorLayer = new DC.VectorLayer('vectorLayer')
    this.viewer.addLayer(this.layers.vectorLayer)
  }
  
  // 业务方法...
  renderPopulation(data) { }
  renderHouse(data) { }
}

七、项目结构参考

src/
├── layout/
│   ├── DcMap.vue           # 地图容器组件
│   └── dcMap/
│       ├── common.js       # 基础地图操作类
│       ├── dashboard.js    # 首页业务操作类
│       ├── place.js        # 场所业务操作类
│       └── event.js        # 事件业务操作类
├── store/
│   └── modules/
│       └── map.js          # 地图状态管理
├── utils/
│   └── mapUtils.js         # 地图工具函数
└── views/
    └── Dashboard.vue       # 首页大屏

八、常见问题与解决方案

Q1: 弹窗被地球遮挡?

// 设置 disableDepthTestDistance 禁用深度测试
billboard.disableDepthTestDistance = Number.POSITIVE_INFINITY

Q2: 图层点击事件不响应?

// 创建图层时必须开启 pickable
const layer = new DC.GeoJsonLayer(id, geoJson, {
  pickable: true  // 必须开启!
})

Q3: 3DTiles 加载太慢?

// 调整 maximumScreenSpaceError
const tileset = new DC.Tileset(url, {
  maximumScreenSpaceError: 32,  // 值越大越模糊但越快
  skipLevelOfDetail: true
})

Q4: 内存持续增长?

// 及时清理图层和实体
this.viewer.entities.removeAll()
this.viewer.dataSources.removeAll()
this.viewer.scene.primitives.removeAll()

总结

DC-SDK 为 Cesium 开发提供了更友好的开发体验,通过本文介绍的:

  1. 基础初始化 - 地球创建与底图配置
  2. 核心图层 - ClusterLayer、GeoJsonLayer、Tileset、HtmlLayer
  3. 相机控制 - 视角管理与飞行控制
  4. 性能优化 - 渲染模式与资源管理
  5. 工程封装 - 面向业务的类封装

希望这篇实战指南能帮助你快速上手 DC-SDK,构建出高性能的三维数字孪生应用。


参考资源

JavaScript 异步编程完全指南:从入门到精通

JavaScript 异步编程完全指南:从入门到精通


目录

第一部分:基础概念
  ├── 1. 为什么需要异步
  ├── 2. 事件循环机制
  └── 3. 任务队列

第二部分:回调函数
  ├── 4. 回调基础
  └── 5. 回调地狱

第三部分:Promise
  ├── 6. Promise 基础
  ├── 7. Promise 链式调用
  ├── 8. Promise 错误处理
  ├── 9. Promise 静态方法
  └── 10. 手写 Promise

第四部分:Async/Await
  ├── 11. 基本语法
  ├── 12. 错误处理
  └── 13. 常见模式

第五部分:高级异步模式
  ├── 14. Generator 与异步迭代
  ├── 15. 并发控制
  ├── 16. 发布/订阅与事件驱动
  └── 17. RxJS 响应式编程简介

第六部分:实战与最佳实践
  ├── 18. 真实项目场景
  ├── 19. 性能优化
  └── 20. 常见陷阱与调试

第一部分:基础概念

1. 为什么需要异步

1.1 JavaScript 是单线程语言

// JavaScript 只有一个主线程执行代码
// 如果所有操作都是同步的,耗时操作会阻塞后续代码

console.log("开始");

// 假设这是一个同步的网络请求(伪代码),需要3秒
// const data = syncFetch("https://api.example.com/data"); // 阻塞3秒!

console.log("结束"); // 必须等上面完成才能执行

1.2 同步 vs 异步的直观对比

// ============ 同步模型(阻塞)============
// 想象你在餐厅:点菜 → 等厨师做完 → 再点下一道
function syncExample() {
    const start = Date.now();
    
    // 模拟同步阻塞(千万别在实际项目中这样做!)
    function sleep(ms) {
        const end = Date.now() + ms;
        while (Date.now() < end) {} // 忙等待,阻塞线程
    }
    
    console.log("任务1: 开始");
    sleep(2000);  // 阻塞2秒
    console.log("任务1: 完成");
    
    console.log("任务2: 开始");
    sleep(1000);  // 阻塞1秒
    console.log("任务2: 完成");
    
    console.log(`总耗时: ${Date.now() - start}ms`); // ≈ 3000ms
}

// ============ 异步模型(非阻塞)============
// 想象你在餐厅:点完所有菜 → 哪道先做好就先上
function asyncExample() {
    const start = Date.now();
    
    console.log("任务1: 开始");
    setTimeout(() => {
        console.log(`任务1: 完成 (${Date.now() - start}ms)`);
    }, 2000);
    
    console.log("任务2: 开始");
    setTimeout(() => {
        console.log(`任务2: 完成 (${Date.now() - start}ms)`);
    }, 1000);
    
    console.log("两个任务都已发起");
    // 输出顺序:
    // 任务1: 开始
    // 任务2: 开始
    // 两个任务都已发起
    // 任务2: 完成 (≈1000ms)  ← 先完成的先执行
    // 任务1: 完成 (≈2000ms)
    // 总耗时 ≈ 2000ms(而非3000ms)
}

1.3 常见的异步操作

// 1. 定时器
setTimeout(() => console.log("延迟执行"), 1000);
setInterval(() => console.log("重复执行"), 1000);

// 2. 网络请求
fetch("https://api.github.com/users/octocat")
    .then(res => res.json())
    .then(data => console.log(data));

// 3. DOM 事件
document.addEventListener("click", (e) => {
    console.log("用户点击了", e.target);
});

// 4. 文件读写(Node.js)
const fs = require("fs");
fs.readFile("./data.txt", "utf8", (err, data) => {
    console.log(data);
});

// 5. 数据库操作
// db.query("SELECT * FROM users", (err, rows) => { ... });

// 6. Web Workers(浏览器多线程)
// const worker = new Worker("worker.js");
// worker.onmessage = (e) => console.log(e.data);

2. 事件循环机制(Event Loop)

这是理解 JS 异步的核心,必须彻底掌握!

2.1 执行模型全景图

┌─────────────────────────────────────────────────────┐
│                    调用栈 (Call Stack)                 │
│  ┌─────────────────────────────────────────────┐    │
│  │  当前正在执行的函数                            │    │
│  └─────────────────────────────────────────────┘    │
└───────────────────────┬─────────────────────────────┘
                        │
                        ▼ 当调用栈为空时
┌─────────────────────────────────────────────────────┐
│                  事件循环 (Event Loop)                 │
│     不断检查:调用栈空了吗?队列里有任务吗?            │
└───────┬──────────────────────────────┬──────────────┘
        │                              │
        ▼ 优先                         ▼ 其次
┌───────────────────┐    ┌─────────────────────────┐
│  微任务队列         │    │  宏任务队列                │
│  (Microtask Queue) │    │  (Macrotask Queue)       │
│                     │    │                           │
│  • Promise.then     │    │  • setTimeout/setInterval │
│  • MutationObserver │    │  • I/O 回调               │
│  • queueMicrotask   │    │  • UI 渲染                │
│  • process.nextTick │    │  • setImmediate (Node)    │
│    (Node.js)        │    │  • requestAnimationFrame  │
└───────────────────┘    └─────────────────────────┘

2.2 事件循环执行顺序

console.log("1. 同步代码 - script start");

setTimeout(() => {
    console.log("6. 宏任务 - setTimeout");
}, 0);

Promise.resolve()
    .then(() => {
        console.log("3. 微任务 - Promise 1");
    })
    .then(() => {
        console.log("5. 微任务 - Promise 2");
    });

queueMicrotask(() => {
    console.log("4. 微任务 - queueMicrotask");
});

console.log("2. 同步代码 - script end");

// 输出顺序(带编号):
// 1. 同步代码 - script start
// 2. 同步代码 - script end
// 3. 微任务 - Promise 1
// 4. 微任务 - queueMicrotask
// 5. 微任务 - Promise 2
// 6. 宏任务 - setTimeout

2.3 事件循环的详细步骤

/*
 * 事件循环算法:
 * 
 * 1. 执行全局同步代码(这本身就是一个宏任务)
 * 2. 调用栈清空后,检查微任务队列
 * 3. 依次执行所有微任务(包括执行过程中新产生的微任务)
 * 4. 微任务队列清空后,进行一次 UI 渲染(如果需要)
 * 5. 取出一个宏任务执行
 * 6. 回到步骤 2
 * 
 * 关键:每执行完一个宏任务,就要清空所有微任务
 */

// 经典面试题:详细分析执行顺序
console.log("script start");                          // 同步 → 立即执行

async function async1() {
    console.log("async1 start");                      // 同步 → 立即执行
    await async2();                                    
    // await 之后的代码相当于 promise.then 的回调
    console.log("async1 end");                        // 微任务
}

async function async2() {
    console.log("async2");                            // 同步 → 立即执行
}

setTimeout(function() {
    console.log("setTimeout");                        // 宏任务
}, 0);

async1();

new Promise(function(resolve) {
    console.log("promise1");                          // 同步 → 立即执行
    resolve();
}).then(function() {
    console.log("promise2");                          // 微任务
});

console.log("script end");                            // 同步 → 立即执行

/*
 * 执行分析:
 * 
 * === 第一轮:执行同步代码(全局宏任务)===
 * 调用栈:[global]
 * 输出:script start
 * 输出:async1 start
 * 输出:async2           (async2 函数体是同步的)
 *   → async1 中 await 后面的代码放入微任务队列
 * 输出:promise1          (Promise 构造函数是同步的)
 *   → then 回调放入微任务队列
 * 输出:script end
 * 
 * 此时微任务队列:[async1 end, promise2]
 * 此时宏任务队列:[setTimeout]
 * 
 * === 第二轮:清空微任务队列 ===
 * 输出:async1 end
 * 输出:promise2
 * 
 * === 第三轮:取一个宏任务 ===
 * 输出:setTimeout
 * 
 * 最终顺序:
 * script start → async1 start → async2 → promise1 → 
 * script end → async1 end → promise2 → setTimeout
 */

2.4 微任务中产生微任务

// 微任务中可以继续产生微任务,会在同一轮全部执行完
console.log("start");

setTimeout(() => console.log("timeout"), 0);

Promise.resolve()
    .then(() => {
        console.log("promise 1");
        // 在微任务中产生新的微任务
        Promise.resolve().then(() => {
            console.log("promise 1-1");
            Promise.resolve().then(() => {
                console.log("promise 1-1-1");
            });
        });
    })
    .then(() => {
        console.log("promise 2");
    });

console.log("end");

// 输出:start → end → promise 1 → promise 1-1 → promise 2 → promise 1-1-1 → timeout
// 注意:微任务全部执行完才会执行宏任务 setTimeout

// ⚠️ 危险:无限产生微任务会阻塞渲染
// Promise.resolve().then(function loop() {
//     Promise.resolve().then(loop); // 永远清不完微任务,页面卡死!
// });

2.5 Node.js 事件循环(与浏览器的区别)

/*
 * Node.js 事件循环有 6 个阶段:
 * 
 * ┌───────────────────────────┐
 * │         timers             │  ← setTimeout, setInterval
 * ├───────────────────────────┤
 * │     pending callbacks      │  ← 系统级回调(如 TCP 错误)
 * ├───────────────────────────┤
 * │       idle, prepare        │  ← 内部使用
 * ├───────────────────────────┤
 * │          poll              │  ← I/O 回调,在此阶段可能阻塞
 * ├───────────────────────────┤
 * │         check              │  ← setImmediate
 * ├───────────────────────────┤
 * │     close callbacks        │  ← socket.on('close')
 * └───────────────────────────┘
 * 
 * 每个阶段之间都会执行 process.nextTick 和 Promise 微任务
 * process.nextTick 优先级高于 Promise.then
 */

// Node.js 特有的优先级演示
process.nextTick(() => console.log("1. nextTick"));
Promise.resolve().then(() => console.log("2. promise"));
setTimeout(() => console.log("3. setTimeout"), 0);
setImmediate(() => console.log("4. setImmediate"));

// Node.js 输出:
// 1. nextTick      (最高优先级微任务)
// 2. promise       (普通微任务)
// 3. setTimeout    (timers 阶段)
// 4. setImmediate  (check 阶段)

3. 调用栈深入理解

// 调用栈是 LIFO(后进先出)结构
function multiply(a, b) {
    return a * b;           // 4. multiply 执行完毕,弹出栈
}

function square(n) {
    return multiply(n, n);  // 3. 调用 multiply,入栈
}                           // 5. square 执行完毕,弹出栈

function printSquare(n) {
    const result = square(n);  // 2. 调用 square,入栈
    console.log(result);       // 6. 调用 console.log
}

printSquare(4);  // 1. printSquare 入栈

/*
 * 调用栈变化过程:
 * 
 * Step 1: [printSquare]
 * Step 2: [printSquare, square]
 * Step 3: [printSquare, square, multiply]
 * Step 4: [printSquare, square]          ← multiply 返回
 * Step 5: [printSquare]                  ← square 返回
 * Step 6: [printSquare, console.log]
 * Step 7: [printSquare]                  ← console.log 返回
 * Step 8: []                             ← printSquare 返回
 *                                           调用栈空 → 事件循环检查队列
 */

// 栈溢出演示
function infiniteRecursion() {
    return infiniteRecursion(); // 无限递归
}
// infiniteRecursion(); 
// RangeError: Maximum call stack size exceeded

第二部分:回调函数

4. 回调函数基础

4.1 什么是回调

// 回调函数:作为参数传递给另一个函数,在适当时机被调用的函数

// === 同步回调 ===
const numbers = [1, 2, 3, 4, 5];

// forEach 的回调是同步执行的
numbers.forEach(function(num) {
    console.log(num); // 立即执行
});
console.log("forEach 之后"); // 在所有回调之后

// map 也是同步回调
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]


// === 异步回调 ===
console.log("请求开始");

// setTimeout 的回调是异步执行的
setTimeout(function callback() {
    console.log("1秒后执行"); // 至少1秒后
}, 1000);

console.log("请求已发起"); // 先于回调执行

4.2 Node.js 错误优先回调(Error-First Callback)

const fs = require('fs');

// Node.js 约定:回调的第一个参数是 error
fs.readFile('./config.json', 'utf8', function(err, data) {
    if (err) {
        // 错误处理
        if (err.code === 'ENOENT') {
            console.error('文件不存在');
        } else {
            console.error('读取失败:', err.message);
        }
        return; // 提前返回,不执行后续逻辑
    }
    
    // 成功处理
    const config = JSON.parse(data);
    console.log('配置:', config);
});

// 自己实现错误优先回调风格
function fetchUserData(userId, callback) {
    setTimeout(() => {
        if (!userId) {
            callback(new Error('userId is required'));
            return;
        }
        
        // 模拟数据库查询
        const user = {
            id: userId,
            name: 'Alice',
            email: 'alice@example.com'
        };
        
        callback(null, user); // 第一个参数为 null 表示没有错误
    }, 1000);
}

// 使用
fetchUserData(1, function(err, user) {
    if (err) {
        console.error('获取用户失败:', err.message);
        return;
    }
    console.log('用户信息:', user);
});

4.3 实际应用:事件监听器

// DOM 事件回调
const button = document.getElementById('myButton');

// 点击事件
button.addEventListener('click', function(event) {
    console.log('按钮被点击', event.target);
});

// 可以添加多个回调
button.addEventListener('click', handleClick);
button.addEventListener('mouseenter', handleHover);
button.addEventListener('mouseleave', handleLeave);

function handleClick(e) {
    console.log('处理点击');
}

function handleHover(e) {
    e.target.style.backgroundColor = '#eee';
}

function handleLeave(e) {
    e.target.style.backgroundColor = '';
}

// 移除事件监听(必须传入同一个函数引用)
button.removeEventListener('click', handleClick);

// ⚠️ 常见错误:匿名函数无法移除
// button.addEventListener('click', () => {}); // 无法移除这个监听器

5. 回调地狱(Callback Hell)

5.1 问题演示

// 需求:获取用户信息 → 获取用户订单 → 获取订单详情 → 获取物流信息

function getUserInfo(userId, callback) {
    setTimeout(() => callback(null, { id: userId, name: 'Alice' }), 300);
}

function getOrders(userId, callback) {
    setTimeout(() => callback(null, [{ orderId: 101 }, { orderId: 102 }]), 300);
}

function getOrderDetail(orderId, callback) {
    setTimeout(() => callback(null, { orderId, product: 'iPhone', trackingId: 'TK001' }), 300);
}

function getShippingInfo(trackingId, callback) {
    setTimeout(() => callback(null, { trackingId, status: '运输中', location: '上海' }), 300);
}

// 😱 回调地狱 - 金字塔形代码
getUserInfo(1, function(err, user) {
    if (err) {
        console.error('获取用户失败', err);
        return;
    }
    console.log('用户:', user.name);
    
    getOrders(user.id, function(err, orders) {
        if (err) {
            console.error('获取订单失败', err);
            return;
        }
        console.log('订单数:', orders.length);
        
        getOrderDetail(orders[0].orderId, function(err, detail) {
            if (err) {
                console.error('获取详情失败', err);
                return;
            }
            console.log('商品:', detail.product);
            
            getShippingInfo(detail.trackingId, function(err, shipping) {
                if (err) {
                    console.error('获取物流失败', err);
                    return;
                }
                console.log('物流状态:', shipping.status);
                console.log('当前位置:', shipping.location);
                
                // 如果还有更多层嵌套...
                // 代码会越来越难以维护
            });
        });
    });
});

5.2 回调地狱的问题

/*
 * 回调地狱的三大问题:
 * 
 * 1. 可读性差(Readability)
 *    - 代码向右缩进,形成"金字塔"
 *    - 逻辑流程难以追踪
 * 
 * 2. 错误处理困难(Error Handling)
 *    - 每一层都需要单独处理错误
 *    - 无法统一 catch
 *    - 容易遗漏错误处理
 * 
 * 3. 控制反转(Inversion of Control)
 *    - 把回调交给第三方库,你无法控制:
 *      - 回调是否会被调用
 *      - 回调会被调用几次
 *      - 回调是同步还是异步调用
 *      - 回调的参数是否正确
 */

// 控制反转的危险示例
function riskyThirdPartyLib(callback) {
    // 你无法控制第三方库如何调用你的回调
    callback(); // 调用了一次
    callback(); // 又调用了一次!  ← 可能导致重复计费等严重问题
    
    // 或者根本不调用
    // 或者同步调用(不在下一个 tick)
}

5.3 改善回调地狱的方法(不用 Promise)

// 方法1:命名函数 + 扁平化
function handleUser(err, user) {
    if (err) return console.error('获取用户失败', err);
    console.log('用户:', user.name);
    getOrders(user.id, handleOrders);
}

function handleOrders(err, orders) {
    if (err) return console.error('获取订单失败', err);
    console.log('订单数:', orders.length);
    getOrderDetail(orders[0].orderId, handleDetail);
}

function handleDetail(err, detail) {
    if (err) return console.error('获取详情失败', err);
    console.log('商品:', detail.product);
    getShippingInfo(detail.trackingId, handleShipping);
}

function handleShipping(err, shipping) {
    if (err) return console.error('获取物流失败', err);
    console.log('物流状态:', shipping.status);
}

// 启动链条
getUserInfo(1, handleUser);
// 代码变平了,但函数间的关系不够直观


// 方法2:使用工具库(如 async.js)
const async = require('async');

async.waterfall([
    function(cb) {
        getUserInfo(1, cb);
    },
    function(user, cb) {
        console.log('用户:', user.name);
        getOrders(user.id, cb);
    },
    function(orders, cb) {
        console.log('订单数:', orders.length);
        getOrderDetail(orders[0].orderId, cb);
    },
    function(detail, cb) {
        console.log('商品:', detail.product);
        getShippingInfo(detail.trackingId, cb);
    }
], function(err, shipping) {
    if (err) {
        console.error('流程出错:', err);
        return;
    }
    console.log('物流状态:', shipping.status);
});

第三部分:Promise

6. Promise 基础

6.1 什么是 Promise

/*
 * Promise 是一个代表异步操作最终结果的对象
 * 
 * 三种状态:
 * ┌─────────┐    resolve(value)    ┌───────────┐
 * │ pending  │ ──────────────────→ │ fulfilled  │
 * │ (等待中)  │                     │ (已成功)    │
 * └─────────┘                     └───────────┘
 *      │
 *      │  reject(reason)          ┌───────────┐
 *      └────────────────────────→ │ rejected   │
 *                                  │ (已失败)    │
 *                                  └───────────┘
 * 
 * 重要特性:
 * 1. 状态一旦改变就不可逆(pending → fulfilled 或 pending → rejected)
 * 2. 状态改变后,任何时候都可以获取结果
 */

// 创建 Promise
const promise = new Promise(function(resolve, reject) {
    // 这个函数叫做 executor(执行器),立即同步执行
    console.log("executor 执行了"); // 同步执行!
    
    // 异步操作
    setTimeout(() => {
        const success = true;
        
        if (success) {
            resolve("操作成功的数据"); // 将 promise 变为 fulfilled
        } else {
            reject(new Error("操作失败的原因")); // 将 promise 变为 rejected
        }
    }, 1000);
});

console.log("Promise 创建后"); // 在 executor 之后,在异步回调之前

// 消费 Promise
promise.then(
    function onFulfilled(value) {
        console.log("成功:", value);
    },
    function onRejected(reason) {
        console.log("失败:", reason.message);
    }
);

6.2 将回调转换为 Promise

// 改造之前回调风格的函数
function getUserInfo(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (!userId) {
                reject(new Error('userId is required'));
                return;
            }
            resolve({ id: userId, name: 'Alice' });
        }, 300);
    });
}

function getOrders(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve([{ orderId: 101 }, { orderId: 102 }]);
        }, 300);
    });
}

function getOrderDetail(orderId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ orderId, product: 'iPhone', trackingId: 'TK001' });
        }, 300);
    });
}

function getShippingInfo(trackingId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ trackingId, status: '运输中', location: '上海' });
        }, 300);
    });
}

// Node.js 提供的通用转换工具
const { promisify } = require('util');
const fs = require('fs');

// 将回调风格的 fs.readFile 转为 Promise 风格
const readFile = promisify(fs.readFile);
readFile('./config.json', 'utf8').then(data => console.log(data));

// 手写 promisify
function myPromisify(fn) {
    return function(...args) {
        return new Promise((resolve, reject) => {
            fn(...args, (err, result) => {
                if (err) reject(err);
                else resolve(result);
            });
        });
    };
}

6.3 Promise 基本使用

// .then() 处理成功
// .catch() 处理失败
// .finally() 无论成功失败都执行

function fetchData(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (url.includes("error")) {
                reject(new Error(`请求失败: ${url}`));
            } else {
                resolve({ data: `来自 ${url} 的数据`, status: 200 });
            }
        }, 500);
    });
}

fetchData("https://api.example.com/data")
    .then(result => {
        console.log("成功:", result.data);
    })
    .catch(error => {
        console.error("失败:", error.message);
    })
    .finally(() => {
        console.log("请求完成(无论成败)");
        // 常用于:隐藏 loading、释放资源等
    });

7. Promise 链式调用

7.1 链式调用原理

/*
 * .then() 返回一个新的 Promise,这是链式调用的关键
 * 
 * 返回值规则:
 * 1. return 普通值 → 新 Promise 以该值 resolve
 * 2. return Promise → 新 Promise 跟随该 Promise 的状态
 * 3. throw 错误   → 新 Promise 以该错误 reject
 * 4. 不 return    → 新 Promise 以 undefined resolve
 */

// 基本链式调用
Promise.resolve(1)
    .then(value => {
        console.log(value); // 1
        return value + 1;   // return 普通值
    })
    .then(value => {
        console.log(value); // 2
        return Promise.resolve(value + 1); // return Promise
    })
    .then(value => {
        console.log(value); // 3
        // 不 return
    })
    .then(value => {
        console.log(value); // undefined
        throw new Error("出错了"); // throw 错误
    })
    .catch(err => {
        console.error(err.message); // "出错了"
        return "recovered"; // catch 也可以 return,链继续
    })
    .then(value => {
        console.log(value); // "recovered"
    });

7.2 用链式调用解决回调地狱

// 之前的回调地狱,用 Promise 链改写
getUserInfo(1)
    .then(user => {
        console.log('用户:', user.name);
        return getOrders(user.id);
    })
    .then(orders => {
        console.log('订单数:', orders.length);
        return getOrderDetail(orders[0].orderId);
    })
    .then(detail => {
        console.log('商品:', detail.product);
        return getShippingInfo(detail.trackingId);
    })
    .then(shipping => {
        console.log('物流状态:', shipping.status);
        console.log('当前位置:', shipping.location);
    })
    .catch(err => {
        // 统一错误处理!任何一步失败都会到这里
        console.error('流程出错:', err.message);
    });

// 代码扁平化、错误统一处理、流程清晰

7.3 链式调用中传递数据

// 问题:后面的 .then 需要用到前面多个步骤的数据

// 方案1:闭包(简单但变量多了会乱)
let savedUser;
getUserInfo(1)
    .then(user => {
        savedUser = user; // 保存到外部变量
        return getOrders(user.id);
    })
    .then(orders => {
        console.log(savedUser.name, '有', orders.length, '个订单');
    });

// 方案2:逐层传递对象(推荐)
getUserInfo(1)
    .then(user => {
        return getOrders(user.id).then(orders => ({
            user,
            orders
        }));
    })
    .then(({ user, orders }) => {
        console.log(user.name, '有', orders.length, '个订单');
        return getOrderDetail(orders[0].orderId).then(detail => ({
            user,
            orders,
            detail
        }));
    })
    .then(({ user, orders, detail }) => {
        console.log(`${user.name} 购买了 ${detail.product}`);
    });

// 方案3:async/await(最佳方案,后面会讲)
async function getFullInfo() {
    const user = await getUserInfo(1);
    const orders = await getOrders(user.id);
    const detail = await getOrderDetail(orders[0].orderId);
    const shipping = await getShippingInfo(detail.trackingId);
    
    // 所有变量都在同一作用域!
    console.log(`${user.name} 购买了 ${detail.product}${shipping.status}`);
}

8. Promise 错误处理

8.1 错误捕获机制

// .catch() 相当于 .then(undefined, onRejected)

// 方式1:.then 的第二个参数
promise.then(
    value => console.log(value),
    error => console.error(error)  // 只能捕获 promise 本身的错误
);

// 方式2:.catch()(推荐)
promise
    .then(value => {
        // 如果这里抛出错误...
        throw new Error("then 中的错误");
    })
    .catch(error => {
        // .catch 可以捕获前面所有 .then 中的错误
        console.error(error.message);
    });

// 区别演示
const p = Promise.reject(new Error("初始错误"));

// ❌ .then 的第二个参数无法捕获同一个 .then 的第一个参数中的错误
p.then(
    value => { throw new Error("then 中的错误"); },
    error => console.log("捕获:", error.message) // 捕获的是"初始错误"
);

// ✅ .catch 可以捕获链上任何位置的错误
p.then(value => {
    throw new Error("then 中的错误");
}).catch(error => {
    console.log("捕获:", error.message); // 可以捕获两种错误
});

8.2 错误传播

// 错误会沿着链向下传播,直到被 catch
Promise.resolve("start")
    .then(v => {
        console.log("step 1:", v);
        throw new Error("step 1 出错");
    })
    .then(v => {
        console.log("step 2:", v); // ❌ 跳过!不执行
    })
    .then(v => {
        console.log("step 3:", v); // ❌ 跳过!不执行
    })
    .catch(err => {
        console.log("捕获错误:", err.message); // "step 1 出错"
        return "error handled"; // 错误恢复
    })
    .then(v => {
        console.log("step 4:", v); // ✅ "error handled" — 继续执行
    });

8.3 多层错误处理

// 可以在链的不同位置放置 catch
fetchData("/api/users")
    .then(users => {
        return processUsers(users);
    })
    .catch(err => {
        // 处理获取/处理用户数据的错误
        console.warn("用户数据处理失败,使用缓存:", err.message);
        return getCachedUsers(); // 降级方案
    })
    .then(users => {
        return fetchData(`/api/users/${users[0].id}/orders`);
    })
    .catch(err => {
        // 处理获取订单的错误
        console.warn("订单获取失败:", err.message);
        return []; // 返回空数组作为默认值
    })
    .then(orders => {
        renderOrders(orders);
    })
    .catch(err => {
        // 最终的错误兜底
        showErrorPage(err);
    });

8.4 未处理的 Promise 拒绝

// ⚠️ 危险:没有 catch 的 rejected Promise
const unhandled = Promise.reject(new Error("无人处理的错误"));
// 浏览器控制台会警告:UnhandledPromiseRejectionWarning
// Node.js 15+ 会直接终止进程!

// 全局捕获未处理的 rejection

// 浏览器环境
window.addEventListener('unhandledrejection', event => {
    console.error('未处理的 Promise 拒绝:', event.reason);
    event.preventDefault(); // 阻止默认的控制台错误输出
    
    // 上报错误到监控系统
    reportError({
        type: 'unhandledrejection',
        message: event.reason?.message || String(event.reason),
        stack: event.reason?.stack
    });
});

// Node.js 环境
process.on('unhandledRejection', (reason, promise) => {
    console.error('未处理的 Promise 拒绝:', reason);
    // 推荐:记录日志后优雅退出
});

process.on('rejectionHandled', (promise) => {
    // 之前未处理的 rejection 后来被处理了
    console.log('延迟处理的 rejection');
});

9. Promise 静态方法

9.1 Promise.resolve() 和 Promise.reject()

// Promise.resolve() — 创建一个 fulfilled 的 Promise
const p1 = Promise.resolve(42);
p1.then(v => console.log(v)); // 42

// 传入 Promise 会直接返回
const p2 = Promise.resolve(Promise.resolve("hello"));
p2.then(v => console.log(v)); // "hello"(不会嵌套)

// 传入 thenable 对象(有 then 方法的对象)
const thenable = {
    then(resolve, reject) {
        resolve("from thenable");
    }
};
Promise.resolve(thenable).then(v => console.log(v)); // "from thenable"


// Promise.reject() — 创建一个 rejected 的 Promise
const p3 = Promise.reject(new Error("失败"));
p3.catch(err => console.log(err.message)); // "失败"

// 注意:reject 不会解包 Promise
const p4 = Promise.reject(Promise.resolve("嵌套"));
p4.catch(v => console.log(v)); // Promise {<fulfilled>: "嵌套"} ← 注意是 Promise 对象

9.2 Promise.all() — 全部成功才成功

/*
 * Promise.all(iterable)
 * - 所有 Promise 都 fulfilled → 结果数组(顺序与输入一致)
 * - 任何一个 rejected → 立即 rejected(快速失败)
 */

// 并行请求多个 API
const userPromise = fetch('/api/user').then(r => r.json());
const ordersPromise = fetch('/api/orders').then(r => r.json());
const settingsPromise = fetch('/api/settings').then(r => r.json());

Promise.all([userPromise, ordersPromise, settingsPromise])
    .then(([user, orders, settings]) => {
        // 三个请求都完成后才执行
        console.log('用户:', user);
        console.log('订单:', orders);
        console.log('设置:', settings);
        
        renderDashboard(user, orders, settings);
    })
    .catch(err => {
        // 任何一个失败就到这里
        console.error('加载仪表盘失败:', err);
    });

// 空数组
Promise.all([]).then(v => console.log(v)); // [](立即 fulfilled)

// 包含非 Promise 值
Promise.all([1, "hello", Promise.resolve(true)])
    .then(values => console.log(values)); // [1, "hello", true]

// 实际应用:批量上传文件
async function uploadFiles(files) {
    const uploadPromises = files.map(file => {
        return fetch('/api/upload', {
            method: 'POST',
            body: file
        });
    });
    
    try {
        const results = await Promise.all(uploadPromises);
        console.log('全部上传成功');
        return results;
    } catch (err) {
        console.error('有文件上传失败:', err);
        throw err;
    }
}

9.3 Promise.allSettled() — 等所有完成(不论成败)

/*
 * Promise.allSettled(iterable)  [ES2020]
 * - 等待所有 Promise 完成(settled = fulfilled 或 rejected)
 * - 永远不会 reject
 * - 结果数组中每个元素:
 *   { status: "fulfilled", value: ... }
 *   { status: "rejected", reason: ... }
 */

const promises = [
    fetch('/api/user'),
    fetch('/api/nonexistent'),  // 这个会失败
    fetch('/api/settings')
];

Promise.allSettled(promises)
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`请求 ${index} 成功:`, result.value);
            } else {
                console.log(`请求 ${index} 失败:`, result.reason);
            }
        });
        
        // 过滤出成功的结果
        const successful = results
            .filter(r => r.status === 'fulfilled')
            .map(r => r.value);
        
        // 过滤出失败的结果
        const failed = results
            .filter(r => r.status === 'rejected')
            .map(r => r.reason);
        
        console.log(`${successful.length} 个成功, ${failed.length} 个失败`);
    });

// 实际场景:批量通知(不因某个失败就停止)
async function notifyUsers(userIds) {
    const notifications = userIds.map(id => sendNotification(id));
    const results = await Promise.allSettled(notifications);
    
    const report = {
        total: results.length,
        success: results.filter(r => r.status === 'fulfilled').length,
        failed: results.filter(r => r.status === 'rejected').length,
        errors: results
            .filter(r => r.status === 'rejected')
            .map(r => r.reason.message)
    };
    
    console.log('通知报告:', report);
    return report;
}

9.4 Promise.race() — 最快的那个

/*
 * Promise.race(iterable)
 * - 返回最先 settle 的 Promise 的结果(无论成功失败)
 */

// 超时控制
function fetchWithTimeout(url, timeoutMs = 5000) {
    const fetchPromise = fetch(url);
    
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error(`请求超时 (${timeoutMs}ms): ${url}`));
        }, timeoutMs);
    });
    
    return Promise.race([fetchPromise, timeoutPromise]);
}

// 使用
fetchWithTimeout('https://api.example.com/data', 3000)
    .then(response => response.json())
    .then(data => console.log('数据:', data))
    .catch(err => console.error(err.message));

// 多个数据源竞速
function fetchFromFastestCDN(resource) {
    return Promise.race([
        fetch(`https://cdn1.example.com/${resource}`),
        fetch(`https://cdn2.example.com/${resource}`),
        fetch(`https://cdn3.example.com/${resource}`)
    ]);
}

// 注意:其他未完成的 Promise 不会被取消,只是结果被忽略

9.5 Promise.any() — 第一个成功的

/*
 * Promise.any(iterable)  [ES2021]
 * - 返回第一个 fulfilled 的 Promise
 * - 全部 rejected → 返回 AggregateError
 * 
 * vs Promise.race():
 * - race: 第一个 settled(无论成败)
 * - any:  第一个 fulfilled(忽略 rejected)
 */

// 从多个镜像获取资源
const mirrors = [
    fetch('https://mirror1.example.com/data.json'),
    fetch('https://mirror2.example.com/data.json'),
    fetch('https://mirror3.example.com/data.json')
];

Promise.any(mirrors)
    .then(response => {
        console.log('从最快的可用镜像获取到数据');
        return response.json();
    })
    .catch(err => {
        // AggregateError: All promises were rejected
        console.error('所有镜像都不可用');
        console.error('错误列表:', err.errors); // 所有错误的数组
    });

// 对比 race 和 any
const p1 = new Promise((_, reject) => setTimeout(() => reject('p1 fail'), 100));
const p2 = new Promise((resolve) => setTimeout(() => resolve('p2 success'), 200));

Promise.race([p1, p2]).catch(e => console.log('race:', e));  // "race: p1 fail"
Promise.any([p1, p2]).then(v => console.log('any:', v));      // "any: p2 success"

9.6 Promise.withResolvers() [ES2024]

/*
 * Promise.withResolvers() — 将 resolve/reject 提取到外部
 * 返回 { promise, resolve, reject }
 */

// 之前的写法
let externalResolve, externalReject;
const promise = new Promise((resolve, reject) => {
    externalResolve = resolve;
    externalReject = reject;
});

// ES2024 新写法
const { promise: p, resolve, reject } = Promise.withResolvers();

// 实际用途:在其他地方控制 Promise 的状态
class EventEmitter {
    #listeners = new Map();
    
    waitFor(eventName) {
        const { promise, resolve } = Promise.withResolvers();
        this.#listeners.set(eventName, resolve);
        return promise;
    }
    
    emit(eventName, data) {
        const resolve = this.#listeners.get(eventName);
        if (resolve) {
            resolve(data);
            this.#listeners.delete(eventName);
        }
    }
}

const emitter = new EventEmitter();
emitter.waitFor('data').then(data => console.log('收到:', data));
emitter.emit('data', { message: 'hello' }); // 收到: { message: 'hello' }

9.7 静态方法对比总结

/*
 * ┌──────────────────┬───────────────┬─────────────────────────────┐
 * │      方法         │    何时 resolve │       何时 reject            │
 * ├──────────────────┼───────────────┼─────────────────────────────┤
 * │ Promise.all      │ 全部 fulfilled │ 任一 rejected(快速失败)     │
 * │ Promise.allSettled│ 全部 settled  │ 永不 reject                  │
 * │ Promise.race     │ 首个 fulfilled │ 首个 rejected                │
 * │ Promise.any      │ 首个 fulfilled │ 全部 rejected(AggregateError)│
 * └──────────────────┴───────────────┴─────────────────────────────┘
 */

// 完整对比示例
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 500));
const fail = new Promise((_, reject) => setTimeout(() => reject('fail'), 200));

// all: 等全部,一个失败就失败
Promise.all([fast, slow, fail]).catch(e => console.log('all:', e)); // "fail"

// allSettled: 等全部,告诉你每个的结果
Promise.allSettled([fast, slow, fail]).then(r => console.log('allSettled:', r));
// [{status:'fulfilled',value:'fast'}, {status:'fulfilled',value:'slow'}, {status:'rejected',reason:'fail'}]

// race: 第一个完成的(无论成败)
Promise.race([fast, slow, fail]).then(v => console.log('race:', v)); // "fast"

// any: 第一个成功的
Promise.any([fast, slow, fail]).then(v => console.log('any:', v)); // "fast"

10. 手写 Promise(面试高频)

class MyPromise {
    static PENDING = 'pending';
    static FULFILLED = 'fulfilled';
    static REJECTED = 'rejected';

    constructor(executor) {
        this.status = MyPromise.PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = (value) => {
            // 处理 resolve 一个 Promise 的情况
            if (value instanceof MyPromise) {
                value.then(resolve, reject);
                return;
            }
            if (this.status === MyPromise.PENDING) {
                this.status = MyPromise.FULFILLED;
                this.value = value;
                this.onFulfilledCallbacks.forEach(fn => fn());
            }
        };

        const reject = (reason) => {
            if (this.status === MyPromise.PENDING) {
                this.status = MyPromise.REJECTED;
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        // 参数默认值:实现值穿透
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

        const promise2 = new MyPromise((resolve, reject) => {
            const fulfilledMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const x = onFulfilled(this.value);
                        this.#resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            };

            const rejectedMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const x = onRejected(this.reason);
                        this.#resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            };

            if (this.status === MyPromise.FULFILLED) {
                fulfilledMicrotask();
            } else if (this.status === MyPromise.REJECTED) {
                rejectedMicrotask();
            } else {
                // pending 状态:收集回调
                this.onFulfilledCallbacks.push(fulfilledMicrotask);
                this.onRejectedCallbacks.push(rejectedMicrotask);
            }
        });

        return promise2;
    }

    // Promise Resolution Procedure (Promises/A+ 规范核心)
    #resolvePromise(promise2, x, resolve, reject) {
        // 不能返回自己(防止死循环)
        if (promise2 === x) {
            reject(new TypeError('Chaining cycle detected'));
            return;
        }

        if (x instanceof MyPromise) {
            x.then(resolve, reject);
        } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
            // 处理 thenable
            let called = false;
            try {
                const then = x.then;
                if (typeof then === 'function') {
                    then.call(x,
                        y => {
                            if (called) return;
                            called = true;
                            this.#resolvePromise(promise2, y, resolve, reject);
                        },
                        r => {
                            if (called) return;
                            called = true;
                            reject(r);
                        }
                    );
                } else {
                    resolve(x);
                }
            } catch (error) {
                if (called) return;
                called = true;
                reject(error);
            }
        } else {
            resolve(x);
        }
    }

    catch(onRejected) {
        return this.then(undefined, onRejected);
    }

    finally(callback) {
        return this.then(
            value => MyPromise.resolve(callback()).then(() => value),
            reason => MyPromise.resolve(callback()).then(() => { throw reason; })
        );
    }

    static resolve(value) {
        if (value instanceof MyPromise) return value;
        return new MyPromise(resolve => resolve(value));
    }

    static reject(reason) {
        return new MyPromise((_, reject) => reject(reason));
    }

    static all(promises) {
        return new MyPromise((resolve, reject) => {
            const results = [];
            let count = 0;
            const promiseArr = Array.from(promises);
            
            if (promiseArr.length === 0) {
                resolve([]);
                return;
            }

            promiseArr.forEach((p, index) => {
                MyPromise.resolve(p).then(
                    value => {
                        results[index] = value;
                        count++;
                        if (count === promiseArr.length) {
                            resolve(results);
                        }
                    },
                    reject
                );
            });
        });
    }

    static race(promises) {
        return new MyPromise((resolve, reject) => {
            for (const p of promises) {
                MyPromise.resolve(p).then(resolve, reject);
            }
        });
    }

    static allSettled(promises) {
        return new MyPromise((resolve) => {
            const results = [];
            let count = 0;
            const promiseArr = Array.from(promises);
            
            if (promiseArr.length === 0) {
                resolve([]);
                return;
            }

            promiseArr.forEach((p, index) => {
                MyPromise.resolve(p).then(
                    value => {
                        results[index] = { status: 'fulfilled', value };
                        if (++count === promiseArr.length) resolve(results);
                    },
                    reason => {
                        results[index] = { status: 'rejected', reason };
                        if (++count === promiseArr.length) resolve(results);
                    }
                );
            });
        });
    }

    static any(promises) {
        return new MyPromise((resolve, reject) => {
            const errors = [];
            let count = 0;
            const promiseArr = Array.from(promises);
            
            if (promiseArr.length === 0) {
                reject(new AggregateError([], 'All promises were rejected'));
                return;
            }

            promiseArr.forEach((p, index) => {
                MyPromise.resolve(p).then(resolve, reason => {
                    errors[index] = reason;
                    if (++count === promiseArr.length) {
                        reject(new AggregateError(errors, 'All promises were rejected'));
                    }
                });
            });
        });
    }
}

// 测试
const test = new MyPromise((resolve) => {
    setTimeout(() => resolve('hello'), 100);
});

test.then(v => {
    console.log(v); // 'hello'
    return v + ' world';
}).then(v => {
    console.log(v); // 'hello world'
});

第四部分:Async/Await

11. 基本语法

11.1 async 函数

// async 函数始终返回一个 Promise

// 声明方式
async function fetchUser() {
    return { name: 'Alice' }; // 自动包装为 Promise.resolve({ name: 'Alice' })
}

// 等价于
function fetchUser() {
    return Promise.resolve({ name: 'Alice' });
}

// 箭头函数
const fetchUser2 = async () => ({ name: 'Bob' });

// 类方法
class UserService {
    async getUser(id) {
        return { id, name: 'Charlie' };
    }
}

// 验证返回 Promise
const result = fetchUser();
console.log(result);              // Promise {<fulfilled>: { name: 'Alice' }}
console.log(result instanceof Promise); // true

result.then(user => console.log(user)); // { name: 'Alice' }

11.2 await 关键字

/*
 * await 做了什么:
 * 1. 暂停 async 函数的执行
 * 2. 等待 Promise settle
 * 3. 如果 fulfilled → 返回 value
 * 4. 如果 rejected → 抛出 reason
 * 5. 恢复 async 函数的执行
 * 
 * await 只能在 async 函数内使用(或在 ES 模块的顶层)
 */

async function demo() {
    console.log("开始");
    
    // await 一个 Promise
    const value = await new Promise(resolve => {
        setTimeout(() => resolve("异步结果"), 1000);
    });
    console.log("得到:", value); // 1秒后: "异步结果"
    
    // await 非 Promise 值会立即继续(自动包装为 Promise.resolve)
    const num = await 42;
    console.log("数字:", num); // 42
    
    // await 一个 rejected Promise 会抛出错误
    try {
        const fail = await Promise.reject(new Error("出错了"));
    } catch (err) {
        console.error("捕获:", err.message); // "出错了"
    }
    
    console.log("结束");
}

demo();

// 顶层 await(ES Modules 中)
// 在 .mjs 文件或 type:"module" 中可以直接使用
// const data = await fetch('/api/data').then(r => r.json());

11.3 用 async/await 改写 Promise 链

// Promise 链版本
function getFullUserInfo_promise(userId) {
    return getUserInfo(userId)
        .then(user => {
            return getOrders(user.id).then(orders => ({ user, orders }));
        })
        .then(({ user, orders }) => {
            return getOrderDetail(orders[0].orderId)
                .then(detail => ({ user, orders, detail }));
        })
        .then(({ user, orders, detail }) => {
            return getShippingInfo(detail.trackingId)
                .then(shipping => ({ user, orders, detail, shipping }));
        });
}

// async/await 版本 ✨
async function getFullUserInfo(userId) {
    const user = await getUserInfo(userId);
    const orders = await getOrders(user.id);
    const detail = await getOrderDetail(orders[0].orderId);
    const shipping = await getShippingInfo(detail.trackingId);
    
    return { user, orders, detail, shipping };
}

// 使用
getFullUserInfo(1).then(info => {
    console.log(`${info.user.name} 购买了 ${info.detail.product}`);
    console.log(`物流状态: ${info.shipping.status}`);
});

// 或者在另一个 async 函数中
async function main() {
    const info = await getFullUserInfo(1);
    console.log(info);
}
main();

12. 错误处理

12.1 try/catch

// 最直接的方式
async function fetchUserSafely(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const user = await response.json();
        return user;
    } catch (error) {
        if (error.name === 'TypeError') {
            console.error('网络错误:', error.message);
        } else {
            console.error('请求失败:', error.message);
        }
        return null; // 返回默认值
    } finally {
        hideLoadingSpinner();
    }
}

// 嵌套 try/catch 处理不同阶段的错误
async function processOrder(orderId) {
    let order;
    
    try {
        order = await fetchOrder(orderId);
    } catch (err) {
        console.error('获取订单失败');
        throw new Error('ORDER_FETCH_FAILED');
    }
    
    try {
        await validateOrder(order);
    } catch (err) {
        console.error('订单验证失败');
        throw new Error('ORDER_VALIDATION_FAILED');
    }
    
    try {
        const result = await submitPayment(order);
        return result;
    } catch (err) {
        console.error('支付失败');
        await rollbackOrder(order);
        throw new Error('PAYMENT_FAILED');
    }
}

12.2 优雅的错误处理模式

// 模式1:Go 风格的错误处理
async function to(promise) {
    try {
        const result = await promise;
        return [null, result];
    } catch (error) {
        return [error, null];
    }
}

// 使用
async function main() {
    const [err, user] = await to(getUserInfo(1));
    if (err) {
        console.error('获取用户失败:', err.message);
        return;
    }
    
    const [err2, orders] = await to(getOrders(user.id));
    if (err2) {
        console.error('获取订单失败:', err2.message);
        return;
    }
    
    console.log(user, orders);
}

// 模式2:包装函数添加错误处理
function withErrorHandler(fn, errorHandler) {
    return async function(...args) {
        try {
            return await fn.apply(this, args);
        } catch (error) {
            return errorHandler(error, ...args);
        }
    };
}

const safeGetUser = withErrorHandler(
    async (id) => {
        const response = await fetch(`/api/users/${id}`);
        return response.json();
    },
    (error, id) => {
        console.error(`获取用户 ${id} 失败:`, error);
        return null;
    }
);

const user = await safeGetUser(123);

// 模式3:装饰器模式(TypeScript/提案阶段)
function catchError(target, name, descriptor) {
    const original = descriptor.value;
    descriptor.value = async function(...args) {
        try {
            return await original.apply(this, args);
        } catch (error) {
            console.error(`${name} 执行出错:`, error);
            throw error;
        }
    };
    return descriptor;
}

12.3 重试模式

// 带指数退避的重试
async function retry(fn, options = {}) {
    const {
        maxRetries = 3,
        baseDelay = 1000,
        maxDelay = 10000,
        backoffFactor = 2,
        retryOn = () => true, // 判断是否应该重试
    } = options;

    let lastError;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            return await fn(attempt);
        } catch (error) {
            lastError = error;
            
            if (attempt === maxRetries || !retryOn(error)) {
                throw error;
            }

            const delay = Math.min(
                baseDelay * Math.pow(backoffFactor, attempt),
                maxDelay
            );
            
            // 添加随机抖动,避免雷群效应
            const jitter = delay * 0.1 * Math.random();
            const totalDelay = delay + jitter;

            console.warn(
                `第 ${attempt + 1} 次失败,${totalDelay.toFixed(0)}ms 后重试:`, 
                error.message
            );
            
            await new Promise(resolve => setTimeout(resolve, totalDelay));
        }
    }

    throw lastError;
}

// 使用
async function fetchWithRetry(url) {
    return retry(
        async (attempt) => {
            console.log(`第 ${attempt + 1} 次尝试请求 ${url}`);
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return response.json();
        },
        {
            maxRetries: 3,
            baseDelay: 1000,
            retryOn: (error) => {
                // 只对特定错误重试
                return error.message.includes('500') || 
                       error.message.includes('503') ||
                       error.name === 'TypeError'; // 网络错误
            }
        }
    );
}

const data = await fetchWithRetry('https://api.example.com/data');

13. Async/Await 常见模式

13.1 串行 vs 并行

// ❌ 串行执行(慢!)— 每个请求等上一个完成
async function serial() {
    const start = Date.now();
    
    const user = await fetchUser();      // 等 1 秒
    const orders = await fetchOrders();  // 再等 1 秒
    const products = await fetchProducts(); // 再等 1 秒
    
    console.log(`串行总耗时: ${Date.now() - start}ms`); // ≈ 3000ms
}

// ✅ 并行执行(快!)— 所有请求同时发出
async function parallel() {
    const start = Date.now();
    
    // 先发起所有请求(不 await)
    const userPromise = fetchUser();
    const ordersPromise = fetchOrders();
    const productsPromise = fetchProducts();
    
    // 再等待所有结果
    const user = await userPromise;
    const orders = await ordersPromise;
    const products = await productsPromise;
    
    console.log(`并行总耗时: ${Date.now() - start}ms`); // ≈ 1000ms
}

// ✅ 更推荐用 Promise.all
async function parallelWithAll() {
    const start = Date.now();
    
    const [user, orders, products] = await Promise.all([
        fetchUser(),
        fetchOrders(),
        fetchProducts()
    ]);
    
    console.log(`并行总耗时: ${Date.now() - start}ms`); // ≈ 1000ms
}

// 混合:部分串行,部分并行
async function mixed() {
    // 先获取用户(必须先有用户信息)
    const user = await fetchUser();
    
    // 然后并行获取用户的订单和收藏(互不依赖)
    const [orders, favorites] = await Promise.all([
        fetchOrders(user.id),
        fetchFavorites(user.id)
    ]);
    
    return { user, orders, favorites };
}

13.2 循环中的 async/await

const urls = [
    '/api/data/1',
    '/api/data/2',
    '/api/data/3'
];

// ❌ forEach 中的 await 不会等待!
async function badLoop() {
    urls.forEach(async (url) => {
        const data = await fetch(url);  // forEach 不会等这个
        console.log(data);
    });
    console.log("完成");  // 这行会在所有 fetch 之前执行!
}

// ✅ 串行:for...of
async function serialLoop() {
    const results = [];
    for (const url of urls) {
        const response = await fetch(url);
        const data = await response.json();
        results.push(data);
        console.log(`完成: ${url}`);
    }
    return results; // 按顺序串行执行
}

// ✅ 并行:Promise.all + map
async function parallelLoop() {
    const results = await Promise.all(
        urls.map(async (url) => {
            const response = await fetch(url);
            return response.json();
        })
    );
    return results; // 并行执行,结果顺序与 urls 一致
}

// ✅ 控制并发数的并行(后面会详细讲)
async function limitedParallelLoop() {
    const limit = 2; // 最多同时2个请求
    const results = [];
    
    for (let i = 0; i < urls.length; i += limit) {
        const batch = urls.slice(i, i + limit);
        const batchResults = await Promise.all(
            batch.map(url => fetch(url).then(r => r.json()))
        );
        results.push(...batchResults);
    }
    
    return results;
}

// ✅ for await...of(异步迭代器)
async function* fetchAll(urls) {
    for (const url of urls) {
        const response = await fetch(url);
        yield await response.json();
    }
}

async function asyncIteratorLoop() {
    for await (const data of fetchAll(urls)) {
        console.log(data);
    }
}

13.3 条件异步

// 根据条件决定是否执行异步操作
async function getUser(id, options = {}) {
    const { useCache = true } = options;
    
    // 有缓存就直接返回(同步路径)
    if (useCache) {
        const cached = cache.get(`user:${id}`);
        if (cached) return cached;
    }
    
    // 无缓存则请求(异步路径)
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    
    cache.set(`user:${id}`, user);
    return user;
}

// 竞态条件处理
let currentRequestId = 0;

async function search(query) {
    const requestId = ++currentRequestId;
    
    const results = await fetchSearchResults(query);
    
    // 如果在等待期间又发起了新请求,丢弃当前结果
    if (requestId !== currentRequestId) {
        console.log('过时的结果,已丢弃');
        return;
    }
    
    displayResults(results);
}

// 更好的方式:使用 AbortController
let currentController = null;

async function searchWithAbort(query) {
    // 取消之前的请求
    if (currentController) {
        currentController.abort();
    }
    
    currentController = new AbortController();
    
    try {
        const response = await fetch(`/api/search?q=${query}`, {
            signal: currentController.signal
        });
        const results = await response.json();
        displayResults(results);
    } catch (err) {
        if (err.name === 'AbortError') {
            console.log('请求已取消');
        } else {
            throw err;
        }
    }
}

13.4 async/await 与类

class DataService {
    #baseUrl;
    #cache = new Map();
    
    constructor(baseUrl) {
        this.#baseUrl = baseUrl;
    }
    
    // 异步方法
    async get(endpoint) {
        const url = `${this.#baseUrl}${endpoint}`;
        
        if (this.#cache.has(url)) {
            return this.#cache.get(url);
        }
        
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        const data = await response.json();
        this.#cache.set(url, data);
        return data;
    }
    
    async post(endpoint, body) {
        const response = await fetch(`${this.#baseUrl}${endpoint}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(body)
        });
        return response.json();
    }
    
    // 静态异步方法
    static async create(baseUrl) {
        const service = new DataService(baseUrl);
        // 可以在工厂方法中做异步初始化
        await service.get('/health'); // 检查服务是否可用
        return service;
    }
}

// 使用(注意:constructor 不能是 async 的)
async function main() {
    const api = await DataService.create('https://api.example.com');
    const users = await api.get('/users');
    console.log(users);
}

第五部分:高级异步模式

14. Generator 与异步迭代

14.1 Generator 基础

// Generator 函数:可以暂停和恢复的函数
function* numberGenerator() {
    console.log("开始");
    yield 1;       // 暂停,返回 1
    console.log("继续");
    yield 2;       // 暂停,返回 2
    console.log("再继续");
    return 3;      // 结束
}

const gen = numberGenerator(); // 不会立即执行!

console.log(gen.next()); // "开始"  → { value: 1, done: false }
console.log(gen.next()); // "继续"  → { value: 2, done: false }
console.log(gen.next()); // "再继续" → { value: 3, done: true }
console.log(gen.next()); //          → { value: undefined, done: true }

// yield 可以接收外部传入的值
function* conversation() {
    const name = yield "你叫什么名字?";
    const age = yield `${name},你多大了?`;
    return `${name} 今年 ${age} 岁`;
}

const chat = conversation();
console.log(chat.next());          // { value: "你叫什么名字?", done: false }
console.log(chat.next("Alice"));   // { value: "Alice,你多大了?", done: false }
console.log(chat.next(25));        // { value: "Alice 今年 25 岁", done: true }

14.2 Generator 实现异步流程控制

// Generator + Promise = async/await 的前身

function* fetchUserFlow() {
    try {
        const user = yield getUserInfo(1);        // yield 一个 Promise
        console.log('用户:', user.name);
        
        const orders = yield getOrders(user.id);  // yield 另一个 Promise
        console.log('订单数:', orders.length);
        
        return { user, orders };
    } catch (err) {
        console.error('出错:', err.message);
    }
}

// 自动执行器(co 库的简化版)
function run(generatorFn) {
    return new Promise((resolve, reject) => {
        const gen = generatorFn();
        
        function step(nextFn) {
            let result;
            try {
                result = nextFn();
            } catch (err) {
                return reject(err);
            }
            
            if (result.done) {
                return resolve(result.value);
            }
            
            // 假设 yield 的都是 Promise
            Promise.resolve(result.value).then(
                value => step(() => gen.next(value)),    // 将结果送回 generator
                error => step(() => gen.throw(error))    // 将错误送回 generator
            );
        }
        
        step(() => gen.next());
    });
}

// 使用
run(fetchUserFlow).then(result => {
    console.log('最终结果:', result);
});

// 对比 async/await(完全等价!)
async function fetchUserAsync() {
    try {
        const user = await getUserInfo(1);
        console.log('用户:', user.name);
        
        const orders = await getOrders(user.id);
        console.log('订单数:', orders.length);
        
        return { user, orders };
    } catch (err) {
        console.error('出错:', err.message);
    }
}
// async/await 本质上就是 Generator + 自动执行器的语法糖!

14.3 异步迭代器(Async Iterator)

// Symbol.asyncIterator 和 for await...of

// 创建异步可迭代对象
const asyncIterable = {
    [Symbol.asyncIterator]() {
        let i = 0;
        return {
            async next() {
                if (i >= 3) {
                    return { value: undefined, done: true };
                }
                // 模拟异步操作
                await new Promise(resolve => setTimeout(resolve, 1000));
                return { value: i++, done: false };
            }
        };
    }
};

// 使用 for await...of
async function consume() {
    for await (const value of asyncIterable) {
        console.log(value); // 每隔1秒: 0, 1, 2
    }
}

// 异步生成器(更简洁的写法)
async function* asyncRange(start, end) {
    for (let i = start; i <= end; i++) {
        // 模拟每个值需要异步获取
        await new Promise(resolve => setTimeout(resolve, 500));
        yield i;
    }
}

async function main() {
    for await (const num of asyncRange(1, 5)) {
        console.log(num); // 每隔500ms: 1, 2, 3, 4, 5
    }
}

// 实际应用:分页获取数据
async function* fetchPages(baseUrl) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`${baseUrl}?page=${page}&limit=20`);
        const data = await response.json();
        
        yield data.items;
        
        hasMore = data.hasMore;
        page++;
    }
}

// 使用
async function getAllItems() {
    const allItems = [];
    
    for await (const items of fetchPages('/api/products')) {
        allItems.push(...items);
        console.log(`已获取 ${allItems.length} 个商品`);
    }
    
    return allItems;
}

// 实际应用:读取大文件流(Node.js)
const fs = require('fs');

async function processLargeFile(filePath) {
    const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
    
    let lineCount = 0;
    for await (const chunk of stream) {
        lineCount += chunk.split('\n').length;
    }
    
    console.log(`文件共 ${lineCount} 行`);
}

15. 并发控制

15.1 并发限制器

// 实现一个通用的并发限制器
class ConcurrencyLimiter {
    #maxConcurrency;
    #running = 0;
    #queue = [];

    constructor(maxConcurrency) {
        this.#maxConcurrency = maxConcurrency;
    }

    async run(fn) {
        // 如果达到并发上限,排队等待
        if (this.#running >= this.#maxConcurrency) {
            await new Promise(resolve => this.#queue.push(resolve));
        }

        this.#running++;

        try {
            return await fn();
        } finally {
            this.#running--;
            // 释放一个排队的任务
            if (this.#queue.length > 0) {
                const next = this.#queue.shift();
                next();
            }
        }
    }

    get running() { return this.#running; }
    get pending() { return this.#queue.length; }
}

// 使用
async function downloadFiles(urls) {
    const limiter = new ConcurrencyLimiter(3); // 最多3个并行
    
    const results = await Promise.all(
        urls.map(url => 
            limiter.run(async () => {
                console.log(`开始下载: ${url} (并行: ${limiter.running})`);
                const response = await fetch(url);
                const data = await response.json();
                console.log(`完成下载: ${url}`);
                return data;
            })
        )
    );
    
    return results;
}

15.2 Promise 池

// 更精细的并发控制:Promise 池
async function promisePool(tasks, poolSize) {
    const results = [];
    const executing = new Set();

    for (const [index, task] of tasks.entries()) {
        // 创建 Promise 并开始执行
        const promise = Promise.resolve().then(() => task()).then(result => {
            results[index] = { status: 'fulfilled', value: result };
        }).catch(error => {
            results[index] = { status: 'rejected', reason: error };
        });

        executing.add(promise);
        
        // Promise 完成后从执行集合中移除
        const clean = promise.then(() => executing.delete(promise));

        // 达到池大小限制时,等待一个完成
        if (executing.size >= poolSize) {
            await Promise.race(executing);
        }
    }

    // 等待剩余的任务完成
    await Promise.all(executing);
    return results;
}

// 使用
const tasks = Array.from({ length: 20 }, (_, i) => {
    return () => new Promise(resolve => {
        const delay = Math.random() * 2000;
        setTimeout(() => {
            console.log(`任务 ${i} 完成 (耗时 ${delay.toFixed(0)}ms)`);
            resolve(`result-${i}`);
        }, delay);
    });
});

const results = await promisePool(tasks, 5);
console.log('所有结果:', results);

15.3 带进度的批量处理

async function batchProcess(items, processor, options = {}) {
    const {
        concurrency = 5,
        onProgress = () => {},
        onItemComplete = () => {},
        onItemError = () => {},
    } = options;

    const limiter = new ConcurrencyLimiter(concurrency);
    const total = items.length;
    let completed = 0;
    let failed = 0;
    const results = [];

    const promises = items.map((item, index) =>
        limiter.run(async () => {
            try {
                const result = await processor(item, index);
                results[index] = { success: true, data: result };
                onItemComplete(item, result, index);
            } catch (error) {
                results[index] = { success: false, error };
                failed++;
                onItemError(item, error, index);
            } finally {
                completed++;
                onProgress({
                    completed,
                    failed,
                    total,
                    percent: ((completed / total) * 100).toFixed(1)
                });
            }
        })
    );

    await Promise.all(promises);
    
    return {
        results,
        summary: { total, completed, failed, success: completed - failed }
    };
}

// 使用:批量上传图片
const images = ['img1.jpg', 'img2.jpg', /* ... */ 'img100.jpg'];

const report = await batchProcess(
    images,
    async (image, index) => {
        const formData = new FormData();
        formData.append('file', image);
        const response = await fetch('/api/upload', { method: 'POST', body: formData });
        if (!response.ok) throw new Error(`上传失败: ${response.status}`);
        return response.json();
    },
    {
        concurrency: 3,
        onProgress: ({ completed, total, percent }) => {
            console.log(`进度: ${completed}/${total} (${percent}%)`);
            updateProgressBar(percent);
        },
        onItemError: (image, error) => {
            console.warn(`${image} 上传失败:`, error.message);
        }
    }
);

console.log(`上传完成: ${report.summary.success} 成功, ${report.summary.failed} 失败`);

16. 发布/订阅与事件驱动

16.1 EventEmitter 实现

class AsyncEventEmitter {
    #listeners = new Map();

    on(event, listener) {
        if (!this.#listeners.has(event)) {
            this.#listeners.set(event, []);
        }
        this.#listeners.get(event).push(listener);
        return this; // 链式调用
    }

    off(event, listener) {
        const listeners = this.#listeners.get(event);
        if (listeners) {
            const index = listeners.indexOf(listener);
            if (index > -1) listeners.splice(index, 1);
        }
        return this;
    }

    once(event, listener) {
        const wrapper = async (...args) => {
            this.off(event, wrapper);
            return listener(...args);
        };
        return this.on(event, wrapper);
    }

    // 异步 emit:等待所有监听器执行完毕
    async emit(event, ...args) {
        const listeners = this.#listeners.get(event) || [];
        const results = [];
        
        for (const listener of [...listeners]) {
            results.push(await listener(...args));
        }
        
        return results;
    }

    // 并行 emit
    async emitParallel(event, ...args) {
        const listeners = this.#listeners.get(event) || [];
        return Promise.all(listeners.map(fn => fn(...args)));
    }

    // 等待某个事件触发(转为 Promise)
    waitFor(event, timeout = 0) {
        return new Promise((resolve, reject) => {
            let timer;
            
            if (timeout > 0) {
                timer = setTimeout(() => {
                    this.off(event, handler);
                    reject(new Error(`等待 "${event}" 事件超时 (${timeout}ms)`));
                }, timeout);
            }

            const handler = (data) => {
                clearTimeout(timer);
                resolve(data);
            };

            this.once(event, handler);
        });
    }
}

// 使用
const bus = new AsyncEventEmitter();

// 注册异步监听器
bus.on('order:created', async (order) => {
    console.log('发送确认邮件...');
    await sendEmail(order.userId, '订单已创建');
});

bus.on('order:created', async (order) => {
    console.log('更新库存...');
    await updateInventory(order.items);
});

// 触发事件
await bus.emit('order:created', { id: 1, userId: 'u1', items: [...] });
console.log('所有后续处理完成');

// 等待事件
const userData = await bus.waitFor('user:login', 30000);
console.log('用户登录了:', userData);

16.2 异步队列

class AsyncQueue {
    #queue = [];
    #processing = false;
    #concurrency;
    #running = 0;

    constructor(concurrency = 1) {
        this.#concurrency = concurrency;
    }

    enqueue(task) {
        return new Promise((resolve, reject) => {
            this.#queue.push({ task, resolve, reject });
            this.#process();
        });
    }

    async #process() {
        if (this.#running >= this.#concurrency || this.#queue.length === 0) {
            return;
        }

        const { task, resolve, reject } = this.#queue.shift();
        this.#running++;

        try {
            const result = await task();
            resolve(result);
        } catch (error) {
            reject(error);
        } finally {
            this.#running--;
            this.#process(); // 处理下一个
        }
    }

    get size() { return this.#queue.length; }
    get pending() { return this.#running; }

    // 等待所有任务完成
    async drain() {
        if (this.#queue.length === 0 && this.#running === 0) return;
        
        return new Promise(resolve => {
            const check = () => {
                if (this.#queue.length === 0 && this.#running === 0) {
                    resolve();
                } else {
                    setTimeout(check, 50);
                }
            };
            check();
        });
    }
}

// 使用:任务队列
const queue = new AsyncQueue(2); // 并发度 2

// 添加任务
for (let i = 0; i < 10; i++) {
    queue.enqueue(async () => {
        console.log(`开始任务 ${i}`);
        await new Promise(r => setTimeout(r, 1000));
        console.log(`完成任务 ${i}`);
        return `result-${i}`;
    }).then(result => {
        console.log(`任务结果: ${result}`);
    });
}

// 等待所有完成
await queue.drain();
console.log('所有任务已完成');

17. 可取消的异步操作

17.1 AbortController

// AbortController 是 Web API,用于取消异步操作

// 基本用法
const controller = new AbortController();
const { signal } = controller;

// 1. 取消 fetch 请求
fetch('/api/large-data', { signal })
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => {
        if (err.name === 'AbortError') {
            console.log('请求已取消');
        } else {
            console.error('请求失败:', err);
        }
    });

// 5秒后取消
setTimeout(() => controller.abort(), 5000);

// 2. 取消多个操作
const controller2 = new AbortController();

await Promise.all([
    fetch('/api/data1', { signal: controller2.signal }),
    fetch('/api/data2', { signal: controller2.signal }),
    fetch('/api/data3', { signal: controller2.signal }),
]);
// controller2.abort() 会同时取消所有三个请求

// 3. 监听取消信号
signal.addEventListener('abort', () => {
    console.log('收到取消信号');
    console.log('取消原因:', signal.reason);
});

// 带原因的取消
controller.abort(new Error('用户取消了操作'));

17.2 自定义可取消操作

// 让任何异步操作都可以取消
function cancellable(asyncFn) {
    const controller = new AbortController();
    
    const promise = new Promise(async (resolve, reject) => {
        // 监听取消
        controller.signal.addEventListener('abort', () => {
            reject(new DOMException('Operation cancelled', 'AbortError'));
        });
        
        try {
            const result = await asyncFn(controller.signal);
            resolve(result);
        } catch (err) {
            reject(err);
        }
    });
    
    return {
        promise,
        cancel: (reason) => controller.abort(reason)
    };
}

// 使用
const { promise, cancel } = cancellable(async (signal) => {
    const response = await fetch('/api/data', { signal });
    return response.json();
});

// 2秒后取消
setTimeout(cancel, 2000);

try {
    const data = await promise;
    console.log(data);
} catch (err) {
    if (err.name === 'AbortError') {
        console.log('已取消');
    }
}

// 可取消的延迟
function delay(ms, signal) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, ms);
        
        signal?.addEventListener('abort', () => {
            clearTimeout(timer);
            reject(new DOMException('Delay cancelled', 'AbortError'));
        });
    });
}

// 可取消的重试
async function fetchWithCancelableRetry(url, { signal, maxRetries = 3 } = {}) {
    for (let i = 0; i <= maxRetries; i++) {
        signal?.throwIfAborted(); // 检查是否已取消
        
        try {
            return await fetch(url, { signal });
        } catch (err) {
            if (err.name === 'AbortError') throw err; // 取消不重试
            if (i === maxRetries) throw err;
            
            await delay(1000 * Math.pow(2, i), signal);
        }
    }
}

18. 响应式编程简介(Observable)

// 简单的 Observable 实现
class Observable {
    constructor(subscribe) {
        this._subscribe = subscribe;
    }

    subscribe(observer) {
        // 标准化 observer
        const normalizedObserver = typeof observer === 'function'
            ? { next: observer, error: () => {}, complete: () => {} }
            : { next: () => {}, error: () => {}, complete: () => {}, ...observer };

        const subscription = this._subscribe(normalizedObserver);
        
        return {
            unsubscribe: () => {
                if (subscription?.unsubscribe) subscription.unsubscribe();
            }
        };
    }

    // 操作符
    map(fn) {
        return new Observable(observer => {
            return this.subscribe({
                next: value => observer.next(fn(value)),
                error: err => observer.error(err),
                complete: () => observer.complete()
            });
        });
    }

    filter(predicate) {
        return new Observable(observer => {
            return this.subscribe({
                next: value => predicate(value) && observer.next(value),
                error: err => observer.error(err),
                complete: () => observer.complete()
            });
        });
    }

    // 从各种来源创建 Observable
    static fromEvent(element, eventName) {
        return new Observable(observer => {
            const handler = event => observer.next(event);
            element.addEventListener(eventName, handler);
            return {
                unsubscribe: () => element.removeEventListener(eventName, handler)
            };
        });
    }

    static fromPromise(promise) {
        return new Observable(observer => {
            promise
                .then(value => {
                    observer.next(value);
                    observer.complete();
                })
                .catch(err => observer.error(err));
        });
    }

    static interval(ms) {
        return new Observable(observer => {
            let i = 0;
            const id = setInterval(() => observer.next(i++), ms);
            return { unsubscribe: () => clearInterval(id) };
        });
    }
}

// 使用示例:搜索框防抖
const searchInput = document.getElementById('search');

const subscription = Observable.fromEvent(searchInput, 'input')
    .map(e => e.target.value)
    .filter(text => text.length >= 2)
    .subscribe({
        next: async (query) => {
            const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
            displayResults(results);
        }
    });

// 取消订阅
// subscription.unsubscribe();

第六部分:实战与最佳实践

19. 真实项目场景

19.1 完整的 API 客户端

class APIClient {
    #baseUrl;
    #defaultHeaders;
    #interceptors = { request: [], response: [] };
    #timeout;

    constructor(config = {}) {
        this.#baseUrl = config.baseUrl || '';
        this.#defaultHeaders = config.headers || {};
        this.#timeout = config.timeout || 30000;
    }

    // 拦截器
    addRequestInterceptor(fn) {
        this.#interceptors.request.push(fn);
        return this;
    }

    addResponseInterceptor(fn) {
        this.#interceptors.response.push(fn);
        return this;
    }

    async #request(method, endpoint, options = {}) {
        let config = {
            method,
            url: `${this.#baseUrl}${endpoint}`,
            headers: { ...this.#defaultHeaders, ...options.headers },
            body: options.body,
            params: options.params,
            timeout: options.timeout || this.#timeout,
            signal: options.signal,
        };

        // 执行请求拦截器
        for (const interceptor of this.#interceptors.request) {
            config = await interceptor(config);
        }

        // 构建 URL(处理查询参数)
        const url = new URL(config.url);
        if (config.params) {
            Object.entries(config.params).forEach(([key, value]) => {
                url.searchParams.append(key, value);
            });
        }

        // 超时控制
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), config.timeout);

        // 合并 signal
        const signal = config.signal
            ? anySignal([config.signal, controller.signal])
            : controller.signal;

        try {
            const fetchOptions = {
                method: config.method,
                headers: config.headers,
                signal,
            };

            if (config.body && method !== 'GET') {
                fetchOptions.body = JSON.stringify(config.body);
                fetchOptions.headers['Content-Type'] = 'application/json';
            }

            let response = await fetch(url.toString(), fetchOptions);

            // 执行响应拦截器
            for (const interceptor of this.#interceptors.response) {
                response = await interceptor(response);
            }

            if (!response.ok) {
                const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
                error.status = response.status;
                error.response = response;
                throw error;
            }

            const contentType = response.headers.get('content-type');
            if (contentType?.includes('application/json')) {
                return await response.json();
            }
            return await response.text();

        } finally {
            clearTimeout(timeoutId);
        }
    }

    get(endpoint, options) { return this.#request('GET', endpoint, options); }
    post(endpoint, body, options) { return this.#request('POST', endpoint, { ...options, body }); }
    put(endpoint, body, options) { return this.#request('PUT', endpoint, { ...options, body }); }
    patch(endpoint, body, options) { return this.#request('PATCH', endpoint, { ...options, body }); }
    delete(endpoint, options) { return this.#request('DELETE', endpoint, options); }
}

// 使用
const api = new APIClient({
    baseUrl: 'https://api.example.com',
    headers: {
        'Accept': 'application/json',
    },
    timeout: 10000,
});

// 添加认证拦截器
api.addRequestInterceptor(async (config) => {
    const token = await getAuthToken();
    config.headers['Authorization'] = `Bearer ${token}`;
    return config;
});

// 添加日志拦截器
api.addResponseInterceptor(async (response) => {
    console.log(`${response.url}${response.status}`);
    return response;
});

// 调用
try {
    const users = await api.get('/users', { params: { page: 1, limit: 20 } });
    const newUser = await api.post('/users', { name: 'Alice', email: 'alice@example.com' });
} catch (err) {
    if (err.status === 401) {
        // 跳转登录
    }
}

19.2 缓存与去重

// 请求去重 + 缓存
class RequestCache {
    #cache = new Map();        // 结果缓存
    #pending = new Map();      // 进行中的请求(去重)
    #ttl;

    constructor(ttl = 60000) { // 默认缓存1分钟
        this.#ttl = ttl;
    }

    async get(key, fetcher) {
        // 1. 检查缓存
        const cached = this.#cache.get(key);
        if (cached && Date.now() - cached.timestamp < this.#ttl) {
            console.log(`[Cache HIT] ${key}`);
            return cached.data;
        }

        // 2. 检查是否有相同的请求正在进行(去重)
        if (this.#pending.has(key)) {
            console.log(`[Cache DEDUP] ${key}`);
            return this.#pending.get(key);
        }

        // 3. 发起新请求
        console.log(`[Cache MISS] ${key}`);
        const promise = fetcher().then(data => {
            // 成功后缓存结果
            this.#cache.set(key, { data, timestamp: Date.now() });
            this.#pending.delete(key);
            return data;
        }).catch(err => {
            this.#pending.delete(key);
            throw err;
        });

        this.#pending.set(key, promise);
        return promise;
    }

    invalidate(key) {
        this.#cache.delete(key);
    }

    clear() {
        this.#cache.clear();
    }
}

// 使用
const cache = new RequestCache(30000); // 30秒缓存

async function getUser(id) {
    return cache.get(`user:${id}`, () => 
        fetch(`/api/users/${id}`).then(r => r.json())
    );
}

// 即使同时调用多次,也只会发一个请求
const [user1, user2, user3] = await Promise.all([
    getUser(1),  // 发起请求
    getUser(1),  // 复用同一个请求(去重)
    getUser(1),  // 复用同一个请求(去重)
]);
// 后续调用使用缓存
const user4 = await getUser(1); // Cache HIT

19.3 WebSocket 封装

class ReconnectableWebSocket {
    #url;
    #ws = null;
    #options;
    #reconnectAttempts = 0;
    #listeners = new Map();
    #messageQueue = [];
    #isConnected = false;

    constructor(url, options = {}) {
        this.#url = url;
        this.#options = {
            maxReconnectAttempts: 10,
            reconnectInterval: 1000,
            maxReconnectInterval: 30000,
            ...options
        };
        this.#connect();
    }

    #connect() {
        this.#ws = new WebSocket(this.#url);

        this.#ws.onopen = () => {
            console.log('[WS] 连接成功');
            this.#isConnected = true;
            this.#reconnectAttempts = 0;
            
            // 发送队列中的消息
            while (this.#messageQueue.length > 0) {
                const msg = this.#messageQueue.shift();
                this.#ws.send(msg);
            }
            
            this.#emit('open');
        };

        this.#ws.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data);
                this.#emit('message', data);
                
                // 支持按类型分发
                if (data.type) {
                    this.#emit(`message:${data.type}`, data.payload);
                }
            } catch {
                this.#emit('message', event.data);
            }
        };

        this.#ws.onclose = (event) => {
            this.#isConnected = false;
            console.log(`[WS] 连接关闭: ${event.code}`);
            this.#emit('close', event);
            
            if (event.code !== 1000) { // 非正常关闭
                this.#reconnect();
            }
        };

        this.#ws.onerror = (error) => {
            console.error('[WS] 错误:', error);
            this.#emit('error', error);
        };
    }

    #reconnect() {
        if (this.#reconnectAttempts >= this.#options.maxReconnectAttempts) {
            console.error('[WS] 达到最大重连次数');
            this.#emit('maxReconnectAttemptsReached');
            return;
        }

        const delay = Math.min(
            this.#options.reconnectInterval * Math.pow(2, this.#reconnectAttempts),
            this.#options.maxReconnectInterval
        );

        console.log(`[WS] ${delay}ms 后重连 (第 ${this.#reconnectAttempts + 1} 次)`);
        
        setTimeout(() => {
            this.#reconnectAttempts++;
            this.#connect();
        }, delay);
    }

    send(data) {
        const message = typeof data === 'string' ? data : JSON.stringify(data);
        
        if (this.#isConnected) {
            this.#ws.send(message);
        } else {
            this.#messageQueue.push(message); // 离线时先队列中
        }
    }

    // 发送请求并等待响应
    request(type, payload, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const requestId = Math.random().toString(36).slice(2);
            
            const timer = setTimeout(() => {
                this.off(`message:${type}:${requestId}`, handler);
                reject(new Error(`WebSocket 请求超时: ${type}`));
            }, timeout);

            const handler = (response) => {
                clearTimeout(timer);
                resolve(response);
            };

            this.once(`message:${type}:${requestId}`, handler);
            this.send({ type, payload, requestId });
        });
    }

    on(event, handler) {
        if (!this.#listeners.has(event)) {
            this.#listeners.set(event, new Set());
        }
        this.#listeners.get(event).add(handler);
        return this;
    }

    off(event, handler) {
        this.#listeners.get(event)?.delete(handler);
        return this;
    }

    once(event, handler) {
        const wrapper = (...args) => {
            this.off(event, wrapper);
            handler(...args);
        };
        return this.on(event, wrapper);
    }

    #emit(event, ...args) {
        this.#listeners.get(event)?.forEach(handler => handler(...args));
    }

    close() {
        this.#options.maxReconnectAttempts = 0;
        this.#ws?.close(1000, 'Client closed');
    }
}

// 使用
const ws = new ReconnectableWebSocket('wss://api.example.com/ws');

ws.on('open', () => console.log('已连接'));
ws.on('message:chat', (msg) => console.log('收到消息:', msg));
ws.on('message:notification', (notif) => showNotification(notif));

ws.send({ type: 'join', payload: { room: 'general' } });

// 请求-响应模式
const userList = await ws.request('getUserList', { room: 'general' });

20. 性能优化

20.1 防抖与节流

// 防抖(Debounce):等用户停止操作后再执行
function debounce(fn, delay, options = {}) {
    const { leading = false, trailing = true } = options;
    let timer = null;
    let lastArgs = null;

    function debounced(...args) {
        lastArgs = args;
        
        const callNow = leading && !timer;
        
        clearTimeout(timer);
        
        timer = setTimeout(() => {
            timer = null;
            if (trailing && lastArgs) {
                fn(...lastArgs);
                lastArgs = null;
            }
        }, delay);

        if (callNow) {
            fn(...args);
        }
    }

    debounced.cancel = () => {
        clearTimeout(timer);
        timer = null;
        lastArgs = null;
    };

    // 返回 Promise 版本
    debounced.promise = (...args) => {
        return new Promise((resolve) => {
            debounced((...result) => resolve(fn(...result)));
        });
    };

    return debounced;
}

// 异步防抖搜索
const debouncedSearch = debounce(async (query) => {
    const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
    displayResults(results);
}, 300);

searchInput.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});


// 节流(Throttle):限制执行频率
function throttle(fn, interval) {
    let lastTime = 0;
    let timer = null;

    return function(...args) {
        const now = Date.now();
        const remaining = interval - (now - lastTime);

        if (remaining <= 0) {
            clearTimeout(timer);
            timer = null;
            lastTime = now;
            fn(...args);
        } else if (!timer) {
            timer = setTimeout(() => {
                lastTime = Date.now();
                timer = null;
                fn(...args);
            }, remaining);
        }
    };
}

// 滚动事件节流
window.addEventListener('scroll', throttle(async () => {
    if (isNearBottom()) {
        await loadMoreItems();
    }
}, 200));

20.2 懒加载与预加载

// 懒加载模式
class LazyLoader {
    #loaders = new Map();
    #cache = new Map();

    register(key, loader) {
        this.#loaders.set(key, loader);
    }

    async get(key) {
        // 已缓存
        if (this.#cache.has(key)) {
            return this.#cache.get(key);
        }

        const loader = this.#loaders.get(key);
        if (!loader) throw new Error(`Unknown resource: ${key}`);

        const value = await loader();
        this.#cache.set(key, value);
        return value;
    }

    // 预加载(后台提前加载)
    preload(...keys) {
        return Promise.allSettled(
            keys.map(key => this.get(key))
        );
    }
}

// 使用
const resources = new LazyLoader();

resources.register('heavyModule', () => import('./heavy-module.js'));
resources.register('userProfile', () => fetch('/api/profile').then(r => r.json()));
resources.register('config', () => fetch('/api/config').then(r => r.json()));

// 只有在需要时才加载
const profile = await resources.get('userProfile');

// 路由跳转前预加载下一页的资源
router.beforeEach((to) => {
    if (to.name === 'dashboard') {
        resources.preload('config', 'userProfile');
    }
});


// 图片懒加载
function lazyLoadImages() {
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.classList.remove('lazy');
                observer.unobserve(img);
            }
        });
    }, { rootMargin: '200px' }); // 提前200px开始加载

    document.querySelectorAll('img.lazy').forEach(img => {
        observer.observe(img);
    });
}

20.3 Web Worker 异步计算

// main.js — 将耗时计算放到 Worker 线程
class WorkerPool {
    #workers = [];
    #queue = [];
    #maxWorkers;

    constructor(workerScript, maxWorkers = navigator.hardwareConcurrency || 4) {
        this.#maxWorkers = maxWorkers;
        
        for (let i = 0; i < maxWorkers; i++) {
            this.#workers.push({
                worker: new Worker(workerScript),
                busy: false
            });
        }
    }

    execute(data) {
        return new Promise((resolve, reject) => {
            const task = { data, resolve, reject };
            
            const freeWorker = this.#workers.find(w => !w.busy);
            if (freeWorker) {
                this.#runTask(freeWorker, task);
            } else {
                this.#queue.push(task);
            }
        });
    }

    #runTask(workerInfo, task) {
        workerInfo.busy = true;
        
        const handleMessage = (e) => {
            workerInfo.worker.removeEventListener('message', handleMessage);
            workerInfo.worker.removeEventListener('error', handleError);
            workerInfo.busy = false;
            
            task.resolve(e.data);
            
            // 处理队列中的下一个任务
            if (this.#queue.length > 0) {
                const nextTask = this.#queue.shift();
                this.#runTask(workerInfo, nextTask);
            }
        };
        
        const handleError = (e) => {
            workerInfo.worker.removeEventListener('message', handleMessage);
            workerInfo.worker.removeEventListener('error', handleError);
            workerInfo.busy = false;
            task.reject(e.error || new Error(e.message));
        };
        
        workerInfo.worker.addEventListener('message', handleMessage);
        workerInfo.worker.addEventListener('error', handleError);
        workerInfo.worker.postMessage(task.data);
    }

    terminate() {
        this.#workers.forEach(w => w.worker.terminate());
    }
}

// worker.js
// self.onmessage = function(e) {
//     const { type, payload } = e.data;
//     
//     switch (type) {
//         case 'heavyComputation':
//             const result = performHeavyWork(payload);
//             self.postMessage(result);
//             break;
//     }
// };

// 使用
const pool = new WorkerPool('worker.js', 4);

const results = await Promise.all([
    pool.execute({ type: 'heavyComputation', payload: data1 }),
    pool.execute({ type: 'heavyComputation', payload: data2 }),
    pool.execute({ type: 'heavyComputation', payload: data3 }),
]);

21. 常见陷阱与调试

21.1 常见陷阱

// 陷阱1:忘记 await
async function trap1() {
    const promise = fetchData(); // ❌ 忘记 await
    console.log(promise);        // Promise {<pending>},不是数据!
    
    const data = await fetchData(); // ✅
    console.log(data);              // 实际数据
}

// 陷阱2:forEach 中使用 async/await
async function trap2() {
    const ids = [1, 2, 3];
    
    // ❌ forEach 不会等待 async 回调
    ids.forEach(async (id) => {
        const data = await fetchData(id);
        console.log(data);
    });
    console.log("完成"); // 在所有 fetchData 之前执行!
    
    // ✅ 使用 for...of
    for (const id of ids) {
        const data = await fetchData(id);
        console.log(data);
    }
    console.log("完成"); // 在所有 fetchData 之后执行
    
    // ✅ 或 Promise.all + map(并行)
    await Promise.all(ids.map(async (id) => {
        const data = await fetchData(id);
        console.log(data);
    }));
    console.log("完成");
}

// 陷阱3:async 函数中的返回值
async function trap3() {
    // ❌ 在 try/catch 的 catch 中 return 但忘记前面的逻辑可能已执行
    try {
        const data = await riskyOperation();
        updateUI(data);
        return data;
    } catch (err) {
        return null; // 但 updateUI 可能已经部分执行了!
    }
}

// 陷阱4:Promise 构造函数中的异步操作
// ❌ 在 Promise 构造函数中使用 async
const badPromise = new Promise(async (resolve, reject) => {
    try {
        const data = await fetchData();
        resolve(data);
    } catch (err) {
        // 如果这里抛出错误,不会被外部 catch 到!
        reject(err);
    }
});

// ✅ 直接使用 async 函数
async function goodApproach() {
    return await fetchData();
}

// 陷阱5:竞态条件
let currentData = null;

async function trap5(query) {
    // ❌ 快速调用可能导致旧请求覆盖新请求
    const data = await search(query);
    currentData = data; // 如果之前的请求比后面的慢,会覆盖新数据
}

// ✅ 使用请求 ID 或 AbortController
let requestCounter = 0;
async function safeSearch(query) {
    const myRequestId = ++requestCounter;
    const data = await search(query);
    
    if (myRequestId === requestCounter) {
        currentData = data; // 只使用最新请求的结果
    }
}

// 陷阱6:内存泄漏
class trap6Component {
    constructor() {
        this.controller = new AbortController();
    }
    
    async loadData() {
        try {
            const data = await fetch('/api/data', {
                signal: this.controller.signal
            });
            this.render(data);
        } catch (err) {
            if (err.name !== 'AbortError') throw err;
        }
    }
    
    // ✅ 组件销毁时取消未完成的请求
    destroy() {
        this.controller.abort();
    }
}

// 陷阱7:错误吞噬
async function trap7() {
    // ❌ catch 后不重新抛出,调用者不知道出错了
    try {
        await riskyOperation();
    } catch (err) {
        console.error(err); // 只是打印,没有抛出
    }
    // 调用者以为一切正常...
    
    // ✅ 要么重新抛出,要么返回明确的错误标志
    try {
        await riskyOperation();
    } catch (err) {
        console.error(err);
        throw err; // 重新抛出让调用者知道
    }
}

21.2 调试技巧

// 1. 使用 async stack traces
// Chrome DevTools → Settings → Enable async stack traces

// 2. 给 Promise 打标签
function labeledFetch(label, url) {
    const promise = fetch(url).then(r => r.json());
    promise.label = label; // 调试用
    return promise;
}

// 3. 日志包装器
function traced(fn, name) {
    return async function(...args) {
        const id = Math.random().toString(36).slice(2, 8);
        console.log(`[${name}:${id}] 开始`, args);
        const start = performance.now();
        
        try {
            const result = await fn.apply(this, args);
            const duration = (performance.now() - start).toFixed(2);
            console.log(`[${name}:${id}] 完成 (${duration}ms)`, result);
            return result;
        } catch (err) {
            const duration = (performance.now() - start).toFixed(2);
            console.error(`[${name}:${id}] 失败 (${duration}ms)`, err);
            throw err;
        }
    };
}

const tracedFetch = traced(
    (url) => fetch(url).then(r => r.json()),
    'API'
);

await tracedFetch('/api/users');
// [API:k3m2n1] 开始 ["/api/users"]
// [API:k3m2n1] 完成 (234.56ms) [{...}, {...}]

// 4. Promise 状态检查
async function inspectPromise(promise) {
    const unique = Symbol();
    const result = await Promise.race([promise, Promise.resolve(unique)]);
    
    if (result === unique) {
        return 'pending';
    }
    return 'fulfilled';
}

// 5. 性能监测
class PerformanceTracker {
    #marks = new Map();
    
    start(label) {
        this.#marks.set(label, performance.now());
    }
    
    end(label) {
        const start = this.#marks.get(label);
        if (!start) throw new Error(`No start mark for: ${label}`);
        
        const duration = performance.now() - start;
        this.#marks.delete(label);
        
        console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
        return duration;
    }
    
    async measure(label, fn) {
        this.start(label);
        try {
            return await fn();
        } finally {
            this.end(label);
        }
    }
}

const perf = new PerformanceTracker();

await perf.measure('加载用户数据', async () => {
    return fetch('/api/users').then(r => r.json());
});
// ⏱️ 加载用户数据: 156.78ms

22. 完整知识图谱总结

JavaScript 异步编程知识图谱
│
├── 基础概念
│   ├── 单线程模型
│   ├── 事件循环 ★★★
│   │   ├── 调用栈 (Call Stack)
│   │   ├── 宏任务队列 (setTimeout, setInterval, I/O)
│   │   ├── 微任务队列 (Promise.then, queueMicrotask, MutationObserver)
│   │   └── 执行顺序:同步 → 微任务(全部) → 宏任务(一个) → 微任务(全部) → ...
│   └── Node.js 事件循环(6个阶段)
│
├── 回调函数
│   ├── 错误优先回调
│   ├── 回调地狱
│   └── 控制反转问题
│
├── Promise ★★★
│   ├── 三种状态:pending → fulfilled / rejected
│   ├── 链式调用(.then 返回新 Promise)
│   ├── 错误处理(.catch 错误传播)
│   ├── 静态方法
│   │   ├── Promise.all (全部成功)
│   │   ├── Promise.allSettled (全部完成)
│   │   ├── Promise.race (最快)
│   │   ├── Promise.any (第一个成功)
│   │   └── Promise.withResolvers (ES2024)
│   └── 手写 Promise(面试)
│
├── Async/Await ★★★
│   ├── async 函数返回 Promise
│   ├── await 暂停执行等待 Promise
│   ├── 错误处理(try/catch)
│   ├── 串行 vs 并行
│   └── 循环中的 await
│
├── 高级模式
│   ├── Generator + 自动执行器
│   ├── 异步迭代器 (for await...of)
│   ├── 并发控制(限流器、Promise池)
│   ├── AbortController(取消操作)
│   ├── 发布/订阅模式
│   └── Observable(响应式编程)
│
└── 实战技巧
    ├── 重试机制(指数退避)
    ├── 请求去重与缓存
    ├── 防抖与节流
    ├── 竞态条件处理
    ├── Web Worker
    └── 常见陷阱
        ├── forEach 中的 async
        ├── 忘记 await
        ├── 未处理的 rejection
        └── 内存泄漏

学习路线建议:

  1. 入门:理解同步/异步 → 事件循环 → 回调
  2. 基础:Promise 创建/消费 → 链式调用 → 错误处理
  3. 进阶:async/await → 并行/串行 → 静态方法
  4. 高级:并发控制 → 取消操作 → Generator/异步迭代
  5. 精通:手写 Promise → 架构设计 → 性能优化 → 响应式编程

CSS Grid 案例

CSS Grid 案例详解

1、min-content、max-content、auto 空间计算

单列 auto 布局:内容宽度决定列宽,剩余空间自动分配

auto-1.png

双列 auto 布局:两列宽度根据内容自动调整

auto-2.png

三列 auto 布局:多列布局时内容宽度的分配方式

auto-3.png

四列 auto 布局:列数增加时的空间分配逻辑

auto-4.png

max-content:列宽等于内容最大宽度,不换行

max-content.png

min-content:列宽等于内容最小宽度,尽可能换行

min-content.png

min-content 最小宽度,max-content 完整内容宽度, auto 自动分配剩余空间 列宽等于内容最小宽度,尽可能换行

<body>
    <div class="grid">
        <div class="item">内容1</div>
        <div class="item">内容2</div>
        <div class="item">内容3</div>
        <div class="item">内容4</div>
    </div>
</body>
<style>
    .grid {
        display: grid;
        /*  左(自动分配剩余)
            右(内容最大或最小) */
        grid-template-columns: auto max-content;
        
        /* 单列,两列,三列,四列 */
        grid-template-columns: auto;
        grid-template-columns: auto auto;
        grid-template-columns: auto auto auto;
        grid-template-columns: auto auto auto auto;
        grid-template-columns: auto min-content;

        /* 加间距,方便看列的边界 */
        grid-column-gap: 5px;
        grid-row-gap: 5px;
        background-color: #c1c1c1;
        padding: 5px;
    }

    .item {
        /* 加背景,直观区分列 */
        border: 1px solid #000;
        padding: 2px;
    }
</style>

<!-- 
★ auto 特性:优先适应父容器宽度,剩余空间自动分配,内容超出时会换行
★ max-content:列宽=内容不换行时的最大宽度(内容不会折行)
★ min-content:列宽=内容能折行时的最小宽度(内容尽可能折行收缩)
★ 多列 auto:列数=auto的个数,列宽优先适配自身内容,剩余空间平分 
-->

2、space-between

2、stretch 内容填充.png

两个内容自动撑开, 左右排列
<div class="container">
    <div class="item">用户123</div>
    <div class="item">很多内容</div>

    <div class="item">内容</div>
    <div class="item">很多内容很多内容</div>
</div>

<style>
    .container {
        width: 300px;
        display: grid;
        grid-template-columns: auto auto;
        /** 两个内容自动撑开, 左右排列 **/
        justify-content: space-between;
        background-color: blueviolet;
    }

    .item {
        background-color: skyblue;
    }
</style>

3、auto-fit、minmax 填满剩余空间

单列布局,当容器宽度不足时自动调整为单列

3、一列.png

两列布局:容器宽度足够时自动扩展为多列

3、两列.png

三列布局:自适应容器宽度的多列排列

3、三列.png

四列布局:充分利用可用空间的自适应布局

3、铺满四列.png

<main>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
</main>

<style>
    main {
        display: grid;
        grid-gap: 5px;
        /* 1、minmax 定义尺寸范围 */

        /* 2、
              auto-fill 在宽度足够的条件下预留了空白
              auto-fit 在宽度足够的条件下充分利用空间
              */
        grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
        border: 3px dashed;
    }

    div {
        background-color: deepskyblue;
        height: 100px;
    }
</style>

4、grid 命名格子

使用grid-template-areas,通过命名区域实现复杂的网格布局

4、grid 命名格子.png

代码

<div class="container">
    <div class="item putao">葡萄</div>
    <div class="item longxia">龙虾</div>
    <div class="item yangyu">养鱼</div>
    <div class="item xigua">西瓜</div>
</div>

<style>
    .container {
        height: 400px;
        display: grid;
        /* grid-template-columns: repeat(3, 1fr);
        grid-template-rows: repeat(4, 1fr);
        grid-template-areas:
            "葡萄 葡萄 葡萄"
            "龙虾 养鱼 养鱼"
            "龙虾 养鱼 养鱼"
            "西瓜 西瓜 西瓜"
        ; */
        /* 简写 */
        grid: "葡萄 葡萄 葡萄" 1fr "龙虾 养鱼 养鱼" 1fr "龙虾 养鱼 养鱼" 1fr "西瓜 西瓜 西瓜" 1fr / 1fr 1fr 1fr;
    }

    .putao {
        grid-area: 葡萄;
        background-color: greenyellow;
    }

    .longxia {
        grid-area: 龙虾;
        background-color: plum;
    }

    .yangyu {
        grid-area: 养鱼;
        background-color: slategray;
    }

    .xigua {
        grid-area: 西瓜;
        background-color: crimson;
    }

    .container .item {
        display: flex;
        align-items: center;
        justify-content: center;
    }
</style>

5、隐形网格

image.png 隐式网格: 超出显式网格范围的项目自动创建新行

代码

<div class="container">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5隐式网格</div>
</div>
<style>
    .container {
        display: grid;
        grid: 1fr 1fr/1fr 1fr;
        grid-auto-rows: 100px;
    }

    .item:nth-of-type(1) {
        background-color: rgb(239, 255, 170);
    }

    .item:nth-of-type(2) {
        background-color: rgb(182, 184, 255);
    }

    .item:nth-of-type(3) {
        background-color: rgb(255, 195, 253);
    }

    .item:nth-of-type(4) {
        background-color: rgb(210, 255, 179);
    }

    .item:nth-of-type(5) {
        background-color: rgb(185, 185, 185);
    }
</style>

6、隐式网格2

隐式网格的列布局: grid-auto-columns设置隐式列的宽度

image.png

<div class="container2">
    <div class="item-a">a</div>
    <div class="item-b">b</div>
</div>
<style>
    .container2 {
        display: grid;
        grid-template: 1fr 1fr/1fr 1fr;
        /* 额外列(第 3 列 +)宽度固定 60px; */
        grid-auto-columns: 60px;
        border: 1px solid #000;
    }

    .item-b {
        /* 让 item-b 占第 3 列(第 3-4 根列线之间) */
        grid-column: 3/4;
        background-color: aqua;
    }
</style>

7、grid-auto-flow 流向

默认row 流向: 左到右、从上到下排列

7、grid-auot-flow 流向.png

column流向: 上到下、从左到右排列

7、grid-auto-flow 流向2.png

<div class="container">
    <div class="item">格子1</div>
    <div class="item">格子2</div>
    <div class="item">格子3</div>
    <div class="item">格子4</div>
    <div class="item">格子5</div>
    <div class="item">格子6</div>
    <div class="item">格子7</div>
    <div class="item">格子8</div>
    <div class="item">格子9</div>
</div>
<style>
    .container{
        display: grid;
        line-height: 40px;
        background-color: skyblue;
        
        /* 默认流向是左往右,再下一行左往右,重复前面方式 */
        grid-template-columns: 1fr 1fr;
        grid-auto-flow: row;

        /* 换一个方向,上往下 */
        /* grid-template-rows: 1fr 1fr;
        grid-auto-flow: column; */
    }
    .item{
        outline: 1px dotted;
    }
</style>

8、grid-auto-flow 图片案例

第一个图片占据两行空间,其他图片自动排列

image.png
<div class="container">
    <div class="item"><img src="./图片/shu.webp"></div>
    <div class="item"><img src="./图片/1.webp"></div>
    <div class="item"><img src="./图片/2.webp"></div>
    <div class="item"><img src="./图片/3.webp"></div>
    <div class="item"><img src="./图片/4.webp"></div>
 
</div>
<style>
 .container {
    display: grid;
    background-color: skyblue;
 
    /* 1. 修复:去掉 grid-auto-flow: column,用默认 row 按行排列 */
    /* 2. 修复:用 minmax(0, 1fr) 防止单元格被图片撑大 */

    grid-template: 
        "a . ." minmax(0, 1fr)
        "a . ." minmax(0, 1fr)
        / minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
    grid-gap: 6px;
    
}

/* 第一个元素占网格区域 a(2行1列) */
.container .item:first-child {
    grid-area: a;
}

/* 修复:图片不溢出、不变形 */
.container img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover; /* 按比例裁剪,填满单元格 */
}
</style>

9、dense 填补

dense填补: 自动填补空缺位置,优化空间利用 image.png

<div class="container">
    <div class="item">各自1</div>
    <div class="item">各自2</div>
    <div class="item">各自3</div>
    <div class="item">各自4</div>
    <div class="item">各自5</div>
    <div class="item">各自6</div>
    <div class="item">各自7</div>
    <div class="item">各自8</div>
    <div class="item">各自9</div>
</div>

<style>
    .container {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        grid-auto-flow: dense;
    }

    .container .item {
        outline: 1px dotted;
    }

    /* 模拟空缺位置 */
    .container .item:first-child {
        grid-column-start: 2;
    }
</style>

10、grid 简写

<style>

    /*
       1、 none 表示设置所有子项属性值为初始化值
        grid:none

       2、template 
        单独:
        grid-template-rows:100px 300px;
        grid-template-columns:3fr 1fr;

        简写:
        grid:100px 300px / 3fr 1fr;
        
        4、auto-flow 
        单独 
        grid-template-rows: 100px 300px;
        grid-auto-flow: column;
        grid-auto-columns: 200px; 

         合并
        grid: 100px 300px / auto-flow 200px;

      3、template-areas
      完整:
       grid-template-columns: repeat(3, 1fr);
       grid-template-rows: repeat(4, 1fr);
       grid-template-areas:
            "葡萄 葡萄 葡萄"
            "龙虾 养鱼 养鱼"
            "龙虾 养鱼 养鱼"
            "西瓜 西瓜 西瓜"; 

        简写:
        grid: 
         "葡萄 葡萄 葡萄" 1fr
         "龙虾 养鱼 养鱼" 1fr 
         "龙虾 养鱼 养鱼" 1fr 
         "西瓜 西瓜 西瓜" 1fr 
         / 1fr 1fr 1fr;
    */
</style>

11、place-items 完整写法

image.png

place-items:项目在单元格内的居中对齐方式

<div class="container">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
</div>

<style>
    .container {
        display: grid;
        grid: 1fr 1fr/1fr 1fr;
        height: 300px;
        background-color: skyblue;

        /* 单个,类似flex  
        
        justify-content: center;
        align-items: center;
        
        */

        /* 合并 */
        place-items: center right;
    }
    .item{
        outline: 1px solid;
    }
 
</style>

12、layout栏布局

经典的页面布局: 包含头部、侧边栏、主内容和底部四个区域 image.png

<style>
    .grid-container {
      width: 500px;
      height: 400px;
      margin: 20px auto;
      display: grid;
      /* 精简核心:去掉重复线名,换行简化,保留关键命名 */
      grid-template:
        [r1] "header header header" 1fr [r2]
        [r2] "sidebar main main"    2fr [r3]
        [r3] "sidebar footer footer" 1fr [r4]
        / [c1] 150px [c2] 1fr [c3] 1fr [c4];   
      gap: 8px;
    }

    /* 子项定位(极简写法) */
    .grid-container>div{display: flex;justify-content: center;align-items: center;}
    .header { grid-area: header; background: #409eff; color: #fff;  }
    .sidebar { grid-row: r2/r4; grid-column: c1/c2; background: #67c23a; color: #fff;  }
    .main { grid-area: main; background: #e6a23c; color: #fff;  }
    .footer { grid-row: r3/r4; grid-column: c2/c4; background: #f56c6c; color: #fff;  }
  </style>

<body>
  <div class="grid-container">
    <div class="header">头部</div>
    <div class="sidebar">侧边栏</div>
    <div class="main">主内容</div>
    <div class="footer">底部</div>
  </div>
</body>
<!-- Grid 布局示例,含命名网格线 + 区域命名,3 行 3 列分头部 / 侧边 / 主内容 / 底部 -->

13、grid-area 线条版

image.png grid重叠: 图片和标题在同一网格单元格内叠加

<figure>
    <img src="./图片/13.png">
    <figcaption>自然风景</figcaption>
</figure>

<style>
    figure {

        display: grid;
    }

    img {
        width: 100%;
    }

    figure>img,
    figure>figcaption {
        grid-area: 1 / 1 / 2 / 2;
    }

    figure>figcaption {
        align-self: end;
        text-align: center;
        background: #0009;
        color: #fff;
        line-height: 2;
    }
</style>

附录

参考资源

  • 《CSS新世界》- 张鑫旭

CSS 伪元素选择器:为元素披上魔法的斗篷

CSS 伪元素选择器:为元素披上魔法的斗篷

在 CSS 的魔法世界中,有一项特别的能力——伪元素选择器。它就像哈利·波特的隐形斗篷,让你不必修改 HTML 结构,就能凭空创造出新的视觉元素。今天,就让我们一起揭开这项魔法技艺的神秘面纱。

🎭 什么是伪元素选择器?

伪元素(Pseudo-element)是 CSS 提供的一种特殊选择器,允许你选择元素的特定部分,或者在元素内容周围插入虚拟的 DOM 节点。它们不是真正的 HTML 元素,而是通过 CSS 渲染出来的“影子元素”。

最核心的魔法咒语有两个:

  • ::before - 在元素内容之前创建伪元素
  • ::after - 在元素内容之后创建伪元素

📝 语法小贴士:现代 CSS 推荐使用双冒号 ::(如 ::before),以区别于伪类的单冒号 :。但单冒号 :before也仍然有效。

🔮 基础咒语:content 属性

要施展伪元素魔法,必须先念出核心咒语——**content** 属性。没有它,伪元素就不会显形。

css
css
复制
.魔法帽子::before {
  content: "🎩";  /* 必须的咒语! */
  margin-right: 8px;
}

content可以接受多种“魔法材料”:

  • 字符串文本content: "→ ";(添加箭头)
  • 空字符串content: "";(纯装饰元素)
  • 属性值content: attr(data-tip);(读取 HTML 属性)
  • 计数器content: counter(chapter);(自动编号)
  • 图片content: url(icon.png);

✨ 实战魔法秀

魔法一:优雅的装饰线条

代码示例:

css
css
复制
.card .header::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 80rpx;
  border-bottom: 4rpx solid #000;
}

这是标题装饰线的经典用法。想象一下,你的标题下方自动长出了一条精致的小横线,就像绅士西装上的口袋巾,既优雅又不过分张扬。

魔法二:自动化的引用标记

css
css
复制
blockquote::before {
  content: "“";  /* 开引号 */
  font-size: 3em;
  color: #e74c3c;
  vertical-align: -0.4em;
  margin-right: 10px;
}

blockquote::after {
  content: "”";  /* 闭引号 */
  font-size: 3em;
  color: #e74c3c;
  vertical-align: -0.4em;
  margin-left: 10px;
}

现在你的 <blockquote>元素会自动戴上红色的巨大引号,仿佛是文学作品中的点睛之笔。

魔法三:视觉引导箭头

css
css
复制
.dropdown::after {
  content: "▾";  /* 向下箭头 */
  display: inline-block;
  margin-left: 8px;
  transition: transform 0.3s;
}

.dropdown.open::after {
  transform: rotate(180deg);  /* 点击时箭头翻转 */
}

导航菜单的交互指示器就此诞生!用户点击时,箭头会优雅地旋转,指示状态变化。

魔法四:清浮动(经典技巧)

css
css
复制
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}

这个古老的魔法曾拯救了无数布局。它在浮动元素后面插入一个看不见的“清扫工”,确保父元素能正确包裹子元素。

🎨 伪元素的艺术:超越 ::before 和 ::after

除了最常用的两个,伪元素家族还有其他成员:

::first-letter- 首字母魔法

css
css
复制
article p::first-letter {
  font-size: 2.5em;
  float: left;
  line-height: 1;
  margin-right: 8px;
  color: #2c3e50;
  font-weight: bold;
}

让段落首字母变得像中世纪手抄本一样华丽,瞬间提升文章的视觉档次。

::first-line- 首行高亮

css
css
复制
.poem::first-line {
  font-variant: small-caps;  /* 小型大写字母 */
  letter-spacing: 1px;
  color: #8e44ad;
}

诗歌的首行会以特殊样式呈现,就像歌剧中主角的第一次亮相。

::selection- 选择区域染色

css
css
复制
::selection {
  background-color: #3498db;
  color: white;
  text-shadow: none;
}

用户选中文本时,背景会变成优雅的蓝色,而不是默认的灰蓝色。

::placeholder- 输入框占位符美化

css
css
复制
input::placeholder {
  color: #95a5a6;
  font-style: italic;
  opacity: 0.8;
}

让表单的提示文字更加柔和友好。

⚡ 伪元素的超能力

能力一:Z 轴分层

伪元素拥有独立的堆叠上下文,可以创造出精美的多层效果:

css
css
复制
.button {
  position: relative;
  background: #3498db;
  color: white;
  padding: 12px 24px;
  border: none;
}

.button::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.3) 100%);
  border-radius: inherit;
  z-index: 1;
}

这个按钮表面有一层半透明的渐变光泽,就像刚打过蜡的汽车漆面。

能力二:动画与过渡

伪元素完全可以动起来!

css
css
复制
.loading::after {
  content: "";
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid #ddd;
  border-top-color: #3498db;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

一个简约而不简单的加载动画,无需任何额外的 HTML 标签。

能力三:复杂的图形绘制

利用边框技巧,伪元素可以绘制各种形状:

css
css
复制
.tooltip::before {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  
  /* 绘制三角形 */
  border: 6px solid transparent;
  border-top-color: #333;
}

这是工具提示框的小箭头,纯 CSS 实现,无需图片。

🚫 伪元素的禁忌与限制

魔法虽强,也有规则:

  1. content是必需的:没有它,伪元素不显现
  2. 某些属性不可用:伪元素不能应用 content属性本身
  3. 不能用于替换元素:如 <img><input><textarea>
  4. SEO 不可见:搜索引擎看不到伪元素的内容
  5. 可访问性注意:屏幕阅读器可能不会读取伪元素内容

💡 最佳实践指南

何时使用伪元素?

适合

  • 纯粹的视觉装饰(图标、线条、形状)
  • 不需要交互的 UI 元素
  • 内容前后的固定标记
  • 不影响语义的样式增强

不适合

  • 重要的交互内容(应使用真实元素)
  • 需要被搜索引擎收录的内容
  • 复杂的、需要维护的动态内容

性能小贴士

css
css
复制
/* 良好实践:减少重绘 */
.decorative::after {
  content: "";
  will-change: transform; /* 提示浏览器优化 */
  transform: translateZ(0); /* 触发 GPU 加速 */
}

/* 避免过度使用 */
/* 每个元素都加伪元素会影响性能 */

🌈 结语:魔法的艺术

伪元素选择器是 CSS 工具箱中的瑞士军刀——小巧、锋利、用途广泛。它们代表了关注点分离的优雅理念:HTML 负责结构,CSS 负责表现。

就像画家不会在画布上固定装饰品,而是在画作上直接绘制光影效果一样,优秀的开发者懂得使用伪元素来增强界面,而不是堆砌冗余的 HTML 标签。

记住:最好的魔法往往是看不见的魔法。当用户觉得“这个界面就是应该长这样”,而不是“这里加了个小图标”时,你就掌握了伪元素的真正精髓。

现在,拿起你的 CSS 魔杖,去创造一些神奇的界面吧!记住这句魔咒: “内容在前,装饰在后,语义清晰,表现灵活” ——这就是伪元素哲学的核心。 ✨

WebMCP 实战指南:让你的网站瞬间变成 AI 的“大脑外挂”

一、 AI 终于不用“瞎猜”你的网页了

我们可以把 WebMCP 想象成一种**“翻译官协议”**:

  • 以前的 AI(视觉模拟派) :就像一个老外在看一份全中文的报纸,他得先拍照,再识别文字,最后猜哪里是按钮。一旦你把按钮从左边挪到右边,他就找不到了。
  • WebMCP(接口直连派) :你的网站现在给 AI 提供了一个**“操作说明书”**。AI 进门后不用看页面长什么样,直接问:“那个‘查询余额’的功能在哪?” 你的网站直接通过 WebMCP 告诉它:“在这里,发个 JSON 给我,我就告诉你结果。”

一句话总结:WebMCP 让网页从“给人看的界面”变成了“给 AI 调用的函数”。


二、 核心能力:WebMCP 的“两把斧”

在实际开发中,WebMCP 提供了两种接入方式:

  1. 宣告式(适合简单动作) :在 HTML 里加个属性,就像给按钮贴个“AI 可读”的标签。
  2. 命令式(适合高级逻辑) :用 JavaScript 编写具体的执行函数,适合处理复杂计算。

三、 实战:WebMCP 的具体使用方法

目前,你可以在 Chrome Canary (v145+) 中通过以下步骤实现一个“AI 自动分析监控日志”的功能。

1. 开启实验室开关

在浏览器地址栏输入:chrome://flags/#enable-webmcp,将其设置为 Enabled 并重启。

2. 定义“说明书” (mcp-config.json)

在你的网站根目录放置一个配置文件,告诉 AI 你有哪些能力。

JSON

{
  "tools": [
    {
      "name": "get_frontend_error_logs",
      "description": "获取当前页面的前端错误日志详情",
      "parameters": {
        "type": "object",
        "properties": {
          "limit": { "type": "number", "description": "返回的日志数量" }
        }
      }
    }
  ]
}

3. JavaScript 具体实现逻辑

在你的网页脚本中,注册这个工具的具体执行逻辑。

JavaScript

// 检查浏览器是否支持 WebMCP
if ('modelContext' in navigator) {
  // 注册工具
  navigator.modelContext.registerTool('get_frontend_error_logs', async (args) => {
    // 1. 从你的监控系统中提取数据
    const logs = window.__MY_MONITOR_LOGS__.slice(0, args.limit || 5);
    
    // 2. 返回给 AI
    return {
      content: [
        { 
          type: "text", 
          text: JSON.stringify(logs, null, 2) 
        }
      ]
    };
  });
  
  console.log("WebMCP 工具已就绪,AI 现在可以直接读取你的日志了!");
}

四、 极致的 Token 优化:从“读图”到“读字典”

web系统经常有成千上万行的表格数据,让 AI 截图识别简直是灾难。

1. 结构化数据的降维打击

  • 视觉识别:1 张 1080P 的截图可能消耗 1000+ Tokens,且 AI 经常看错行。
  • WebMCP:你返回一个 JSON.stringify(summary) 仅需几十个 Tokens。
  • 工程技巧:在 registerTool 的返回结果中,你可以预先对数据进行特征提取(比如只返回异常波动点),将原本需要 AI 自己总结的过程,在本地通过高性能 JS 预处理完成,进一步压榨 Token 成本。

五、 交互新范式:从“被动响应”到“双向协同”

在你的实战代码中,展示了 AI 如何“主动”调数据,但 WebMCP 的真正威力在于它构建了一个双向总线

1. 订阅模式:让 AI 实时盯盘

在金融监控或 K 线分析场景,AI 不应该总在问“现在价格是多少”,而应该是当价格波动超过阈值时,网站主动推送到 AI 上下文。

  • 扩展用法:利用 WebMCP 的资源订阅(Resources Subscription)机制。
  • 代码逻辑:通过 navigator.modelContext.updateResource(),当监控系统发现异常流量或金融数据触发对冲点时,自动将上下文注入 AI 的实时缓存,实现“无感预警”。

六、 安全深度防御:权限隔离

你一定关心:万一 AI 乱发指令怎么办?WebMCP 引入了显式授权与沙箱隔离

1. 权限最小化原则

WebMCP 不会默认给 AI 权限去读你的整个 LocalStorage 或 Cookie。

  • 层级控制:每一个 registerTool 注册的功能,都会在 Chrome 侧边栏显示详细的权限说明。
  • 人类确认 (HITL) :对于涉及敏感操作(如:执行转账、删除线上日志)的 Tool,WebMCP 支持声明 userApproval: "required"。当 AI 尝试调用时,浏览器会跳出原生确认框,这种系统级的阻断是任何第三方插件无法模拟的安全保障。

七、 架构解耦:一套标准,适配所有 AI 终端

你目前在研究 AI Prompt ManagerTrae/Cursor。WebMCP 的出现解决了前端工程中的“适配地狱”。

1. “一次开发,到处运行”的 AI 能力

  • 旧模式:你得写一个 Chrome 插件给 Gemini 用,再写一个 MCP Server 给 Claude Desktop 用,再给 Cursor 写特定的插件。
  • WebMCP 模式:你只需要在网页里 registerTool。由于 Chrome 是宿主,只要你在 Chrome 里打开这个网页,无论是侧边栏的 Gemini,还是通过 DevTools 接入的 AI Agent,都能识别并使用这套能力。这极大降低了你维护 AI 基础设施 的成本。

八、为什么非用它不可?

  1. 性能屠宰场:不再需要给 AI 发送几万个 DOM 节点的 HTML 文本,只传核心 JSON,Token 消耗节省 90%。
  2. 安全围栏:数据处理在本地浏览器完成。大模型只发指令,不直接接触你的敏感数据库明细。
  3. 开发效率:你不再需要为不同的 AI 插件写不同的适配层,只要符合 WebMCP 标准,所有支持该协议的 AI 助手都能秒懂你的业务。

源码回溯的艺术:SourceMap 底层 VLQ 编码与离线解析架构实战

对于正在自研监控系统的架构师来说,SourceMap 绝不仅是一个调试工具,它是线上治理的“黑匣子”。

如果你的监控系统只能报出 at a.js:1:1234 这种“天书”,那它和盲人摸象没有区别。要实现“一眼定位代码行”,不仅需要理解其底层的编码协议,更要构建一套工业级的自动化闭环体系,在确保源码安全的同时,抗住海量错误冲击下的解析压力。


一、 协议拆解:SourceMap 为什么要搞得这么复杂?

混淆压缩(Minification)的目的是为了极致的传输性能,而 SourceMap 的目的是为了极致的调试体验。

1. 为什么不能直接记录映射表?

假设你的源码有 10,000 行,如果简单地用 JSON 记录每一行每一列的对应关系,这个 .map 文件可能会达到几十 MB。为了解决体积问题,SourceMap 引入了三个层级的压缩逻辑:

  • 层级一:分组压缩。它将 mappings 字段按行(用分号 ; 分隔)和位置点(用逗号 , 分隔)进行切分。
  • 层级二:相对偏移。不记录绝对坐标 [100, 200],而是记录相对于前一个点的增量 [+5, +10]
  • 层级三:VLQ 编码。将这些增量数字转换成极短的字符序列。

2. 揭秘 VLQ (Variable-Length Quantity) 编码

VLQ 是一种针对整数的变长编码方案。它的核心思想是:用 6 位(一个 Base64 字符)作为基本单元,其中 1 位表示是否有后续单元,1 位表示正负号,剩下 4 位存数值。

  • 极致紧凑:对于小的数字(如偏移量通常很小),它只需要 1 个字符就能表示。这让数万个映射点压缩到几百 KB 成为可能。

二、 工业级离线解析架构:安全性与性能的博弈

作为架构师,你必须坚守一条底线:SourceMap 永远不能出现在生产环境的 CDN 上。一旦泄露,混淆后的代码将毫无秘密可言。

1. CI/CD 流程中的“双轨制”

在自动化构建流程中,我们需要建立一套同步机制:

  • 外轨(公开) :生成的 .js 文件正常发布,但通过配置(如 Webpack 的 hidden-source-map)移除文件末尾的 //# sourceMappingURL= 声明,确保浏览器不会尝试加载它。
  • 内轨(私有) :生成的 .map 文件通过 API 自动上传到监控系统的私有存储服务器(如 MinIO 或 S3)
  • 关联键(Release ID) :每个构建版本必须生成一个唯一的版本号(可以是 Git Commit Hash),并同时注入到前端 SDK 和存储文件名中,确保解析时能“对号入座”。

2. 后端解析引擎:性能瓶颈的突破

如果监控系统并发量极高,解析过程会成为 CPU 黑洞。

  • V8 的局限:传统的 source-map JS 库在反解析时极其耗时,且内存占用极高。

  • Native 级加速:推荐引入由 Rust 编写的解析库(通过 N-API 接入 Node.js)。例如 oxc-sourcemap@jridgewell/trace-mapping。这些库利用二进制层面的位运算,解析速度比传统库快一个数量级。

  • 多级缓存方案

    • L1(内存) :缓存最近解析过的 SourceMap 对象的实例。
    • L2(磁盘缓存) :缓存反解析后的堆栈片段。
    • L3(存储) :原始 .map 文件。

三、 实战避坑:那些年老兵踩过的“暗雷”

  1. 列偏移量的一致性

    有些压缩工具(如早期的 UglifyJS)生成的列号是从 0 开始的,而有些(如某些浏览器报错)是从 1 开始的。在反解析时,必须严格校准这个 0/1 的差异,否则还原出来的代码会错位一个字符。

  2. 异步解析的原子性

    当一个错误高频发生(例如全局报错)时,不要并发去下载同一个 .map 文件。利用 Promise 缓存(Singleflight 模式) 确保同一个版本的 Map 文件只被拉取并解析一次。

  3. 内联(Inline)风险警示

    绝对不要在 webpack.config.js 中使用 evalinline 开头的 devtool 配置。这不仅会暴露源码,还会因为 Base64 字符串嵌入导致 JS 运行速度下降 30% 以上。


💡 结语与下一步

SourceMap 解决了“在哪里报错”的问题。但在监控系统的进阶阶段,我们还需要知道“报错时的上下文(上下文变量、网络请求、用户轨迹)”。

每日一题-计数二进制子串🟢

给定一个字符串 s,统计并返回具有相同数量 01 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。

重复出现(不同位置)的子串也要统计它们出现的次数。

 

示例 1:

输入:s = "00110011"
输出:6
解释:6 个子串满足具有相同数量的连续 1 和 0 :"0011"、"01"、"1100"、"10"、"0011" 和 "01" 。
注意,一些重复出现的子串(不同位置)要统计它们出现的次数。
另外,"00110011" 不是有效的子串,因为所有的 0(还有 1 )没有组合在一起。

示例 2:

输入:s = "10101"
输出:4
解释:有 4 个子串:"10"、"01"、"10"、"01" ,具有相同数量的连续 1 和 0 。

 

提示:

  • 1 <= s.length <= 105
  • s[i]'0''1'

TypeScript 类型体操练习笔记(二)

进度(90 /188)

其中标记 ※ 的是我认为比较难或者涉及新知识点的题目

刷题也许没有什么意义,但是喜欢一个人思考一整天的灵光一现,也喜欢看到新奇的答案时的恍然大悟,仅此而已。

42. Medium - 1130 - ReplaceKeys ※

实现一个类型 ReplaceKeys,用于替换联合类型中的键,如果某个类型不包含该键则跳过替换。该类型接受三个参数。

一开始我只是想这么写,我想分布式条件类型 + Pick + Omit 来实现。

type ReplaceKeys<U, T, Y> = U extends any 
 ? Omit<U, T & keyof U> & Pick<Y, T & keyof U & keyof Y>
 : any

理论上 case1 是能通过的,但是一直报错。然后我又试了一下,看来判断的 Equal 不认为这两种是相等的:

type T1 = { a: number }
type T2 = { b: number }
type E = Equal<T1 & T2, { a: number, b: number }> // false

不过还是有办法的,我们可以通过一层映射把交叉类型拍平:

type IntersectionToObj<T> = {
  [K in keyof T]: T[K]
}
type E1 = Equal<IntersectionToObj<T1 & T2>, { a: number, b: number }> // true

不过我试了下第二个 case 还是不太好实现,那就直接用映射类型来解决。

利用分布式特性处理联合元素,然后遍历 U 的属性然后按要求进行处理即可。

type ReplaceKeys<U, T, Y> = U extends any
  ? {
    [K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K]
  }
  : never // 不会进入这个分支

但是看到别人的答案我又开始困惑了:

type ReplaceKeys<U, T, Y> = {
  [K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K]
}

查了半天只有这个 pr官方文档里也没有明确说明。

形如 { [P in keyof T]: X } 的映射类型(其中 T 是类型参数)被称为 isomorphic mapped type(同构映射类型),因为它会产生一个与 T 具有相同结构的类型。通过此 PR,我们使同构映射类型的实例化在联合类型上具有分布性。

43. Medium - 1367 - Remove Index Signature 移除索引签名 ※

实现 RemoveIndexSignature<T>,移除一个对象类型的索引签名。

索引签名(Index Signature) 是 TypeScript 中用于描述对象中未明确声明的属性的类型。它允许你定义一个对象可以有任意数量的属性,只要这些属性的键和值符合指定的类型。

interface StringDictionary {
  [key: string]: string;  // 索引签名
  // 表示该对象可以有任意多个属性,键必须是 string 类型,值也必须是 string 类型。
}

和索引签名对应的是具体属性,这两种也可以混合使用,但是具体属性的类型必须是索引签名类型的子类型:

interface MixedType {
  // 具体属性
  name: string;
  age: number;
  // 索引签名
  [key: string]: string | number;  // 必须包含具体属性的类型
}

要处理这个问题,就要针对索引签名的特点,他是一个宽泛的类型(string/number/symbol),而具体属性是一个字面量类型,比如 "name" ,我们依次判断它是否为 stringnumbersymbol 都不是则证明是具体属性,否则为索引签名。

type RemoveIndexSignature<T> = {
  [K in keyof T as
  string extends K 
    ? never
    : number extends K 
      ? never
      : symbol extends K 
        ? never
        : K
  ]: T[K]
}`

在评论区看到一个很天才的解法

type RemoveIndexSignature<T, P = PropertyKey> = {
  [K in keyof T as P extends K? never : K extends P ? K : never]: T[K]
}

其中 PropertyKey 上是 TypeScript 的内置类型 type PropertyKey = string | number | symbol;。它的判断过程如下:

P extends K ? never : (K extends P ? K : never)  /* P = string | number | symbol */

// becomes
(string | number | symbol) extends K ? never : (K extends P ? K : never)

// becomes
| string extends K ? never : (K extends string ? K : never)
| number extends K ? never : (K extends number ? K : never)
| symbol extends K ? never : (K extends symbol ? K : never)

本质上和我们上面的写法是一样的,但是利用条件类型的分布性,一下子判断了三种类型。୧(๑•̀◡•́๑)૭

44. Medium - 1978 - Percentage Parser 百分比解析器

实现类型 PercentageParser。根据规则 /^(\+|\-)?(\d*)?(\%)?$/ 匹配类型 T

匹配的结果由三部分组成,分别是:[正负号, 数字, 单位],如果没有匹配,则默认是空字符串。

type Sign = '+' | '-'
type PercentageParser<A extends string> =
  A extends `${infer F}%`
  /** 存在 % */
    ? F extends `${infer S extends Sign}${infer N}`
      ? [S, N, '%']
      : ['', F, '%']
  /** 不存在 % */
    : A extends `${infer S extends Sign}${infer N}`
      ? [S, N, '']
      : ['', A, '']

题目不难,加几个分支判断就可以了。或者这样写优雅一点(大概):

type SignParser<A extends string> = A extends `${infer S extends '+' | '-'}${infer N}` ? [S, N] : ['', A]
type PercentageParser<A extends string> = A extends `${infer F}%` ? [...SignParser<F>, '%'] : [...SignParser<A>, '']

45. Medium - 2070 - Drop Char 删除字符

从字符串中剔除指定字符。

type DropChar<S, C> = S extends `${infer F}${infer R}`
  ? F extends C 
    ? DropChar<R, C>
    : `${F}${DropChar<R, C>}`
  : ''

没有新的知识点,简单题。

46. Medium - 2257 - MinusOne 减一 ※

给定一个正整数作为类型的参数,要求返回的类型是该数字减 1。

有点意思的一道题目,没有新的知识点,但是类似于算法中的模拟题。需要递归加不同情况的判断,复杂度较高。

我先想到了一个比较搞的办法,生成长度为 T 的数组,然后移除一个元素,再获取数组长度。

type MakeArray<T extends number, R extends any[] = []> =
  R['length'] extends T ? R : MakeArray<T, [...R, any]>
type MinusOne<T extends number> = 
  MakeArray<T> extends [infer _F, ...infer R] ? R['length'] : never

1000 以内是可行的,但是再大就会出现错误:

type A = MakeArray<1101> // error: 类型实例化过深,且可能无限。ts(2589)

那么只能换一种方法,通过模拟减法的方式实现,枚举最后一位即可,如果最后一位大于 0 则只需要操作最后一位,否则需要递归处理:

type MinusOne2String<T extends string> =
  T extends `${infer F}0` // 如果最后一位是0,则把此位改为9,然后递归处理(题目限定了是正数)
  ? `${MinusOne2String<F>}9`
  : T extends `${infer F}9` // 其他情况直接把最后一位减一
  ? `${F}8`
  : T extends `${infer F}8`
  ? `${F}7`
  : T extends `${infer F}7`
  ? `${F}6`
  : T extends `${infer F}6`
  ? `${F}5`
  : T extends `${infer F}5`
  ? `${F}4`
  : T extends `${infer F}4`
  ? `${F}3`
  : T extends `${infer F}3`
  ? `${F}2`
  : T extends `${infer F}2`
  ? `${F}1`
  : T extends `${infer F}1`
  ? `${F}0`
  : '0'
// 100-1=099 这种情况需要删除前导零
type removeLeadZero<T extends string> = 
  T extends '0' ? '0' : T extends `0${infer R}` ? removeLeadZero<R> : T 
// 删除前导零后,转换为数字类型
type MinusOne<T extends number> = 
  removeLeadZero<MinusOne2String<`${T}`>> extends `${infer X extends number}` ? X : 0

47. Medium - 2595 - PickByType

T 中选择可赋值给 U 的属性类型集合。

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never ]: T[K]
}

送分题,知识点前面的题目都有涉及。

48. Medium - 2688 - StartsWith

实现 StartsWith<T, U>,接收两个 string 类型参数,然后判断 T 是否以 U 开头,根据结果返回 truefalse

type StartsWith<T extends string, U extends string> =
  T extends `${U}${infer F}` ? true : false

送分题+1,模板字符串类型基础。

49. Medium - 2693 - EndsWith

实现 EndsWith<T, U>,接收两个 string 类型参数,然后判断 T 是否以 U 结尾,根据结果返回 truefalse

type EndsWith<T extends string, U extends string> =
  T extends `${string}${U}` ? true : false

50. Medium - 2757 - PartialByKeys

实现一个通用的 PartialByKeys<T, K>,它接收两个类型参数 TK

K 指定应设置为可选的 T 的属性集。当没有提供 K 时,它就和普通的 Partial<T> 一样使所有属性都是可选的。

前面已经讲过 IntersectionToObj 这个小技巧,这里就比较简单了,其中 Partial 是内置的工具类型,可以把一个对象类型的全部属性都变成可选。

type IntersectionToObj<T> = {
  [K in keyof T]: T[K]
}

type PartialByKeys<T , K extends keyof T = keyof T> = 
  IntersectionToObj< Omit<T, K> & Partial<Pick<T, K>> >

51. Medium - 2759 - RequiredByKeys

实现一个通用的 RequiredByKeys<T, K>,它接收两个类型参数 TK

K 指定应设为必选的 T 的属性集。当没有提供 K 时,它就和普通的 Required<T> 一样使所有的属性成为必选的。

PartialByKeys 本质上没有什么区别,Required 也是内置的工具类型,可以把一个对象类型的全部属性,用于将一个类型 T 中的所有属性转换为‌必填属性(即移除其可选性 ?)。

type IntersectionToObj<T> = {
  [K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends keyof T = keyof T> =
  IntersectionToObj<Omit<T, K> & Required<Pick<T, K>>>

52. Medium - 2793 - Mutable ※

实现一个通用的类型 Mutable<T>,使类型 T 的全部属性可变(非只读)。

这题不难,但是涉及到映射类型的一个语法,之前没有涉及过。mapped-types

type Mutable<T extends object> ={
  -readonly [K in keyof T]: T[K]
}

53. Medium - 2852 - OmitByType

从类型 T 中选择不可赋值给 U 的属性成为一个新的类型。

直到了 asMapped Types 的用法,这也很简单,和之前的 Omit 没什么区别。

type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K]
}

54. Medium - 2946 - ObjectEntries

实现 Object.entries 的类型版本。

首先这题需要应用分布式条件类型,所以需要先构造一个由类型key组成的联合类型 U 然后 U extends ... 触发分布式。

type ObjectEntries<T, U = keyof T> =
  U extends keyof T ? [U, T[U]] : never

不过有个case 过不去。

type eq = Equal<ObjectEntries<Partial<Model>>, ModelEntries> // false
type o = ObjectEntries<Partial<Model>>
// ["name", string | undefined] | ["age", number | undefined] | ["locations", string[] | null | undefined]

可以看到由于 Partial 导致每个类型都多了一个 undefined。很明显这里需要 Required,但是需要先了解一下它的特性。

type r1 = Required<{ key?: undefined }> // {key: never}
type r2 = Required<{ key: undefined }> // {key: undefined}
type r3 = Required<{ key: string | undefined }> // {key: string | undefined}
type r4 = Required<{ key?: string | undefined }>  // {key:string}

可以看到在存在 ? 时,Required 会删除类型中的 undefined,否则不会。

而此题的要求是:如果类型存在 ? 就删除 undefined,但是如果类型只有 undefined 则不处理。我只能说,题本身不难,但是描述的不清楚,只能看用例。

type ObjectEntries<T, U = keyof T> =
  U extends keyof T 
    ? [U, [T[U]] extends [undefined] ? undefined : Required<T>[U]] 
    : never

55. Medium - 3062 - Shift

实现类型版本的 Array.shift

type Shift<T extends any[]> = T extends [infer F, ...infer R] ? [...R] : []

infer 的基础应用,在最前面的 First of Array 就了解过了。

56. Medium - 3188 - Tuple to Nested Object

给一个只包含字符串类型的元组 T 和一个类型 U 递归构建一个对象。

type TupleToNestedObject<T extends string[], U> = 
  T extends [infer F extends string, ...infer R extends string[]] 
    ? Record<F, TupleToNestedObject<R, U>> : U

每次提取数组中第一个元素,然后把该元素作为键,递归构造的对象作为值。

57. Medium - 3192 - Reverse

实现类型版本的数组反转 Array.reverse

type Reverse<T extends any[]> = T extends [infer F, ...infer R] 
  ? [...Reverse<R>, F]
  : []

使用递归的方式,每次都把第一个元素移到最后一个。

58. Medium - 3196 - Flip Arguments

实现 lodash 中 _.flip 函数的类型版本。

类型转换函数 FlipArguments<T> 要求函数类型 T,并返回一个新的函数类型,该类型具有与 T 相同的返回类型但参数顺序颠倒。

type FlipArguments<T extends Function> = 
  T extends (...args: infer P) => infer R ? (...args: Reverse<P>) => R : never

通过 infer 获取函数的参数和返回值,并且通过上一题实现的 Reverse 将参数反转。

59. Medium - 3243 - FlattenDepth

递归展开数组至指定深度

首先需要实现一个铺平一次的函数,这个比较简单

type FlattenOnce<A extends any[]> = A extends [infer F, ...infer R]
  ? F extends any[] ? [...F, ...FlattenOnce<R>] : [F, ...FlattenOnce<R>]
  : []

TypeScript 中无法进行数字计算,我们可以通过邪修实现,这点我们在前面的 MinusOne 已经实现,所以这里直接引用 MinusOne 就可以了。

type FlattenDepth<T extends any[], depth extends number = 1> =
  depth extends 0 // 判断深度为零,则已经不需要铺平了
  ? T
  : FlattenOnce<T> extends T // 判断是否铺平前后的结果一致,一致则不需要再处理了
    ? T 
    : FlattenDepth<FlattenOnce<T>, MinusOne<depth>>

当然我们可以用之前在里面用过的用数组记录数字的方法,只不过 TypeScript 中数组长度有限制,当然这一题中是没有问题的,嵌套最多才 5 层

type FlattenDepth<T extends any[], depth = 1, depArr extends any[] = []> = 
  depArr['length'] extends depth
    ? T 
    : FlattenOnce<T> extends T ? T : FlattenDepth<FlattenOnce<T>, depth, [...depArr, any]>

60. Medium - 3326 - BEM style string

使用块(Block)、元素(Element)、修饰符(Modifier)命名(BEM)是 CSS 中类的一种流行命名约定。

例如,块组件表示为 btn,依赖于块的元素表示为 btn__price,更改块样式的修饰符表示为 btn-bigbtn__prise-warning

实现 BEM<B,E,M>,从这三个参数生成字符串并集。其中 B 是字符串文字,EM 是字符串数组(可以为空)。

// 把A和B连接,先把B处理为联合类型,然后用S连接
type JoinWithSeparator<A extends string, B extends string[], S extends string, B2Union extends string = B[number]> = 
  B2Union extends any ? `${A}${S}${B2Union}` : never

type BEM<B extends string, E extends string[], M extends string[]> = 
  E['length'] extends 0 // 判断E是否为空
    ? JoinWithSeparator<B, M, '--'> // 如果E为空, B和M连接
    : M['length'] extends 0 // 判断M是否为空
      ? JoinWithSeparator<B, E, '__'> // E不为空,M为空,把B和M连接
      : JoinWithSeparator<JoinWithSeparator<B, E, '__'>, M, '--'> // 都不为空,先把B和E连接,然后再加上M

需要写一个辅助工具类型 JoinWithSeparator 用于连接一个字符和数组,逻辑有点小复杂,已经加了完整注释。

61. Medium - 3376 - InorderTraversal

实现二叉树中序遍历的类型版本。

如果会中序遍历二叉树这题就不难了,不会的可以先学学数据结构。

type InorderTraversal<T extends TreeNode | null> = 
 T extends null 
   ? [] 
   : [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]

比较麻烦的是,T extends null 语法无法判断第二个分支中 T 不为空,所以可以反过来,判断 T 是否为 TreeNode

type InorderTraversal<T extends TreeNode | null> =
  T extends TreeNode
  ? [
    ...InorderTraversal<T['left']>,
    T['val'],
    ...InorderTraversal<T['right']>
  ] : []

62. Medium - 4179 - Flip

实现 just-flip-object 的类型版本(把类型的键和值类型反转)。

type Flip<T> = {
  [K in keyof T as T[K] extends number | string | boolean ? `${T[K]}` : never]: K
}

为了保证 T[K] 类型正确,加了一个 extends number | string | boolean 的限制。

63. Medium - 4182 - Fibonacci Sequence 斐波那契序列 ※

实现一个通用的 Fibonacci<T>,它接受一个数字 T 并返回其相应的斐波纳契数

序列开始:1、1、2、3、5、8、13、21、34、55、89、144...

首先斐波纳契公式 f(n)=f(n-1)+f(n-2) 可以递归实现。由于 TypeScript 类型无法使用加法,所以我们通过数组的元素个数来变向进行计算,至于减法可以复用之前实现的 MinusOne

type FibonacciArray<T extends number, A extends any[] = []> =
    T extends 1
    ? [any]
    : T extends 2
      ? [any]
      : [...FibonacciArray<MinusOne<MinusOne<T>>>, ...FibonacciArray<MinusOne<T>>]
type Fibonacci<T extends number> = FibonacciArray<T>['length']

看了下别人的答案,优化空间还是很大的,下面是正向计算,Index 表示计算到了第 n 个数字,Cur 表示 f(n)Prev 表示 f(n-1)

type Fibonacci<
  T extends number,
  Index extends any[] = [any, any],
  Cur extends any[] = [any],
  Prev extends any[] = [any]
> =
  T extends 1 | 2
    ? 1
    : Index['length'] extends T
      ? Cur['length']
      : Fibonacci<T, [...Index, any], [...Cur, ...Prev], Cur>

64. Medium - 4260 - AllCombinations ※

实现 AllCombinations<S> 类型,该类型返回使用 S 中的字符所组成的所有字符串,每个字符最多使用一次。

type AllCombinations<S extends string, P extends string = ''> =
  S extends `${infer F}${infer R}` 
    ? '' | F | `${F}${AllCombinations<`${R}${P}`>}` // S[0] 开头的所有排列情况
      | AllCombinations<R, `${P}${F}`> // 除了 S[0] 开头以外的所有情况
    : ''

很有意思的题目,实现一个字符串中字符的所有组合。我的解法是 AllCombinations<S, P> 表示获取字符串 ${S}${P} 中,以 S 每个字母开头的全排列组合。

所以 AllCombinations<S, ''> 就是答案,而它等于 S[0] 为开头的所有情况,再加上 AllCombinations<S.split(1), S[0]>(伪代码示例)

S[0] 为开头的所有情况,就是求 S[0] 连接剩余字符的全排列,也就是 AllCombinations<S.split(1), ''>

65. Medium - 4425 - Greater Than

在本次挑战中,你需要实现一个类似 T > U 的类型: GreaterThan<T, U> 负数无需考虑。

这种题我可以说不难,就是有点恶心。我不喜欢!下面的代码我加了注释,应该可以看懂。

type LengthOfString<S extends string> = Split<S>['length'];
type FirstOfString<S extends string> = S extends `${infer F}${infer R}`
  ? F
  : never;
type RestOfString<S extends string> = S extends `${infer F}${infer R}`
  ? R
  : never;

type Split<S extends string> = S extends `${infer F}${infer R}`
  ? [F, ...Split<R>]
  : [];

// 比较10以内数字的大小
type GreaterThanDigit<
  T extends string,
  U extends string,
  D extends string = '9876543210'
> = D extends `${infer F}${infer R}` // 从大到小依次比较每一个数字
  ? F extends U // 如果先匹配了U 则证明T≤U 返回false
    ? false
    : F extends T // 如果先匹配了T 则证明T>U 返回true
      ? true
      : GreaterThanDigit<T, U, R> // 再尝试匹配下一个数字
  : false;

type GreaterThanString<
  T extends string,
  U extends string,
  LEN_T extends number = LengthOfString<`${T}`>, // T的长度
  LEN_U extends number = LengthOfString<`${U}`>, // U的长度
  FIRST_T extends string = FirstOfString<`${T}`>, // T的长度
  FIRST_U extends string = FirstOfString<`${U}`> // U的长度
> = LEN_U extends LEN_T // 判断长度是否相同
  ? LEN_T extends 1 // 判断相同,长度是否为1
    ? GreaterThanDigit<FIRST_T, FIRST_U> // 长度为1 直接比较首位
    : FIRST_T extends FIRST_U // 长度相同,且长度不为1,依次比较每一位,先判断首位
      ? GreaterThanString<RestOfString<`${T}`>, RestOfString<`${U}`>> // 首位相同 则比较下一位
      : GreaterThanDigit<FIRST_T, FIRST_U> // 首位不同,则比较大小
  : GreaterThan<LEN_T, LEN_U>; // 如果长度不相同,则长度大的数字更大

type GreaterThan<T extends number, U extends number> = GreaterThanString<
  `${T}`,
  `${U}`
>;

66. Medium - 4471 - Zip

在这个挑战中,你需要实现一个类型 Zip<T, U>,其中 TU 必须是元组。

就是把所有元组中的第1项组成结果中的第1项,所有元组中的第2项组成结果中的第2项....所有元组中的第n项组成结果中的第n项,比较简单。

type Zip<T extends any[], U extends any[]> = T extends [infer TF, ...infer TR]
  ? U extends [infer UF, ...infer UR]
    ? [[TF, UF], ...Zip<TR, UR>]
    : []
  : [];

67. Medium - 4484 - IsTuple ※

实现类型 IsTuple, 传入类型 T 并返回类型 T 是否为一个元组类型

tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.

元组类型 是另一种“数组”类型,它确切地知道它包含多少元素,以及它在特定位置包含哪些类型。

T extends readonly any[] 判断 T 为数组和元素,添加 readonly 可以兼容 readonly [1] 这种情况。

根据定义可以知道,元组类型的长度是固定的,所以 Tuple['length'] 是一个具体的数字,而数组 A['length']number

因此,可以通过 number extends T['length'] 来判断 T 是否为元组而不是数组。

type IsTuple<T> = [T] extends [never]
  ? false
  : T extends readonly any[]
    ? number extends T['length']
      ? false
      : true
    : false;

68. Medium - 4499 - Chunk

你知道 lodash 吗?Chunk 是其中一个非常有用的函数,现在让我们实现它。Chunk<T,N> 接受两个必需的类型参数,T 必须是元组,N 必须是大于等于 1 的整数。

type Chunk<
  T extends any[],
  N extends number,
  Result extends any[] = [],
  Current extends any[] = []
> = T extends [infer F, ...infer R] // 判断是否还有元素
  ? Current['length'] extends N // 有元素,判断当前的块已经满了
    ? Chunk<R, N, [...Result, Current], [F]> // 如果当前的块已经满了,把它放进结果数组里
    : Chunk<R, N, Result, [...Current, F]> // 没有满,就把元素放进当前块
  : Current['length'] extends 0 // T中所有元素都处理完了,判断当前块中是否有元素
    ? Result // 当前块为空的,直接返回结果
    : [...Result, Current]; // 否则把当前块放进结果,再返回

69. Medium - 4518 - Fill

Fill 是一个通用的 JavaScript 函数,现在来实现它的类型版本。Fill<T, N, Start?, End?> 接受 4 个参数, T 是一个元组,N 是任意类型, Start and End 是大于等于 0 的整数。把 T[Start.End] 范围内的元素都替换为 N

type Fill<
  T extends unknown[], // 原数组
  N, // 要填充的类型
  Start extends number = 0, // 开始下标
  End extends number = T['length'], // 结束下标
  Result extends unknown [] = [], // 结果数组
  In extends boolean = false // 是否在[Start,End]范围内
> = T extends [infer F, ...infer R] // T是否存在第一个元素
  ? Result['length'] extends End // 先判断是否为结束下标
    ? Fill<R, N, Start, End, [...Result, F], false>  // 是结束下标,则证明已经填充完了,后面填充T的内容就行
    : Result['length'] extends Start // 不是,判断是否为开始下标
      ? Fill<R, N, Start, End, [...Result, N], true>  // 是开始下标,则填充N,用IN=true表示已经在范围内
      : In extends true // 判断是否在[Start,End]范围内
        ? Fill<R, N, Start, End, [...Result, N], true> // 如果在范围内 则用N填充
        : Fill<R, N, Start, End, [...Result, F], false> // 不在范围内 用T中内容填充
  : Result // 处理完成,返回结果

70. Medium - 4803 - Trim Right

实现 TrimRight<T>,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串结尾的空白字符串。

type TrimRight<S extends string> =
  S extends `${infer F}${' '|'\n'|'\t'}` ? TrimRight<F> : S

71. Medium - 5117 - Without

实现一个像 Lodash.without 函数一样的泛型 Without<T, U>,它接收数组类型的 T 和数字或数组类型的 U 为参数,会返回一个去除 U 中元素的数组 T

Equal 是玄学,别问,用就完事了。

type Includes<T extends readonly unknown[], U> = 
  T extends [infer F, ...infer Rest]
    ? Equal<F, U> extends true ? true : Includes<Rest, U>
    : false;

type Without<T extends any[], U extends any> = T extends [infer F, ...infer R]
  ? U extends any[]
    ? Includes<U, F> extends true // 如果U是数组类型,使用Includes判断是否包含
      ? Without<R, U>
      : [F, ...Without<R, U>]
    : F extends U // 如果U不是数组,直接判断
      ? Without<R, U>
      : [F, ...Without<R, U>]
  : []

评论区看到了更好的解法,先转成联合在判断是否包含

type ToUnion<T extends any> = T extends any[] ? T[number] : T;

type Without<T extends any[], U extends any> = 
T extends [infer F, ...infer R]
  ? F extends ToUnion<U>
    ? Without<R, U>
    : [F, ...Without<R, U>]
  : [];

72. Medium - 5140 - Without

实现 Math.trunc 的类型版本,它接受字符串或数字,并通过删除所有小数来返回数字的整数部分。

简单的模板字符串模式匹配,注意 '-.3' 这种删除小数点后面的内容后需要手动补 0。

type Trunc<T extends number | string> =
  `${T}` extends `${infer F}.${infer _}`
    ? F extends '-' | ''
      ? `${F}0`
      : F
    : `${T}`;

73. Medium - 5153 - IndexOf

实现类型版本的 Array.indexOfindexOf<T, U> 接受两个参数,数组 T 和任意类型 U 返回 UT 中第一次出现的下标,不存在返回 -1

type IndexOf<T extends any[], U, Pre extends any[] = []> =
  T extends [infer F, ...infer R]
    ? Equal<F, U> extends true
      ? Pre['length']
      : IndexOf<R, U, [...Pre, F]>
    : -1

因为 TypeScript 无法进行计算,所以思路还是一样,用一个数组 Pre 记录已经遍历了几个数字,用 Pre['length'] 计数。

74. Medium - 5310 - Join

实现类型版本的 Array.joinJoin<T, U>接受一个数组 T 字符串或数字类型 U,返回 T 中的所有元素用 U 连接的字符串,U 默认为 ','

type Join<
  T extends any[],
  U extends string | number = ',',
  Pre extends string = ''
> = T extends [infer F, ...infer R]
  ? Pre extends ''
    ? Join<R, U, `${F & string}`>
    : Join<R, U, `${Pre}${U}${F & string}`>
  : Pre;

其中 F & string 因为 F 的类型是 any 但是只有一部分类型可以反正模板字符串中,所以这里类型会把报错,通过 & string 限制为 string

75. Medium - 5317 - LastIndexOf

实现类型版本的 Array.lastIndexOfLastIndexOf<T, U> 接受数组 T, any 类型的 U, 如果 U 存在于 T 中, 返回 U 在数组 T 中最后一个位置的索引, 不存在则返回 -1

type LastIndexOf<
  T extends any[],
  U,
  Pre extends any[] = [],
  Result extends number = -1
> = T extends [infer F, ...infer R]
? Equal<F, U> extends true // 判断当前的值是否为U
  ? LastIndexOf<R, U, [...Pre, F], Pre['length']> // 如果是,更新Result,然后继续处理
  : LastIndexOf<R, U, [...Pre, F], Result>
: Result

76. Medium - 5360 - Unique 数组去重

实现类型版本的 Lodash.uniq 方法,Unique 接收数组类型 T, 返回去重后的数组类型。

type Includes<T extends readonly unknown[], U> = 
  T extends [infer F, ...infer Rest]
    ? Equal<F, U> extends true ? true : Includes<Rest, U>
    : false;

type Unique<
  T extends any[],
  Result extends any[] = [],
> = T extends [infer F, ...infer R]
  ? Includes<Result, F> extends true
    ? Unique<R, Result>
    : Unique<R, [...Result, F]>
  : Result;

顺便评论区看到另一个,算是利用分布式条件类型更简洁的实现了 Includes

type Unique<T, U = never> =
  T extends [infer F, ...infer R]
    ? true extends (U extends U ? Equal<U, [F]> : never)
      ? Unique<R, U>
      : [F, ...Unique<R, U | [F]>]
    : []

77. Medium - 5821 - MapTypes 映射类型

实现 MapTypes<T, R>,把对象 T 中的类型根据 R 做转换。比如 R{ mapFrom: string, mapTo: boolean } 表示把 T 中的所有 string 类型改为 boolean

// 根据类型映射的定义 和 原类型 获取映射后的类型
type getType<T extends { mapFrom: any; mapTo: any }, P> = T extends any
  ? T['mapFrom'] extends P // 利用分布式条件类型,依次判断是否匹配mapFrom类型
    ? T['mapTo'] // 符合的返回对应的mapTo类型
    : never // 不符合返回never 其他类型和never联合只会剩下其他类型
  : never;

type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
  // { mapFrom: T[K]; mapTo: any } 是否可以赋值给R
  [K in keyof T]: { mapFrom: T[K]; mapTo: any } extends R
    ? getType<R, T[K]> // 证明他的类型匹配了mapFrom 需要返回对应的mapTo
    : T[K]; // 否则不需要调整
};

78. Medium - 7544 - Construct Tuple 构造元组 ※

构造一个给定长度的元组。

这题简直太水了,递归就可以 TypeScript 最多递归到999层,所以最后一个 case Expect<Equal<ConstructTuple<1000>['length'], 1000>> 会失败。

// 生成元组,但是TS递归只能到999
type ConstructTuple<
  N extends string | number,
  Result extends any[] = []
> = `${Result['length']}` extends `${N}`
  ? Result
  : ConstructTuple<N, [...Result, unknown]>;

但是,让 9999 成功才是有趣的问题,我开始一直想二分,把自己困住了,后来发现简单的按位计算就可以。参考github.com/type-challe…

// 生成元组,但是TS递归只能到999
type ConstructTupleSimple<
  N extends string | number,
  Result extends any[] = []
> = `${Result['length']}` extends `${N}`
  ? Result
  : ConstructTupleSimple<N, [...Result, unknown]>;

// 把数组T中的元素数量*10
type Multi10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]

// 从左到右依次计算 例321 = (3*10+2)*10+1
type ConstructTuple<
  L extends number | string,
  Result extends any[] = []
> = `${L}` extends `${infer F}${infer R}`
  ? ConstructTuple<R, [...Multi10<Result>, ...ConstructTupleSimple<F>]>
  : Result;

79. Medium - 8640 - Number Range

构造指定范围内所有数字的联合。

好像在前面做过类似的题目……通过 Arr 辅助记录遍历数字,Result 记录结果,InRange 记录是否在范围内。

type NumberRange<L, H, Arr extends any[] = [], Result = never, InRange = false> =
  Arr['length'] extends L
    ? NumberRange<L, H, [...Arr, unknown], L | Result, true>
    : Arr['length'] extends H
      ? Result | H
      : InRange extends true
        ? NumberRange<L, H, [...Arr, unknown], Arr['length'] | Result, InRange>
        : NumberRange<L, H, [...Arr, unknown], Result, InRange>

也有更直观的解法:

type ConstructUnion<
  N extends string | number,
  Result extends any[] = []
> = `${Result['length']}` extends `${N}`
  ? Result[number]
  : ConstructUnion<N, [...Result, Result['length']]>;

type NumberRange<L extends number, H extends number> =
  | Exclude<ConstructUnion<H>, ConstructUnion<L>>
  | L
  | H;

80. Medium - 8767 - Combination ※

给定一个字符串数组,执行排列和组合。

// 计算T中每个元素开头的 T和P中所有元素组成的全部组合
type Combination<T extends string[], P extends string[] = []> =
  T extends [infer F extends string, ...infer R extends string[]] // 先计算F开头的所有情况
    ? `${F} ${Combination<[...R, ...P]>}` // 首个单词是F 然后连接 其他单词的全排列
        //(在模板字符串中的联合类型会自动生成所有情况的模板字符串结果的联合)
      | Combination<R, [...P, F]> // 继续计算单个单词剩余单词的情况
      | F // 单个单词
    : never

这种题我每次都要花一个小时做出来,头痛。看了下别人的解法很nb,利用联合类型分布式遍历 T,少了一次递归。

type Combination<T extends string[], All = T[number], Item = All>
  = Item extends string
    ? Item | `${Item} ${Combination<[], Exclude<All, Item>>}`
    : never

81. Medium - 8987 - Subsequence ※

给定一个唯一元素数组,返回所有可能的子序列。

type UnionAddT<U extends any[], T> = U extends any ? [T, ...U] : never
type Subsequence<T extends any[]> = T extends [infer F, ...infer R]
  ? Subsequence<R> // 不包含F的所有子序列
      | UnionAddT<Subsequence<R>, F> // 包含F的所有子序列
  : []

这题不难,加了重点标识因为新学了一个语法:当 ... 展开运算符应用到联合类型时,会对联合类型的每个成员分别展开,然后将结果再组成联合类型。

type Subsequence<T extends unknown[]> = T extends [infer X, ...infer Y]
  ? [X, ...Subsequence<Y>] | Subsequence<Y>
  : [];

82. Medium - 9142 - CheckRepeatedChars

实现类型 CheckRepeatedChars<S> 返回 S 中是否有重复字符。

type CheckRepeatedChars<
  T extends string,
  visited = never
> = T extends `${infer F}${infer R}`
  ? F extends visited
    ? true
    : CheckRepeatedChars<R, visited | F>
  : false;

83. Medium - 9286 - FirstUniqueCharIndex

给一个字符串 S 找到第一个不重复字符的下标,不存在返回 -1。 (灵感来自 leetcode 387)(笑死力扣都来了)

type GetRepectChars<T extends string, Once = never, Repeated = never> = 
  T extends `${infer F}${infer R}`
    ? F extends Once
      ? GetRepectChars<R, Once, Repeated | F>
      : GetRepectChars<R, Once | F, Repeated>
    : Repeated

type FirstUniqueCharIndex<T extends string, Repeated = GetRepectChars<T>, Index extends any[] = []> = 
  T extends `${infer F}${infer R}`
    ? F extends Repeated
      ? FirstUniqueCharIndex<R, Repeated, [...Index, F]>
      : Index['length']
    : -1

84. Medium - 9616 - Parse URL Params

实现类型层面的解析器,把 URL 中的参数字符串解析为一个联合。

type ParseUrlParams<T extends string> = T extends `${infer F}/${infer R}`
  ? F extends `:${infer P}`
    ? P | ParseUrlParams<R>
    : ParseUrlParams<R>
  : T extends `:${infer P}`
    ? P
    : never

85. Medium - 9896 - GetMiddleElement

通过实现一个 GetMiddleElement 方法,获取数组的中间元素,用数组表示

如果数组的长度为奇数,则返回中间一个元素 如果数组的长度为偶数,则返回中间两个元素

type GetMiddleElement<T extends any[]> =
  T['length'] extends 0 | 1 | 2
    ? T
    : T extends [infer _L, ...infer M, infer _R]
      ? GetMiddleElement<M>
      : []

简单,每次删除前后两个元素,对长度为 0 1 2 的数组特殊处理。

86. Medium - 9898 - Appear only once

找出目标数组中只出现过一次的元素。例如:输入 [1,2,2,3,3,4,5,6,6,6],输出 [1,4,5]

// 判断联合类型T中是否存在U
type Includes<T, U> = true extends (T extends any ? Equal<T, U> : never)
  ? true
  : false;

type FindEles<
  T extends any[],
  Pre extends any[] = [],
  Res extends any[] = []
> = T extends [infer F, ...infer R]
  ? Includes<[...Pre, ...R][number], F> extends true // 如果F前后组成的数组是否包含F
    ? FindEles<R, [...Pre, F], Res> // 包含F 则证明不唯一 结果不添加F
    : FindEles<R, [...Pre, F], [...Res, F]> // 不包含F 则证明唯一 结果添加F
  : Res; // 遍历结束 返回结果

87. Medium - 9989 - Count Element Number To Object

通过实现一个 CountElementNumberToObject 方法,统计数组中相同元素的个数。

// 把数组拍平,然后把其中never元素删除
type Flatten<A extends any[]> = A extends [infer F, ...infer R] // 判断A存在第一个元素F
  ? [F] extends [never]
    ? [...Flatten<R>]
    : F extends any[]
    ? [...Flatten<F>, ...Flatten<R>]
    : [F, ...Flatten<R>]
  : [];

type CountElementNumberToObject<
  T extends any[],
  // 辅助计数的对象,用数组计数
  Aux extends Record<string | number, any[]> = {},
  // T中有嵌套数组 把T拍平
  F extends (number | string)[] = Flatten<T>
> = F extends [
  infer L extends number | string, // 取第一个元素
  ...infer R extends (number | string)[]
]
  ? CountElementNumberToObject<
      R,
      {
        [K in keyof Aux | L]: K extends L // 遍历Aux中key
          ? L extends keyof Aux // 遍历到L了,判断如果L是Aux中的key
            ? [...Aux[K], unknown] // 就在对应数组中添加一个元素
            : [unknown] // 不在就新创建一个数组,添加一个元素
          : Aux[K]; // 其他key不做处理
      }
    >
  : {
      [K in keyof Aux]: Aux[K]['length']; // 把结果映射为数组的长度
    };

88. Medium - 10969 - Integer ※

请完成类型 Integer<T>,类型 T 继承于 number,如果 T 是一个整数则返回它,否则返回 never

type OnlyZero<T> = T extends `${infer F}${infer R}`
  ? F extends '0'
    ? OnlyZero<R>
    : false
  : true;
type ToNumber<T> = T extends `${infer N extends number}` ? N : never;
type Integer<T> = T extends number
  ? number extends T
    ? never
    : `${T}` extends `${infer Int}.${infer Deci}`
    ? OnlyZero<Deci> extends true
      ? ToNumber<Int>
      : never
    : T
  : never;

虽然也不算难,但是一看评论区天塌了。

type Integer<T extends string | number> = number extends T
  ? never
  : `${T}` extends `${string}.${string}`
  ? never
  : T;
// 或者这样,因为 bigint 只能是整数
type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never

我自己试了一下数字转字符串,发现对于多余的小数点后面的 0 会被删除。

type x = `${1.0}` // "1"
type x1 = `${1.2}` // "1.2"
type x2 = `${1.200}` // "1.2"

89. Medium - 16259 - ToPrimitive ※

把对象中类型为字面类型(标签类型)的属性,转换为基本类型。

这题可以枚举类型实现,不过就没啥意思了。看到一种神奇的解法,用到了 valueOf

type ToPrimitive<T> = T extends (...args: any[]) => any
  ? Function
  : T extends object
    ? { [K in keyof T]: ToPrimitive<T[K]> }
    : T extends { valueOf: () => infer R }
      ? R
      : T;

JavaScript 中每个包装对象都有 valueOf() 方法:

  • String.prototype.valueOf() 返回 string
  • Number.prototype.valueOf() 返回 number
  • ...

在 TypeScript 类型系统中,我们可以利用这个特性:

interface String {
  /** Returns the primitive value of the specified object. */
  valueOf(): string;
  // ... 其他方法
}

// string 字面量类型
type Test1 = 'Tom' extends { valueOf: () => infer R } ? R : never
// 'Tom' 是 string 类型,string 有 valueOf(): string
// R = string ✅

type Test2 = 30 extends { valueOf: () => infer R } ? R : never
// 30 是 number 类型,number 有 valueOf(): number
// R = number ✅

90. Medium - 17973 - DeepMutable

实现一个通用的 DeepMutable ,它使对象的每个属性,及其递归的子属性 - 可变。

type DeepMutable<T extends object> = {
  -readonly [K in keyof T]: T[K] extends (...args: any) => any 
    ? T[K]
    : T[K] extends object ? DeepMutable<T[K]> : T[K]
}

一次遍历,简洁写法(Python/Java/C++/C/Go/JS/Rust)

题意:子串必须形如 $\underbrace{\texttt{0}\cdots \texttt{0}}{k\ 个\ \texttt{0}}\underbrace{\texttt{1}\cdots \texttt{1}}{k\ 个\ \texttt{1}}$ 或者 $\underbrace{\texttt{1}\cdots \texttt{1}}{k\ 个\ \texttt{1}}\underbrace{\texttt{0}\cdots \texttt{0}}{k\ 个\ \texttt{0}}$。只能有一段 $\texttt{0}$ 和一段 $\texttt{1}$,不能是 $\texttt{00111}$(两段长度不等)或者 $\texttt{010}$(超过两段)等。

例如 $s = \texttt{001110000}$,按照连续相同字符,分成三组 $\texttt{00},\texttt{111},\texttt{0000}$。

  • 在前两组中,我们可以得到 $2$ 个合法子串:$\texttt{0011}$ 和 $\texttt{01}$。
  • 在后两组中,我们可以得到 $3$ 个合法子串:$\texttt{111000}$、$\texttt{1100}$ 和 $\texttt{10}$。

一般地,遍历 $s$,按照连续相同字符分组,计算每一组的长度。设当前这组的长度为 $\textit{cur}$,上一组的长度为 $\textit{pre}$,那么当前这组和上一组,能得到 $\min(\textit{pre},\textit{cur})$ 个合法子串,加到答案中。

###py

class Solution:
    def countBinarySubstrings(self, s: str) -> int:
        n = len(s)
        pre = cur = ans = 0
        for i in range(n):
            cur += 1
            if i == n - 1 or s[i] != s[i + 1]:
                # 遍历到了这一组的末尾
                ans += min(pre, cur)
                pre = cur
                cur = 0
        return ans

###java

class Solution {
    public int countBinarySubstrings(String S) {
        char[] s = S.toCharArray();
        int n = s.length;
        int pre = 0;
        int cur = 0;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            cur++;
            if (i == n - 1 || s[i] != s[i + 1]) {
                // 遍历到了这一组的末尾
                ans += Math.min(pre, cur);
                pre = cur;
                cur = 0;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int countBinarySubstrings(string s) {
        int n = s.size();
        int pre = 0, cur = 0, ans = 0;
        for (int i = 0; i < n; i++) {
            cur++;
            if (i == n - 1 || s[i] != s[i + 1]) {
                // 遍历到了这一组的末尾
                ans += min(pre, cur);
                pre = cur;
                cur = 0;
            }
        }
        return ans;
    }
};

###c

#define MIN(a, b) ((b) < (a) ? (b) : (a))

int countBinarySubstrings(char* s) {
    int pre = 0, cur = 0, ans = 0;
    for (int i = 0; s[i]; i++) {
        cur++;
        if (s[i] != s[i + 1]) {
            // 遍历到了这一组的末尾
            ans += MIN(pre, cur);
            pre = cur;
            cur = 0;
        }
    }
    return ans;
}

###go

func countBinarySubstrings(s string) (ans int) {
n := len(s)
pre, cur := 0, 0
for i := range n {
cur++
if i == n-1 || s[i] != s[i+1] {
// 遍历到了这一组的末尾
ans += min(pre, cur)
pre = cur
cur = 0
}
}
return
}

###js

var countBinarySubstrings = function(s) {
    const n = s.length;
    let pre = 0, cur = 0, ans = 0;
    for (let i = 0; i < n; i++) {
        cur++;
        if (i === n - 1 || s[i] !== s[i + 1]) {
            // 遍历到了这一组的末尾
            ans += Math.min(pre, cur);
            pre = cur;
            cur = 0;
        }
    }
    return ans;
};

###rust

impl Solution {
    pub fn count_binary_substrings(s: String) -> i32 {
        let s = s.as_bytes();
        let n = s.len();
        let mut pre = 0;
        let mut cur = 0;
        let mut ans = 0;
        for i in 0..n {
            cur += 1;
            if i == n - 1 || s[i] != s[i + 1] {
                // 遍历到了这一组的末尾
                ans += pre.min(cur);
                pre = cur;
                cur = 0;
            }
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $s$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面双指针题单的「六、分组循环」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

【计数二进制子串】计数

思路

  1. 对于 000111 来说,符合要求的子串是 000111 0011 01
    1. 不难发现,如果我们找到一段类似 000111 的数据,就可以用来统计答案
    2. 即 这样前面是连续 0/1 后面是连续 1/0 的数据
    3. 这一段的所有 3 个子串,取决于前面 0/1 的个数和后面 1/0 的个数
    4. min(cnt_pre, cnt_cur)

图片.png

  1. 遍历时,当数字再一次改变时(或到达结尾时),意味着一段结束,并能得到这一段前面和后面数字的个数。
    1. 11101 来说,当我们遍历到最后的 1 时,1110 就是一段可以用来统计答案的数据
    2. 而末尾的 01 则是另一段可以用来统计答案的数据

<图片.png,图片.png>

  1. 小技巧,对字符串结尾增加一个字符,可以将判断逻辑写在一个地方

答题

class Solution {
public:
    int countBinarySubstrings(string s) {
        int ans = 0;
        char last = '-';
        int cnt_pre = 0;
        int cnt_cur = 0;

        s += '-';
        for (auto c : s) {
            if (last != c) {
                last = c;
                ans += min(cnt_pre, cnt_cur);
                cnt_pre = cnt_cur;
                cnt_cur = 0;
            }
            cnt_cur++;
        }
        return ans;
    }
};

致谢

感谢您的观看,如果感觉还不错就点个赞吧,关注我的 力扣个人主页 ,欢迎热烈的交流!

计数二进制子串

方法一:按字符分组

思路与算法

我们可以将字符串 $s$ 按照 $0$ 和 $1$ 的连续段分组,存在 $\textit{counts}$ 数组中,例如 $s = 00111011$,可以得到这样的 $\textit{counts}$ 数组:$\textit{counts} = {2, 3, 1, 2}$。

这里 $\textit{counts}$ 数组中两个相邻的数一定代表的是两种不同的字符。假设 $\textit{counts}$ 数组中两个相邻的数字为 $u$ 或者 $v$,它们对应着 $u$ 个 $0$ 和 $v$ 个 $1$,或者 $u$ 个 $1$ 和 $v$ 个 $0$。它们能组成的满足条件的子串数目为 $\min { u, v }$,即一对相邻的数字对答案的贡献。

我们只要遍历所有相邻的数对,求它们的贡献总和,即可得到答案。

不难得到这样的实现:

###C++

class Solution {
public:
    int countBinarySubstrings(string s) {
        vector<int> counts;
        int ptr = 0, n = s.size();
        while (ptr < n) {
            char c = s[ptr];
            int count = 0;
            while (ptr < n && s[ptr] == c) {
                ++ptr;
                ++count;
            }
            counts.push_back(count);
        }
        int ans = 0;
        for (int i = 1; i < counts.size(); ++i) {
            ans += min(counts[i], counts[i - 1]);
        }
        return ans;
    }
};

###Java

class Solution {
    public int countBinarySubstrings(String s) {
        List<Integer> counts = new ArrayList<Integer>();
        int ptr = 0, n = s.length();
        while (ptr < n) {
            char c = s.charAt(ptr);
            int count = 0;
            while (ptr < n && s.charAt(ptr) == c) {
                ++ptr;
                ++count;
            }
            counts.add(count);
        }
        int ans = 0;
        for (int i = 1; i < counts.size(); ++i) {
            ans += Math.min(counts.get(i), counts.get(i - 1));
        }
        return ans;
    }
}

###JavaScript

var countBinarySubstrings = function(s) {
    const counts = [];
    let ptr = 0, n = s.length;
    while (ptr < n) {
        const c = s.charAt(ptr);
        let count = 0;
        while (ptr < n && s.charAt(ptr) === c) {
            ++ptr;
            ++count;
        }
        counts.push(count);
    }
    let ans = 0;
    for (let i = 1; i < counts.length; ++i) {
        ans += Math.min(counts[i], counts[i - 1]);
    }
    return ans;
};

###Go

func countBinarySubstrings(s string) int {
    counts := []int{}
    ptr, n := 0, len(s)
    for ptr < n {
        c := s[ptr]
        count := 0
        for ptr < n && s[ptr] == c {
            ptr++
            count++
        }
        counts = append(counts, count)
    }
    ans := 0
    for i := 1; i < len(counts); i++ {
        ans += min(counts[i], counts[i-1])
    }
    return ans
}

###C

int countBinarySubstrings(char* s) {
    int n = strlen(s);
    int counts[n], counts_len = 0;
    memset(counts, 0, sizeof(counts));
    int ptr = 0;
    while (ptr < n) {
        char c = s[ptr];
        int count = 0;
        while (ptr < n && s[ptr] == c) {
            ++ptr;
            ++count;
        }
        counts[counts_len++] = count;
    }
    int ans = 0;
    for (int i = 1; i < counts_len; ++i) {
        ans += fmin(counts[i], counts[i - 1]);
    }
    return ans;
}

###Python

class Solution:
    def countBinarySubstrings(self, s: str) -> int:
        counts = []
        ptr, n = 0, len(s)
        
        while ptr < n:
            c = s[ptr]
            count = 0
            while ptr < n and s[ptr] == c:
                ptr += 1
                count += 1
            counts.append(count)
        
        ans = 0
        for i in range(1, len(counts)):
            ans += min(counts[i], counts[i - 1])
        
        return ans

###C#

public class Solution {
    public int CountBinarySubstrings(string s) {
        List<int> counts = new List<int>();
        int ptr = 0, n = s.Length;
        
        while (ptr < n) {
            char c = s[ptr];
            int count = 0;
            while (ptr < n && s[ptr] == c) {
                ptr++;
                count++;
            }
            counts.Add(count);
        }
        
        int ans = 0;
        for (int i = 1; i < counts.Count; i++) {
            ans += Math.Min(counts[i], counts[i - 1]);
        }
        
        return ans;
    }
}

###TypeScript

function countBinarySubstrings(s: string): number {
    const counts: number[] = [];
    let ptr = 0, n = s.length;
    
    while (ptr < n) {
        const c = s[ptr];
        let count = 0;
        while (ptr < n && s[ptr] === c) {
            ptr++;
            count++;
        }
        counts.push(count);
    }
    
    let ans = 0;
    for (let i = 1; i < counts.length; i++) {
        ans += Math.min(counts[i], counts[i - 1]);
    }
    
    return ans;
}

###Rust

impl Solution {
    pub fn count_binary_substrings(s: String) -> i32 {
        let mut counts = Vec::new();
        let bytes = s.as_bytes();
        let n = bytes.len();
        let mut ptr = 0;
        
        while ptr < n {
            let c = bytes[ptr];
            let mut count = 0;
            while ptr < n && bytes[ptr] == c {
                ptr += 1;
                count += 1;
            }
            counts.push(count);
        }
        
        let mut ans = 0;
        for i in 1..counts.len() {
            ans += counts[i].min(counts[i - 1]);
        }
        
        ans
    }
}

这个实现的时间复杂度和空间复杂度都是 $O(n)$。

对于某一个位置 $i$,其实我们只关心 $i - 1$ 位置的 $\textit{counts}$ 值是多少,所以可以用一个 $\textit{last}$ 变量来维护当前位置的前一个位置,这样可以省去一个 $\textit{counts}$ 数组的空间。

代码

###C++

class Solution {
public:
    int countBinarySubstrings(string s) {
        int ptr = 0, n = s.size(), last = 0, ans = 0;
        while (ptr < n) {
            char c = s[ptr];
            int count = 0;
            while (ptr < n && s[ptr] == c) {
                ++ptr;
                ++count;
            }
            ans += min(count, last);
            last = count;
        }
        return ans;
    }
};

###Java

class Solution {
    public int countBinarySubstrings(String s) {
        int ptr = 0, n = s.length(), last = 0, ans = 0;
        while (ptr < n) {
            char c = s.charAt(ptr);
            int count = 0;
            while (ptr < n && s.charAt(ptr) == c) {
                ++ptr;
                ++count;
            }
            ans += Math.min(count, last);
            last = count;
        }
        return ans;
    }
}

###JavaScript

var countBinarySubstrings = function(s) {
    let ptr = 0, n = s.length, last = 0, ans = 0;
    while (ptr < n) {
        const c = s.charAt(ptr);
        let count = 0;
        while (ptr < n && s.charAt(ptr) === c) {
            ++ptr;
            ++count;
        }
        ans += Math.min(count, last);
        last = count;
    }
    return ans;
};

###Go

func countBinarySubstrings(s string) int {
    var ptr, last, ans int
    n := len(s)
    for ptr < n {
        c := s[ptr]
        count := 0
        for ptr < n && s[ptr] == c {
            ptr++
            count++
        }
        ans += min(count, last)
        last = count
    }

    return ans
}

###C

int countBinarySubstrings(char* s) {
    int ptr = 0, n = strlen(s), last = 0, ans = 0;
    while (ptr < n) {
        char c = s[ptr];
        int count = 0;
        while (ptr < n && s[ptr] == c) {
            ++ptr;
            ++count;
        }
        ans += fmin(count, last);
        last = count;
    }
    return ans;
}

###Python

class Solution:
    def countBinarySubstrings(self, s: str) -> int:
        ptr, n = 0, len(s)
        last, ans = 0, 0
        
        while ptr < n:
            c = s[ptr]
            count = 0
            while ptr < n and s[ptr] == c:
                ptr += 1
                count += 1
            ans += min(count, last)
            last = count
        
        return ans

###C#

public class Solution {
    public int CountBinarySubstrings(string s) {
        int ptr = 0, n = s.Length;
        int last = 0, ans = 0;
        
        while (ptr < n) {
            char c = s[ptr];
            int count = 0;
            while (ptr < n && s[ptr] == c) {
                ptr++;
                count++;
            }
            ans += Math.Min(count, last);
            last = count;
        }
        
        return ans;
    }
}

###TypeScript

function countBinarySubstrings(s: string): number {
    let ptr = 0, n = s.length;
    let last = 0, ans = 0;
    
    while (ptr < n) {
        const c = s[ptr];
        let count = 0;
        while (ptr < n && s[ptr] === c) {
            ptr++;
            count++;
        }
        ans += Math.min(count, last);
        last = count;
    }
    
    return ans;
}

###Rust

impl Solution {
    pub fn count_binary_substrings(s: String) -> i32 {
        let bytes = s.as_bytes();
        let n = bytes.len();
        let mut ptr = 0;
        let mut last = 0;
        let mut ans = 0;
        
        while ptr < n {
            let c = bytes[ptr];
            let mut count = 0;
            while ptr < n && bytes[ptr] == c {
                ptr += 1;
                count += 1;
            }
            ans += count.min(last);
            last = count;
        }
        
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(n)$。
  • 空间复杂度:$O(1)$。
❌