阅读视图

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

VitePress 文档站点:打造专业级组件文档(含交互式示例)

本文是《从零到一:构建现代化企业级 Monorepo 项目实战》系列的第六篇。这篇文章将教你如何搭建一个媲美官方文档的专业文档站点。

🎯 本文目标

  • VitePress 在 Monorepo 中的集成
  • 组件示例自动导入和渲染
  • 自定义主题和样式
  • 文档站点性能优化
  • 部署配置

📖 为什么选择 VitePress?

文档工具对比

工具 性能 Vue 支持 Markdown 扩展 学习成本 推荐指数
VuePress ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Docusaurus ⭐⭐⭐ ❌ React ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Docsify ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
VitePress ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

VitePress 优势:

  • ⚡ 基于 Vite,开发和构建速度极快
  • 🎨 Vue 组件可以直接在 Markdown 中使用
  • 📝 Markdown 扩展能力强大
  • 🎯 专为 Vue 生态设计

🏗️ 项目结构设计

docs/
├── .vitepress/
│   ├── config.ts              # 配置文件
│   ├── theme/
│   │   ├── index.ts           # 主题入口
│   │   ├── components/
│   │   │   └── DemoBlock.vue  # 示例容器组件
│   │   └── custom.css         # 自定义样式
│   └── plugins/
│       └── demo-container.ts  # 自定义插件
├── components/
│   ├── button/
│   │   ├── index.md           # Button 文档
│   │   └── basic.vue          # 基础示例
│   └── index.md
├── guide/
│   ├── index.md
│   └── getting-started.md
├── index.md                   # 首页
└── package.json

🚀 第一步:安装和配置

1.1 安装 VitePress

cd docs
pnpm init
pnpm add -D vitepress vue

1.2 基础配置

// docs/.vitepress/config.ts
import { defineConfig } from 'vitepress'
import { resolve } from 'path'

export default defineConfig({
  title: 'GDU Common',
  description: 'GDU 前端通用组件库和工具集',
  lang: 'zh-CN',

  // 主题配置
  themeConfig: {
    logo: '/logo.svg',

    nav: [
      { text: '指南', link: '/guide/' },
      { text: '组件', link: '/components/' },
      { text: '工具', link: '/utils/' },
    ],

    sidebar: {
      '/guide/': [
        {
          text: '开始',
          items: [
            { text: '介绍', link: '/guide/' },
            { text: '快速开始', link: '/guide/getting-started' },
          ],
        },
      ],
      '/components/': [
        {
          text: '组件',
          items: [{ text: 'Button 按钮', link: '/components/button' }],
        },
      ],
    },

    socialLinks: [{ icon: 'github', link: 'https://github.com/your-org/gdu-common' }],

    search: {
      provider: 'local', // 本地搜索
    },
  },

  // Vite 配置
  vite: {
    resolve: {
      alias: {
        '@gdu-common/ui': resolve(__dirname, '../../packages/ui/src'),
        '@gdu-common/utils': resolve(__dirname, '../../packages/utils/src'),
        '@gdu-common/shared': resolve(__dirname, '../../packages/shared/src'),
      },
    },
  },
})

1.3 添加脚本

// docs/package.json
{
  "scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview"
  }
}

🎨 第二步:自定义主题

2.1 创建自定义样式

/* docs/.vitepress/theme/custom.css */

/* 首页渐变背景 */
:root {
  --vp-home-hero-name-color: transparent;
  --vp-home-hero-name-background: linear-gradient(120deg, #bd34fe 30%, #41d1ff);
}

/* 自定义品牌色 */
:root {
  --vp-c-brand-1: #42b983;
  --vp-c-brand-2: #35a069;
  --vp-c-brand-3: #299e5c;
}

/* 代码块样式优化 */
.vp-code-group {
  margin: 16px 0;
}

/* 表格样式 */
.vp-doc table {
  display: block;
  overflow-x: auto;
}

2.2 注册全局组件

// docs/.vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import './custom.css'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 自动注册所有示例组件
    const examples = import.meta.glob('../../components/**/*.vue', {
      eager: true,
    })

    for (const path in examples) {
      const component = examples[path].default
      const name = path.match(/\/([^/]+)\.vue$/)?.[1]
      if (name) {
        app.component(name, component)
      }
    }
  },
}

🎭 第三步:交互式组件示例

3.1 创建 Demo 容器组件

<!-- docs/.vitepress/theme/components/DemoBlock.vue -->
<template>
  <div class="demo-block">
    <div class="demo-preview">
      <slot name="demo" />
    </div>

    <div class="demo-actions">
      <button @click="toggleCode" class="toggle-code-btn">
        {{ showCode ? '隐藏代码' : '查看代码' }}
      </button>
    </div>

    <div v-show="showCode" class="demo-code">
      <slot name="code" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const showCode = ref(false)
const toggleCode = () => {
  showCode.value = !showCode.value
}
</script>

<style scoped>
.demo-block {
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  margin: 16px 0;
}

.demo-preview {
  padding: 24px;
  background: var(--vp-c-bg-soft);
}

.demo-actions {
  padding: 12px 16px;
  border-top: 1px solid var(--vp-c-divider);
  display: flex;
  justify-content: flex-end;
}

.toggle-code-btn {
  padding: 4px 12px;
  font-size: 14px;
  border: 1px solid var(--vp-c-brand-1);
  color: var(--vp-c-brand-1);
  background: transparent;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
}

.toggle-code-btn:hover {
  background: var(--vp-c-brand-1);
  color: white;
}

.demo-code {
  border-top: 1px solid var(--vp-c-divider);
}
</style>

3.2 Markdown 自定义容器插件

// docs/.vitepress/plugins/demo-container.ts
import MarkdownIt from 'markdown-it'
import container from 'markdown-it-container'
import { readFileSync } from 'fs'
import { resolve } from 'path'

export default (md: MarkdownIt) => {
  md.use(container, 'demo', {
    validate(params: string) {
      return params.trim().match(/^demo\s+(.*)$/)
    },

    render(tokens: any[], idx: number) {
      const token = tokens[idx]
      const info = token.info.trim().match(/^demo\s+(.*)$/)

      if (tokens[idx].nesting === 1) {
        // 开始标签
        const demoPath = info?.[1] || ''
        const filePath = resolve(__dirname, '../../components', `${demoPath}.vue`)
        const source = readFileSync(filePath, 'utf-8')

        return `
          <DemoBlock>
            <template #demo>
              <${demoPath.split('/').pop()} />
            </template>
            <template #code>
              
\`\`\`vue
${source}
\`\`\`

            </template>
          </DemoBlock>
          <div>
        `
      } else {
        // 结束标签
        return '</div>\n'
      }
    },
  })
}

3.3 在 config.ts 中注册插件

import demoContainer from './plugins/demo-container'

export default defineConfig({
  markdown: {
    config: md => {
      md.use(demoContainer)
    },
  },
})

📝 第四步:编写组件文档

4.1 创建示例组件

<!-- docs/components/button/basic.vue -->
<template>
  <div class="demo">
    <Button>默认按钮</Button>
    <Button type="primary">主要按钮</Button>
    <Button type="success">成功按钮</Button>
  </div>
</template>

<script setup lang="ts">
import { Button } from '@gdu-common/ui'
</script>

<style scoped>
.demo {
  display: flex;
  gap: 12px;
}
</style>

4.2 编写文档

<!-- docs/components/button.md -->

# Button 按钮

常用的操作按钮。

## 基础用法

最简单的按钮用法。

:::demo button/basic
:::

## API

### Props

| 属性     | 说明     | 类型                                  | 默认值      |
| -------- | -------- | ------------------------------------- | ----------- |
| type     | 按钮类型 | `'default' \| 'primary' \| 'success'` | `'default'` |
| size     | 按钮尺寸 | `'small' \| 'medium' \| 'large'`      | `'medium'`  |
| loading  | 加载状态 | `boolean`                             | `false`     |
| disabled | 禁用状态 | `boolean`                             | `false`     |

### Events

| 事件名 | 说明           | 回调参数                      |
| ------ | -------------- | ----------------------------- |
| click  | 点击按钮时触发 | `(event: MouseEvent) => void` |

### Slots

| 插槽名  | 说明       |
| ------- | ---------- |
| default | 按钮内容   |
| icon    | 自定义图标 |

效果:

  • 📖 文档和代码在一起,易于维护
  • 🎨 实时渲染的组件示例
  • 💻 可以查看源代码
  • 📱 响应式设计

🎨 第五步:首页定制

5.1 英雄区域

## <!-- docs/index.md -->

layout: home

hero:
name: GDU Common
text: 企业级前端通用组件库
tagline: 基于 Vue 3 + TypeScript + Vite
image:
src: /logo.svg
alt: GDU Common
actions: - theme: brand
text: 快速开始
link: /guide/getting-started - theme: alt
text: 查看组件
link: /components/ - theme: alt  
 text: GitHub
link: https://github.com/your-org/gdu-common

---

5.2 特性展示

features:

- icon: 🎨
  title: Vue 3 组件库
  details: 基于 Vue 3 Composition API 开发,提供丰富且高质量的 UI 组件
  link: /components/
  linkText: 查看组件
- icon: ⚡
  title: Vite & Turborepo
  details: 使用 Vite 极速构建,Turborepo 智能缓存,构建速度提升 19 倍
- icon: 🔧
  title: TypeScript 优先
  details: 完整的类型定义和智能提示,提供一流的开发体验
- icon: 📦
  title: Monorepo 架构
  details: 使用 pnpm workspace + Turborepo 管理,支持多包开发和发布

5.3 自定义样式

<!-- docs/index.md -->
<style>
:root {
  --vp-home-hero-name-color: transparent;
  --vp-home-hero-name-background: linear-gradient(-45deg, #bd34fe 30%, #41d1ff);

  --vp-home-hero-image-background-image: linear-gradient(-45deg, #bd34fe50 50%, #47caff50);
  --vp-home-hero-image-filter: blur(44px);
}

.VPFeature {
  transition: all 0.3s;
}

.VPFeature:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
</style>

📱 第六步:高级功能

6.1 组件 Playground

<!-- docs/.vitepress/theme/components/Playground.vue -->
<template>
  <div class="playground">
    <div class="playground-preview">
      <component :is="currentComponent" v-bind="props" />
    </div>

    <div class="playground-controls">
      <h4>属性配置</h4>
      <div v-for="(value, key) in props" :key="key" class="control-item">
        <label>{{ key }}</label>
        <input v-model="props[key]" />
      </div>
    </div>

    <div class="playground-code">
      <pre><code>{{ generatedCode }}</code></pre>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, reactive } from 'vue'
import { Button } from '@gdu-common/ui'

const props = reactive({
  type: 'primary',
  size: 'medium',
  loading: false,
})

const currentComponent = Button

const generatedCode = computed(() => {
  const propsStr = Object.entries(props)
    .map(([key, val]) => `${key}="${val}"`)
    .join(' ')
  return `<Button ${propsStr}>点击我</Button>`
})
</script>

6.2 API 表格自动生成

// docs/.vitepress/plugins/api-table.ts
import { readFileSync } from 'fs'
import { parse } from 'vue/compiler-sfc'

export function extractProps(filePath: string) {
  const source = readFileSync(filePath, 'utf-8')
  const { descriptor } = parse(source)

  // 解析 <script setup> 中的 defineProps
  const scriptContent = descriptor.script?.content || ''
  const propsMatch = scriptContent.match(/defineProps<(.+)>/)

  if (propsMatch) {
    // 提取 Props 类型定义
    return parsePropsType(propsMatch[1])
  }

  return []
}

6.3 暗黑模式支持

// config.ts
export default defineConfig({
  appearance: true, // 启用暗黑模式切换

  themeConfig: {
    // 暗黑模式下的logo
    logo: {
      light: '/logo-light.svg',
      dark: '/logo-dark.svg',
    },
  },
})
/* 暗黑模式样式 */
.dark .demo-block {
  border-color: var(--vp-c-divider);
  background: var(--vp-c-bg-soft);
}

🔧 第七步:Markdown 扩展

7.1 自定义容器

::: tip 提示
这是一个提示信息
:::

::: warning 警告
这是一个警告信息
:::

::: danger 危险
这是一个危险警告
:::

::: details 点击查看详情
这是详细内容
:::

7.2 代码组

::: code-group

\`\`\`bash [pnpm]
pnpm add @gdu-common/ui
\`\`\`

\`\`\`bash [npm]
npm install @gdu-common/ui
\`\`\`

\`\`\`bash [yarn]
yarn add @gdu-common/ui
\`\`\`

:::

7.3 代码高亮行

// 高亮特定行
\`\`\`typescript {2,4-6}
function hello() {
  const name = 'world'  // [!code highlight]

  console.log('line 4')  // [!code highlight]
  console.log('line 5')  // [!code highlight]
  console.log('line 6')  // [!code highlight]
}
\`\`\`

// 标记添加/删除
\`\`\`typescript
function hello() {
  const name = 'world'   // [!code --]
  const name = 'Vue 3'   // [!code ++]
}
\`\`\`

📊 第八步:SEO 优化

8.1 Meta 标签配置

export default defineConfig({
  head: [
    // SEO
    ['meta', { name: 'keywords', content: 'Vue3, 组件库, TypeScript, Monorepo' }],
    ['meta', { name: 'author', content: 'GDU Team' }],

    // Open Graph
    ['meta', { property: 'og:type', content: 'website' }],
    ['meta', { property: 'og:title', content: 'GDU Common' }],
    ['meta', { property: 'og:description', content: '企业级前端组件库' }],
    ['meta', { property: 'og:image', content: '/og-image.png' }],

    // Favicon
    ['link', { rel: 'icon', href: '/favicon.ico' }],
    ['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }],
  ],

  // 最后更新时间
  lastUpdated: true,

  // 干净的 URL
  cleanUrls: true,
})

8.2 站点地图生成

// docs/.vitepress/config.ts
export default defineConfig({
  sitemap: {
    hostname: 'https://gdu-common.example.com',
  },
})

🚀 第九步:性能优化

9.1 构建优化

export default defineConfig({
  vite: {
    build: {
      // 代码压缩
      minify: 'terser',

      // 分包策略
      rollupOptions: {
        output: {
          manualChunks: {
            'vue-vendor': ['vue'],
            'vitepress-vendor': ['vitepress'],
          },
        },
      },

      // Chunk 大小警告
      chunkSizeWarningLimit: 1000,
    },
  },
})

9.2 图片优化

# 使用 webp 格式
docs/public/
├── logo.svg
├── hero.webp          # 首页图片
└── og-image.webp      # 社交分享图片

9.3 代码分割

// 异步加载组件
export default {
  async enhanceApp({ app }) {
    if (!import.meta.env.SSR) {
      const HeavyComponent = await import('./components/HeavyComponent.vue')
      app.component('HeavyComponent', HeavyComponent.default)
    }
  },
}

📦 第十步:部署配置

10.1 静态站点部署

export default defineConfig({
  base: '/', // 根目录部署
  // 或
  base: '/docs/', // 子目录部署

  outDir: '.vitepress/dist',
  cacheDir: '.vitepress/cache',
})

10.2 Nginx 配置

server {
    listen 80;
    server_name docs.gdu-common.com;
    root /var/www/docs/.vitepress/dist;
    index index.html;

    # SPA 路由支持
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

10.3 Docker 部署

# docs/Dockerfile
FROM node:20-alpine

WORKDIR /app

# 复制文件
COPY package.json pnpm-lock.yaml ./
COPY docs ./docs
COPY packages ./packages

# 安装依赖
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile

# 构建文档
RUN pnpm --filter @gdu-common/docs build

# 使用 nginx 服务
FROM nginx:alpine
COPY --from=0 /app/docs/.vitepress/dist /usr/share/nginx/html
COPY docs/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

💡 最佳实践

1. 文档组织结构

docs/
├── guide/                 # 指南(给新手)
│   ├── index.md          # 介绍
│   ├── getting-started.md # 快速开始
│   └── installation.md   # 安装
├── components/            # 组件文档(给使用者)
│   ├── index.md
│   └── button.md
├── utils/                 # 工具文档
│   ├── index.md
│   └── common.md
└── advanced/              # 高级用法(给进阶用户)
    ├── customization.md
    └── theme.md

2. 示例代码规范

<!-- ✅ 好的示例 -->
<template>
  <!-- 清晰的HTML结构 -->
  <div class="demo">
    <Button @click="handleClick"> 点击次数:{{ count }} </Button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@gdu-common/ui'

// 简洁的逻辑
const count = ref(0)
const handleClick = () => {
  count.value++
}
</script>

<style scoped>
/* 必要的样式 */
.demo {
  padding: 20px;
}
</style>

3. 文档写作规范

# ✅ 清晰的标题层级

# 组件名

## 基础用法

### 示例标题

#### 细节说明

# ✅ 代码示例在前,文字说明在后

:::demo button/basic
:::

按钮组件支持多种类型...

# ✅ 完整的 API 文档

## Props

## Events

## Slots

## Methods

📈 效果展示

构建性能

# 文档构建速度
pnpm --filter @gdu-common/docs build

vitepress v1.6.4
✓ building client + server bundles...
✓ rendering pages...

build complete in 4.3s

访问性能

指标 数值 说明
FCP 0.8s 首次内容绘制
LCP 1.2s 最大内容绘制
TTI 1.5s 可交互时间
总分 95/100 Lighthouse 性能分数

用户体验

  • ✅ 本地搜索(无需服务器)
  • ✅ 暗黑模式切换
  • ✅ 响应式设计
  • ✅ 代码一键复制
  • ✅ 交互式示例

🤔 常见问题

Q1: 如何在文档中引用组件?

// 方式1:配置 alias
vite: {
  resolve: {
    alias: {
      '@gdu-common/ui': resolve(__dirname, '../../packages/ui/src')
    }
  }
}

// 方式2:直接导入
import { Button } from '@gdu-common/ui'

Q2: 如何实现组件的在线编辑?

// 集成 @vue/repl
import { Repl } from '@vue/repl'

// 提供在线编辑环境
<Repl :store="store" />

Q3: 如何自动生成 API 文档?

# 使用 vue-docgen-api
pnpm add -D vue-docgen-api

# 解析组件生成 JSON
# 渲染成 Markdown 表格

Q4: 文档部署后 404 怎么办?

// 配置正确的 base
export default defineConfig({
  base: '/', // 根目录
  // 或
  base: '/docs/', // 子目录

  cleanUrls: true, // 干净的 URL
})

🎁 完整示例

目录结构

docs/
├── .vitepress/
│   ├── config.ts
│   ├── theme/
│   │   ├── index.ts
│   │   ├── components/
│   │   │   ├── DemoBlock.vue
│   │   │   └── Playground.vue
│   │   └── custom.css
│   └── plugins/
│       └── demo-container.ts
├── components/
│   ├── button/
│   │   ├── index.md
│   │   ├── basic.vue
│   │   ├── types.vue
│   │   └── loading.vue
│   └── index.md
├── guide/
│   ├── index.md
│   └── getting-started.md
├── public/
│   ├── logo.svg
│   └── favicon.ico
├── index.md
└── package.json

🎉 总结

VitePress 文档站点的核心价值:

技术实现

  • ✅ VitePress 配置和自定义主题
  • ✅ 交互式组件示例
  • ✅ 自动代码提取和高亮
  • ✅ SEO 和性能优化

实际效果

  • 📖 专业级文档体验
  • ⚡ 4.3s 构建,1.2s 加载
  • 🎨 美观的 UI 设计
  • 📱 完美的响应式

关键配置

pnpm doc:dev     # 开发
pnpm doc:build   # 构建
pnpm doc:preview # 预览

下一篇文章,我将分享 CI/CD 自动化流程,包括:

  • GitLab CI 配置
  • 自动化测试
  • 自动化发布
  • Docker 镜像构建

🔗 系列文章


文档站点搭建完成!觉得有帮助的话点个赞支持一下! 👍

你的文档站点用的什么工具?有什么优化技巧?评论区交流! 💬

版本管理实战:Changeset 工作流完全指南(含中英文对照)

本文是《从零到一:构建现代化企业级 Monorepo 项目实战》系列的第五篇。前面我们搭建了项目和代码质量工具,这篇文章将讲解如何优雅地管理多包版本和发布流程。

🎯 本文目标

读完这篇文章,你将学会:

  • Changeset 的工作原理和优势
  • 完整的版本发布流程
  • 语义化版本控制实践
  • CHANGELOG 自动生成技巧
  • Changeset 中英文交互对照

📖 Monorepo 版本管理的挑战

传统方式的问题

# 场景:需要同时发布 3 个相关的包

# 😫 方式1:手动修改版本号
vim packages/shared/package.json    # 1.0.0 → 1.0.1
vim packages/utils/package.json     # 1.0.0 → 1.0.1
vim packages/ui/package.json        # 1.0.0 → 1.0.1

# 😫 方式2:使用 lerna
lerna version patch
# 所有包都升级,即使有些包没有改动

# 😫 方式3:使用 standard-version
pnpm exec standard-version
# 只能基于 commit 判断,不够灵活

理想的版本管理

# ✅ 期望的效果:
1. 灵活选择要更新的包
2. 自动处理依赖关系
3. 生成清晰的 CHANGELOG
4. 一键发布所有变更

🚀 Changeset 工作流

核心概念

Changeset = 变更集合

一个 Changeset 包含:
1. 哪些包需要更新
2. 每个包的版本类型(major/minor/patch)
3. 变更描述(会出现在 CHANGELOG 中)

工作流程图

graph LR
    A[开发代码] --> B[添加 Changeset]
    B --> C[提交代码]
    C --> D[准备发布]
    D --> E[执行 version]
    E --> F[更新版本号]
    F --> G[生成 CHANGELOG]
    G --> H[构建项目]
    H --> I[发布到 NPM]
    I --> J[推送到远程]

📦 安装和配置

1. 安装 Changeset

pnpm add -Dw @changesets/cli
pnpm changeset init

2. 配置文件

// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "access": "public",
  "baseBranch": "master",
  "updateInternalDependencies": "patch",
  "ignore": ["@gdu-common/docs", "@gdu-common/build-config"]
}

配置说明:

字段 说明 推荐值
changelog CHANGELOG 生成器 @changesets/cli/changelog
commit 是否自动提交 false(手动控制更好)
access NPM 发布权限 publicrestricted
baseBranch 主分支名称 mastermain
ignore 不需要发布的包 文档、构建配置等

3. 添加脚本

// package.json
{
  "scripts": {
    "changeset:add": "pnpm changeset add",
    "changeset:version": "pnpm changeset version",
    "changeset:publish": "pnpm build && pnpm changeset publish",
    "changeset:status": "pnpm changeset status",
    "uv": "pnpm changeset && pnpm changeset version"
  }
}

🎬 完整发布流程实战

场景:修复了一个 Bug

# 1. 修改代码
vim packages/ui/src/components/Button/button.vue

# 2. 添加 Changeset
pnpm changeset add

Changeset 交互流程(中英文对照)

步骤1:选择包

🦋 Which packages would you like to include?
   选择要更新的包

( ) @gdu-common/shared
( ) @gdu-common/utils
(●) @gdu-common/ui          # 空格选中
( ) @gdu-common/controls-sdk

→ 操作:空格键选择,回车确认

步骤2:选择 Major(主版本)

🦋 Which packages should have a major bump?
   选择需要主版本更新的包(不兼容的 API 变更)

( ) all packages
( ) @gdu-common/ui@1.0.0

→ 操作:如果不是破坏性变更,直接回车跳过

步骤3:选择 Minor(次版本)

🦋 Which packages should have a minor bump?
   选择需要次版本更新的包(新功能)

( ) all packages
( ) @gdu-common/ui@1.0.0

→ 操作:如果不是新功能,直接回车跳过

步骤4:Patch(补丁版本)

🦋 The following packages will be patch bumped:
   以下包将进行补丁更新(Bug 修复)

@gdu-common/ui@1.0.0

→ 说明:未选 major/minor 的包会自动归到 patch

步骤5:输入变更描述

🦋 Please enter a summary for this change
   请输入变更说明(会出现在 CHANGELOG 中)

Summary » 修复 Button 组件点击事件bug

→ 操作:输入中文描述,回车确认

步骤6:确认

🦋 === Summary of changesets ===
🦋 patch:  @gdu-common/ui

🦋 Is this your desired changeset? (Y/n)
   这是你期望的变更吗?

→ 操作:输入 Y 或直接回车

生成的 Changeset 文件

## <!-- .changeset/funny-elephants-dance.md -->

## "@gdu-common/ui": patch

修复 Button 组件点击事件bug

提交 Changeset

git add .changeset/funny-elephants-dance.md
git commit -m "docs: 添加变更记录"
git push

🔄 更新版本号

执行 version 命令

pnpm changeset version

自动执行的操作:

🦋 Running version command...

# 1. 读取所有 changeset 文件
📝 Found 1 changeset:
   - funny-elephants-dance.md

# 2. 更新 package.json
📦 @gdu-common/ui: 1.0.0 → 1.0.1

# 3. 更新依赖此包的其他包
📦 @gdu-common/docs (dev): @gdu-common/ui@1.0.0 → 1.0.1

# 4. 生成 CHANGELOG
📝 Generating CHANGELOG.md for @gdu-common/ui

# 5. 删除已处理的 changeset 文件
🗑️ Removing .changeset/funny-elephants-dance.md

✅ Version bump complete!

生成的 CHANGELOG

<!-- packages/ui/CHANGELOG.md -->

# @gdu-common/ui

## 1.0.1

### Patch Changes

- 修复 Button 组件点击事件bug

提交版本更新

git add .
git commit -m "chore: 更新版本到 1.0.1"
git push

📦 发布到 NPM

构建并发布

# 1. 构建所有包
pnpm build

# 2. 发布到 NPM
pnpm changeset publish

自动执行:

🦋 Publishing packages to npm...

📦 @gdu-common/ui@1.0.1
✅ Published successfully!

🏷️ Creating git tag...
✅ Tag v1.0.1 created

🚀 Pushing tag to remote...
✅ Tag pushed

🎉 All packages published!

推送代码

git push --follow-tags

🎯 版本类型详解

Major - 主版本(破坏性变更)

示例:修改组件 API

// ❌ 旧版本 1.0.0
<Button type="primary">点击</Button>

// ✅ 新版本 2.0.0
<Button variant="primary">点击</Button>  // type 改成 variant

// 版本:1.0.0 → 2.0.0

Minor - 次版本(新功能)

示例:添加新属性

// 旧版本 1.0.0
<Button>点击</Button>

// 新版本 1.1.0
<Button loading>点击</Button>  // ✨ 新增 loading 属性

// 版本:1.0.0 → 1.1.0
// 向后兼容,不影响旧代码

Patch - 补丁版本(Bug 修复)

示例:修复 Bug

// 旧版本 1.0.0 - 有 Bug
const handleClick = () => {
  emit('click') // 😱 忘记传递 event
}

// 新版本 1.0.1 - 修复
const handleClick = event => {
  emit('click', event) // ✅ 修复
}

// 版本:1.0.0 → 1.0.1

💡 高级用法

1. 预发布版本(Alpha/Beta)

# 进入预发布模式
pnpm changeset pre enter alpha

# 添加变更
pnpm changeset add

# 更新版本
pnpm changeset version
# 生成:1.0.0 → 1.0.1-alpha.0

# 继续添加变更
pnpm changeset add
pnpm changeset version
# 生成:1.0.1-alpha.0 → 1.0.1-alpha.1

# 退出预发布模式
pnpm changeset pre exit

# 最终发布
pnpm changeset version
# 生成:1.0.1-alpha.1 → 1.0.1

2. 批量更新多个包

pnpm changeset add

# 选择多个包
🦋 Which packages would you like to include?
(●) @gdu-common/shared    # 空格选中
(●) @gdu-common/utils     # 空格选中
(●) @gdu-common/ui        # 空格选中

3. 依赖自动更新

# ui 依赖 utils
{
  "dependencies": {
    "@gdu-common/utils": "workspace:^"
  }
}

# 当 utils 更新到 1.1.0 时
# ui 的 CHANGELOG 会自动记录:

## 1.0.5

### Patch Changes

- Updated dependencies
  - @gdu-common/utils@1.1.0

4. 查看即将发布的内容

# 查看状态
pnpm changeset status

# 输出
🦋 Changeset status

This branch has:
- 2 changesets that will bump packages

Packages to be bumped:
@gdu-common/ui: patch
@gdu-common/utils: minor

# 预览版本更新(不实际执行)
pnpm changeset version --dry-run

🔧 实际项目配置

package.json 配置

{
  "name": "@gdu-common/ui",
  "version": "1.2.3",
  "publishConfig": {
    "registry": "http://jfrog.gdu-tech.com/artifactory/api/npm/gdu-npm-front/",
    "access": "public"
  },
  "repository": {
    "type": "git",
    "url": "https://gitlab.gdu-tech.com/front-group/template/gdu-common.git",
    "directory": "packages/ui"
  }
}

发布脚本优化

// 根 package.json
{
  "scripts": {
    "uv": "pnpm changeset && pnpm changeset version",
    "publish:all": "pnpm build && pnpm changeset publish",
    "push": "git push --follow-tags"
  }
}

简化流程:

# 一键添加变更并更新版本
pnpm uv

# 一键构建并发布
pnpm publish:all

# 推送
pnpm push

📝 CHANGELOG 管理

自动生成的 CHANGELOG

# @gdu-common/ui

## 1.2.0

### Minor Changes

- 添加 Button 组件的 loading 状态
- 支持自定义图标

### Patch Changes

- 修复 Input 组件的清除按钮样式问题
- Updated dependencies
  - @gdu-common/utils@1.1.0

## 1.1.0

### Minor Changes

- 新增 Input 组件
- 新增 Modal 组件

### Patch Changes

- 修复 Button 组件在 Safari 下的样式问题

CHANGELOG 最佳实践

# ✅ 好的变更描述

- 修复 Button 组件在暗色模式下的对比度问题
- 添加 Input 组件的键盘导航支持
- 优化 Modal 组件的动画性能

# ❌ 不好的变更描述

- 修复 bug
- 更新组件
- 改进代码

🎯 实战演练:完整发布流程

场景:开发了一个新功能

# ===== 第1步:开发完成 =====
# 添加了 Button 组件的 loading 状态

# ===== 第2步:添加 Changeset =====
pnpm changeset add

🦋 Which packages would you like to include?
(●) @gdu-common/ui

🦋 Which packages should have a minor bump?
(●) @gdu-common/ui@1.1.0  # 新功能选 minor

🦋 Summary » 添加 Button 组件的 loading 状态

🦋 Is this your desired changeset? (Y/n) » Y

✅ Changeset added!

# ===== 第3步:提交 Changeset =====
git add .changeset/
git commit -m "docs: 添加变更记录"
git push

# ===== 第4步:准备发布 =====
# 所有功能开发完成,准备发布

# ===== 第5步:更新版本号 =====
pnpm changeset version

🦋 All files have been updated and committed

# 查看变更
git diff HEAD~1

# packages/ui/package.json
- "version": "1.1.0"
+ "version": "1.2.0"

# packages/ui/CHANGELOG.md 新增内容
+ ## 1.2.0
+ ### Minor Changes
+ - 添加 Button 组件的 loading 状态

# ===== 第6步:提交版本更新 =====
git add .
git commit -m "chore: 发布 v1.2.0"
git push

# ===== 第7步:构建 =====
pnpm build

Tasks:    4 successful, 4 total
Cached:   3 cached, 4 total
  Time:   2.1s

# ===== 第8步:发布到 NPM =====
pnpm changeset publish

🦋 Publishing packages to npm...
📦 @gdu-common/ui@1.2.0
✅ Published

# ===== 第9步:推送标签 =====
pnpm push

✅ 发布完成!

🎨 高级技巧

1. 一次更新多个包

# 场景:utils 添加了新函数,ui 使用了这个函数

pnpm changeset add

🦋 Which packages would you like to include?
(●) @gdu-common/utils     # utils 新增功能
(●) @gdu-common/ui        # ui 使用新功能

🦋 Which packages should have a minor bump?
(●) @gdu-common/utils@1.0.0  # utils minor

🦋 Which packages should have a patch bump?
(●) @gdu-common/ui@1.1.0     # ui patch(只是使用,不是新功能)

🦋 Summary »
工具库新增 formatCurrency 函数
UI 组件使用新的货币格式化工具

2. 依赖自动升级

// .changeset/config.json
{
  "updateInternalDependencies": "patch"
}

效果:

# utils 升级到 1.1.0
# ui 依赖 utils,自动升级一个 patch 版本

@gdu-common/utils: 1.0.0 → 1.1.0 (minor)
@gdu-common/ui: 1.0.0 → 1.0.1 (patch, 因为依赖更新)

3. 快捷命令

{
  "scripts": {
    "uv": "pnpm changeset && pnpm changeset version"
  }
}
# 一个命令完成添加和更新
pnpm uv

# 交互完成后,自动执行 version
# 减少操作步骤!

📊 版本管理最佳实践

1. 语义化版本控制

严格遵循 SemVer:

版本格式:主版本.次版本.补丁版本

1.2.3
│ │ │
│ │ └─ Patch:Bug 修复,向后兼容
│ └─── Minor:新功能,向后兼容
└───── Major:破坏性变更,不向后兼容

判断标准:

变更类型 版本类型 示例
修改 API 接口 Major 删除/重命名参数
添加新组件 Minor 新增 Button 组件
修复样式bug Patch 修复按钮颜色
优化性能 Patch 优化渲染性能
添加新属性 Minor Button 新增 loading
修改内部实现 Patch 重构内部逻辑

2. 变更描述规范

✅ 好的描述:

- 添加 Button 组件的 loading 状态支持
- 修复 Input 组件在 IE11 下的兼容性问题
- 优化 Modal 组件的打开动画性能,减少 30% 的渲染时间

❌ 不好的描述:

- 更新
- 修复bug
- 改进

3. 发布前检查

# 创建 pre-publish 脚本
{
  "scripts": {
    "prepublishOnly": "pnpm lint:all && pnpm build"
  }
}

# 发布前自动执行检查和构建

🤖 自动化发布

CI/CD 集成

# .gitlab-ci.yml
version-check:
  stage: check
  script:
    - |
      if [ -d ".changeset" ] && [ "$(ls -A .changeset/*.md 2>/dev/null)" ]; then
        echo "✅ Found changeset files"
      else
        echo "⚠️ No changeset files found"
        echo "💡 Run: pnpm changeset add"
        exit 1
      fi

publish:
  stage: deploy
  only:
    - master
  script:
    - pnpm install
    - pnpm build
    - pnpm changeset publish

📈 实际效果

版本发布效率对比

方式 手动操作 Changeset 提升
修改版本号 5分钟 30秒 10x
生成 CHANGELOG 15分钟 自动
发布到 NPM 10分钟 2分钟 5x
总耗时 30分钟 3分钟 10x

版本管理准确性

# 使用 Changeset 前
- 忘记更新版本号:20% 的发布
- 版本号错误:10% 的发布
- CHANGELOG 缺失:50% 的发布

# 使用 Changeset 后
- 自动更新,100% 准确 ✅
- 自动生成 CHANGELOG ✅
- 依赖关系自动处理 ✅

🤔 常见问题

Q1: 多人协作时如何处理冲突?

场景: 两个人同时添加了 changeset

# 开发者 A
.changeset/cool-cats-sing.md
  → 更新 ui 组件

# 开发者 B
.changeset/brave-dogs-jump.md
  → 更新 utils 工具

# 合并后
pnpm changeset version
# ✅ 自动合并两个变更,生成统一的版本

Q2: 如何回滚发布?

# 1. 删除 NPM 上的版本(24小时内)
npm unpublish @gdu-common/ui@1.2.0

# 2. 删除 Git tag
git tag -d v1.2.0
git push origin :refs/tags/v1.2.0

# 3. 回滚代码
git reset --hard HEAD~1

Q3: 如何发布特定的包?

# 只发布 ui 包
pnpm changeset publish --filter @gdu-common/ui

# 或者在添加 changeset 时只选择 ui

Q4: Changeset 文件可以手动编辑吗?

## <!-- .changeset/cool-update.md -->

"@gdu-common/ui": minor
"@gdu-common/utils": patch

---

添加新功能并修复相关bug

详细说明:

- UI 组件新增 loading 状态(minor)
- 工具函数修复边界情况(patch)

可以! 手动编辑可以更精确地控制版本和描述。

🎁 快速上手清单

初始化

# 1. 安装
pnpm add -Dw @changesets/cli

# 2. 初始化
pnpm changeset init

# 3. 配置 .changeset/config.json
# 4. 添加脚本到 package.json

日常使用

# 开发功能 → 添加变更
pnpm changeset add

# 准备发布 → 更新版本
pnpm changeset version

# 发布 → 构建并发布
pnpm build && pnpm changeset publish

# 推送
git push --follow-tags

检查清单

  • .changeset/config.json 配置正确
  • baseBranch 设置为你的主分支
  • ignore 包含不需要发布的包
  • 各包的 publishConfig 配置正确
  • Git 远程仓库配置正确

🎉 总结

Changeset 提供了:

核心价值

  1. 灵活的版本控制 - 精确控制每个包的版本
  2. 自动的依赖更新 - 依赖包自动同步版本
  3. 清晰的 CHANGELOG - 自动生成,格式统一
  4. 简单的工作流 - 3个命令完成发布

实际收益

  • 📈 版本管理效率提升 10 倍
  • 🎯 版本准确性 100%
  • 📝 CHANGELOG 覆盖率 100%
  • ⏱️ 发布时间从 30分钟 → 3分钟

关键命令

pnpm changeset add      # 添加变更
pnpm changeset version  # 更新版本
pnpm changeset publish  # 发布
pnpm changeset status   # 查看状态

在下一篇文章中,我将分享 VitePress 文档站点的搭建和优化,包括:

  • 自定义主题配置
  • 组件示例自动导入
  • 交互式文档
  • SEO 和性能优化

🔗 系列文章


Changeset 真的很好用!如果你也在用或者准备用,点个赞让我知道! 👍

你的版本管理用的什么方案?有什么坑要分享?评论区见! 💬

Monorepo 工具大比拼:为什么我最终选择了 pnpm + Turborepo?

本文是《从零到一:构建现代化企业级 Monorepo 项目实战》系列的第二篇。上一篇我们深入分析了 Monorepo 的核心概念,这篇文章将聚焦于工具选型,带你了解主流 Monorepo 工具的优劣,以及我的选择理由。

🎯 本文目标

读完这篇文章,你将了解:

  • 主流 Monorepo 工具的对比分析
  • pnpm workspace 的核心优势
  • Turborepo 为什么这么快
  • 如何根据项目规模选择合适的工具

📖 Monorepo 工具全景图

工具分类

Monorepo 工具链
├── 包管理器层
│   ├── npm workspaces
│   ├── yarn workspaces  
│   └── pnpm workspace ⭐ (我的选择)
│
├── 构建编排层
│   ├── Lerna
│   ├── Rush
│   ├── Nx
│   └── Turborepo ⭐ (我的选择)
│
└── 一体化方案
    ├── Nx (包管理 + 构建)
    └── Rush (包管理 + 构建)

🔍 包管理器对比

npm workspaces vs yarn workspaces vs pnpm workspace

特性 npm yarn pnpm 推荐指数
安装速度 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ 🏆 pnpm
磁盘空间 ⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ 🏆 pnpm
依赖隔离 ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ 🏆 pnpm
幽灵依赖 ❌ 有 ❌ 有 ✅ 无 🏆 pnpm
生态成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ 🏆 npm
学习成本 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ 🏆 npm

pnpm 的杀手级特性

1. 节省磁盘空间(最多节省 75%)

传统 npm/yarn:

# 每个项目都复制一份依赖
~/projects/
├── project-a/node_modules/lodash/  # 1MB
├── project-b/node_modules/lodash/  # 1MB  
├── project-c/node_modules/lodash/  # 1MB
└── project-d/node_modules/lodash/  # 1MB
# 总共 4MB

pnpm 的硬链接:

# 所有项目共享同一份依赖
~/.pnpm-store/
└── lodash@4.17.21/     # 1MB(只存一份)

~/projects/
├── project-a/node_modules/lodash/  → 硬链接
├── project-b/node_modules/lodash/  → 硬链接
├── project-c/node_modules/lodash/  → 硬链接
└── project-d/node_modules/lodash/  → 硬链接
# 总共只占用 1MB!

实际效果:

# 我的项目数据
npm:  1.2 GB node_modules
pnpm: 350 MB node_modules

# 节省空间:70.8%!

2. 杜绝幽灵依赖

什么是幽灵依赖?

// package.json 中没有声明 lodash
{
  "dependencies": {
    "some-package": "^1.0.0"  // some-package 依赖了 lodash
  }
}

// 但你居然可以直接用!这就是幽灵依赖
import _ from 'lodash'  // 😱 能用,但不安全!

pnpm 的严格模式:

# pnpm 会报错
Error: Cannot find module 'lodash'
# 必须显式声明依赖才能使用 ✅

3. 更快的安装速度

性能对比(安装 1000+ 依赖):

npm:  45s  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
yarn: 32s  ▓▓▓▓▓▓▓▓▓▓▓▓
pnpm: 12s  ▓▓▓▓ ⚡

速度提升:
- 比 npm 快 3.75 倍
- 比 yarn 快 2.67 倍

🚀 构建编排工具对比

Lerna vs Rush vs Nx vs Turborepo

📊 综合对比

工具 学习曲线 性能 功能丰富度 配置复杂度 社区活跃度 推荐指数
Lerna ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Rush ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐ ⭐⭐⭐
Nx ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Turborepo ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

1️⃣ Lerna:老牌工具,渐显疲态

优势:

# 简单易用
lerna init
lerna bootstrap
lerna publish

劣势:

  • ❌ 性能较差(没有缓存机制)
  • ❌ 功能有限(主要是版本管理)
  • ❌ 维护不活跃(已转交给 Nx 团队)

适用场景: 小型项目,简单的版本管理需求

2️⃣ Rush:微软出品,企业级方案

优势:

// rush.json - 强大的配置能力
{
  "projects": [
    { "packageName": "ui-lib", "projectFolder": "packages/ui" }
  ],
  "pnpmOptions": {
    "strictPeerDependencies": true
  }
}

特点:

  • ✅ 严格的依赖管理
  • ✅ 企业级特性完善
  • ✅ 支持 pnpm

劣势:

  • ❌ 学习曲线陡峭
  • ❌ 配置复杂
  • ❌ 社区相对小众

适用场景: 大型企业项目,需要严格管理

3️⃣ Nx:功能最强大的方案

优势:

# 强大的代码生成
nx generate @nx/react:component Button

# 智能的依赖图分析
nx graph

# 高效的缓存
nx run-many --target=build --all

特点:

  • ✅ 功能最丰富(代码生成、依赖图、插件系统)
  • ✅ 性能优秀(智能缓存)
  • ✅ 支持多种框架(React、Vue、Angular)

劣势:

  • ❌ 学习成本高
  • ❌ 配置复杂
  • ❌ 上手门槛高

适用场景: 大型项目,需要完整的工具链支持

4️⃣ Turborepo:我的最终选择 🏆

核心优势:

📈 极致的性能

# 真实项目数据对比
                无缓存    有缓存    提升倍数
Lerna:         45s       45s       1x
Rush:          38s       12s       3.2x
Nx:            35s       2.5s      14x
Turborepo:     9s        0.45s     20x ⚡

# Turborepo 在缓存命中时快了 19-20 倍!

🎯 极简的配置

Turborepo 配置:

// turbo.json - 仅需 76 行配置
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

对比 Nx 配置:

// nx.json + workspace.json + project.json
// 需要 200+ 行配置

🔥 零配置开箱即用

# 3 步搞定
npx create-turbo@latest
cd my-turborepo
pnpm install

# 就这么简单!

⚡ 智能缓存机制

# 第一次构建
pnpm build
✓ @company/utils built in 2.1s
✓ @company/ui built in 3.4s

# 代码没变化,再次构建
pnpm build
✓ @company/utils cached ⚡
✓ @company/ui cached ⚡
# 完成时间:0.3s!

🎨 我的技术选型过程

项目需求分析

GDU Common 项目特点:
✓ 4 个包(ui、utils、shared、controls-sdk)
✓ 都使用 Vue 3 + TypeScript
✓ 需要频繁联调
✓ 团队 5-8 人
✓ 需要快速迭代

决策树

开始
  ↓
需要 Monorepo? → 是
  ↓
团队规模? → 5-8人(中小型)
  ↓
是否需要代码生成? → 否
  ↓
是否需要多框架支持? → 否(只用 Vue)
  ↓
最看重什么? → 性能 + 简单
  ↓
选择:pnpm + Turborepo ✅

选型理由

1️⃣ pnpm workspace

为什么不选 npm/yarn?

# npm workspaces 的问题
npm install
# 幽灵依赖问题
# 速度较慢

# yarn workspaces 的问题  
yarn install
# 依赖提升导致的版本冲突
# PnP 模式不够成熟

# pnpm workspace 的优势
pnpm install
# ✅ 快速
# ✅ 严格
# ✅ 节省空间

pnpm-workspace.yaml 配置:

packages:
  - packages/*
  - docs
  - build

就这么简单!

2️⃣ Turborepo

为什么不选 Nx?

# Nx 的问题
- 配置复杂(3-4 个配置文件)
- 学习曲线陡
- 功能过于丰富(我们用不上)

# Turborepo 的优势
- 配置简单(1 个 turbo.json)
- 性能极致(Go 语言编写)
- 专注于构建(做好一件事)

为什么不选 Lerna?

# Lerna 的问题
- 性能差(无缓存机制)
- 功能有限
- 维护不活跃

# 数据对比
Lerna:     45s 构建
Turborepo: 9s 构建(无缓存)
          0.45s 构建(有缓存)

# 差距太明显了!

🛠️ pnpm 深度解析

核心原理:基于符号链接的依赖管理

传统的 node_modules 结构(npm/yarn)

node_modules/
├── package-a/
│   ├── index.js
│   └── node_modules/
│       └── package-b/  # 依赖被提升到顶层
├── package-b/          # 重复了!
└── package-c/

问题:

  • 依赖提升导致幽灵依赖
  • 重复的依赖占用空间

pnpm 的 content-addressable 存储

node_modules/
├── .pnpm/
│   ├── package-a@1.0.0/
│   │   └── node_modules/
│   │       ├── package-a/ → ~//.pnpm-store/...
│   │       └── package-b/ → .pnpm/package-b@1.0.0/...
│   └── package-b@1.0.0/
│       └── node_modules/
│           └── package-b/ → ~/.pnpm-store/...
└── package-a/ → .pnpm/package-a@1.0.0/...

优势:

  • ✅ 扁平的 node_modules,但严格的依赖隔离
  • ✅ 全局存储,硬链接复用
  • ✅ 避免幽灵依赖

实战配置

package.json:

{
  "name": "gdu-common",
  "private": true,
  "scripts": {
    "install": "pnpm install"
  }
}

pnpm-workspace.yaml:

packages:
  - packages/*
  - docs
  - build

.npmrc 配置:

# 使用严格的 peer 依赖检查
strict-peer-dependencies=true

# 不要幽灵依赖
shamefully-hoist=false

# 使用硬链接
link-workspace-packages=true

常用命令

# 安装依赖
pnpm install

# 添加依赖到根目录
pnpm add -w lodash-es

# 添加依赖到特定包
pnpm add vue --filter @gdu-common/ui

# 运行所有包的脚本
pnpm -r build

# 只运行特定包
pnpm --filter @gdu-common/ui build

⚡ Turborepo 深度解析

核心概念

1. 任务编排(Task Orchestration)

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],  // ^ 表示依赖包的 build
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],   // 先 build 再 test
      "outputs": ["coverage/**"]
    }
  }
}

依赖图:

@gdu-common/ui:build
  ↓ 依赖
@gdu-common/utils:build
  ↓ 依赖
@gdu-common/shared:build

# Turborepo 会自动计算正确的执行顺序

2. 智能缓存(Smart Caching)

缓存键计算:

# Turborepo 会基于这些内容计算缓存键
- 源代码的哈希值
- 依赖的哈希值
- 环境变量
- 任务配置

# 任何一个变化,缓存失效

缓存命中示例:

$ pnpm build

Tasks:    4 successful, 4 total
Cached:   4 cached, 4 total ⚡
  Time:   450ms >>> FULL TURBO

# 4 个包全部命中缓存,只用了 450ms!

3. 并行执行(Parallel Execution)

# Turborepo 自动分析依赖关系,最大化并行
                时间轴 →
shared:build    ▓▓▓
                  ↓
utils:build       ▓▓▓▓
                    ↓
ui:build            ▓▓▓▓▓
docs:build        ▓▓▓▓▓▓▓▓

# shared 和 docs 可以并行
# utils 等待 shared 完成
# ui 等待 utils 完成

真实性能数据

我的项目构建性能:

场景 时间 缓存命中 说明
首次构建 9.2s 0/4 无缓存
完全缓存 450ms 4/4 ⚡ FULL TURBO
修改 1 个包 2.3s 3/4 增量构建
修改配置文件 9.1s 0/4 配置变化,缓存失效

效率提升:

  • 完全缓存时提升 20.4 倍 🚀
  • 日常开发平均提升 4-5 倍

Turborepo 配置实战

基础配置:

{
  "$schema": "https://turbo.build/schema.json",
  "ui": "tui",  // 使用终端 UI
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".vitepress/dist/**"],
      "cache": true
    },
    "lint": {
      "cache": true
    },
    "lint:fix": {
      "cache": false  // 修改文件的任务不缓存
    },
    "clean": {
      "cache": false
    }
  }
}

高级配置:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": [
        "$TURBO_DEFAULT$",
        "!{dist,build,coverage,.turbo}/**",
        "!**/*.md",
        "!**/*.test.{ts,tsx}"
      ],
      "outputs": ["dist/**"],
      "cache": true
    }
  },
  "globalDependencies": [
    ".env",
    "tsconfig.json",
    "eslint.config.js"
  ],
  "globalEnv": ["NODE_ENV", "CI"]
}

🎯 最终选择:pnpm + Turborepo

组合优势

pnpm workspace (包管理)
    +
Turborepo (构建编排)
    =
完美组合 🎉

1. pnpm 负责依赖管理

  • 快速安装
  • 节省空间
  • 严格隔离

2. Turborepo 负责构建编排

  • 智能缓存
  • 并行执行
  • 增量构建

3. 强强联合

# pnpm 快速安装依赖
pnpm install  # 12s

# Turborepo 快速构建
pnpm build    # 9s(首次) / 0.45s(缓存)

# 总时间:13s(首次) / 12.5s(缓存)

实际效果

开发效率提升:

# 传统 Multirepo 工作流
修改共享函数 → 发布 → 更新依赖 → 重新安装 → 测试
总耗时:5-10 分钟 😫

# Monorepo + Turborepo 工作流
修改共享函数 → 保存 → 自动重建 → 热更新
总耗时:2-3 秒 ⚡

# 效率提升:100-200 倍!

CI/CD 性能:

# .gitlab-ci.yml
build:
  script:
    - pnpm install          # 3s
    - pnpm build            # 9s (首次)
    # 后续 pipeline 只需 0.5s!

💡 选型建议

根据项目规模选择

小型项目(2-3 个包)

✅ 推荐:pnpm workspace
❌ 不需要:Turborepo

# 理由:包少,构建快,不需要复杂的编排

中型项目(4-10 个包)

✅ 推荐:pnpm + Turborepo
⭐ 最佳组合!

# 理由:缓存和并行构建带来明显收益

大型项目(10+ 个包)

✅ 推荐:pnpm + Turborepo
或
✅ 推荐:pnpm + Nx

# Nx 提供更多功能(代码生成、依赖图)
# Turborepo 更简单,性能更好
# 根据团队技术储备选择

根据团队特点选择

团队特点 推荐方案
前端团队,技术栈统一 pnpm + Turborepo
全栈团队,多技术栈 pnpm + Nx
大型企业,严格管理 pnpm + Rush
简单项目,快速上手 pnpm workspace

🚀 快速体验

创建一个 Turborepo 项目

# 使用官方脚手架
npx create-turbo@latest my-monorepo

# 选择 pnpm
? Which package manager do you want to use? › pnpm

# 项目结构
my-monorepo/
├── apps/
│   ├── web/
│   └── docs/
├── packages/
│   ├── ui/
│   └── eslint-config/
├── turbo.json
└── package.json

运行命令

# 安装依赖
pnpm install

# 构建所有包
pnpm build

# 查看缓存效果
pnpm build  # 第二次运行,体验闪电般的速度 ⚡

📊 成本收益分析

迁移成本

项目 学习成本 迁移时间 配置复杂度
Lerna 1 天 2-3 天
Rush 3-5 天 1-2 周
Nx 5-7 天 1-2 周
Turborepo 半天 1-2 天

长期收益

开发效率:

  • 跨包重构时间减少 80%
  • 本地构建时间减少 90%(缓存命中)
  • CI/CD 时间减少 70%

维护成本:

  • 配置文件减少 75%(统一管理)
  • 依赖冲突减少 90%
  • 版本管理复杂度降低 80%

团队协作:

  • 代码审查效率提升 50%
  • 跨项目问题定位快 3 倍
  • 新人上手时间减少 60%

🎉 总结

经过详细的对比和实践,我选择了 pnpm + Turborepo 组合,理由是:

pnpm 的三大优势

  1. - 安装速度比 npm 快 3.75 倍
  2. - 节省 70% 磁盘空间
  3. - 杜绝幽灵依赖,依赖管理更安全

Turborepo 的三大优势

  1. 极致性能 - 缓存命中时快 20 倍
  2. 🎯 极简配置 - 一个 turbo.json 搞定
  3. 🚀 零学习成本 - 半天上手,开箱即用

实际收益

  • 📈 构建速度提升 20 倍(缓存命中)
  • 💾 磁盘空间节省 70%
  • ⏱️ 开发效率提升 100 倍(跨包修改)

在下一篇文章中,我将手把手带你从零搭建一个完整的 pnpm + Turborepo 项目,包括:

  • 项目初始化
  • 包结构设计
  • 配置文件详解
  • 第一个 Hello World 包

🔗 系列文章


你的项目用的是什么 Monorepo 工具?效果如何?欢迎在评论区分享! 🙏

觉得 pnpm + Turborepo 组合不错?点个赞收藏一下,下篇文章将实战搭建! 👍

2025年了,你还在用传统的多仓库管理吗?Monorepo 架构深度解析

本文是《从零到一:构建现代化企业级 Monorepo 项目实战》系列的第一篇,将带你深入了解 Monorepo 架构的核心概念、优势劣势,以及在什么场景下应该选择它。 结尾会附上源码,开箱即用

微信图片_20251017154022_41_35.png

🎯 本文目标

读完这篇文章,你将了解:

  • 什么是 Monorepo,它解决了什么问题
  • Monorepo vs Multirepo 的详细对比
  • 如何判断你的项目是否适合 Monorepo
  • 业界大厂的 Monorepo 实践案例

📖 背景:多仓库管理的痛点

想象一下这个场景:你的团队维护着一个前端项目生态系统,包含:

my-company/
├── ui-components/     # UI 组件库
├── utils-library/     # 工具函数库
├── shared-types/      # 共享类型定义
├── mobile-app/        # 移动端应用
├── admin-dashboard/   # 管理后台
└── marketing-site/    # 官网

每个项目都有自己的 Git 仓库,看起来很整洁,但实际开发中你会遇到这些问题:

😫 依赖地狱

# 在 mobile-app 中更新 ui-components
cd ui-components
git pull
npm version patch
npm publish

cd ../mobile-app
npm update ui-components  # 😱 版本不匹配!
npm install              # 😱 又要重新安装!

🔄 重复配置

每个仓库都需要:

  • package.json 配置
  • ESLint、Prettier、TypeScript 配置
  • CI/CD 流水线配置
  • Git hooks 配置

6个仓库 = 6套重复配置 = 维护噩梦!

🚫 跨仓库重构困难

// 想要重构一个在多个包中使用的接口?
interface UserInfo {
  id: number
  name: string
  email: string // 想改成 emailAddress
}

你需要:

  1. shared-types 中修改接口
  2. 发布新版本
  3. ui-components 中更新依赖
  4. mobile-app 中更新依赖
  5. admin-dashboard 中更新依赖
  6. 测试所有项目...

一个简单的重构变成了跨仓库的大工程!

🏗️ Monorepo:一个仓库管理所有项目

什么是 Monorepo?

Monorepo(单一仓库)是一种项目管理策略,将多个相关的项目或包存储在同一个 Git 仓库中。

my-company-monorepo/
├── packages/
│   ├── ui-components/
│   ├── utils-library/
│   ├── shared-types/
│   ├── mobile-app/
│   ├── admin-dashboard/
│   └── marketing-site/
├── tools/              # 共享工具
├── docs/               # 统一文档
└── package.json        # 根配置

核心理念

"一个仓库,多个项目,统一管理,独立发布"

⚖️ Monorepo vs Multirepo 深度对比

📊 对比表格

维度 Monorepo Multirepo 胜者
代码共享 ✅ 直接引用,实时同步 ❌ 需要发布-安装流程 🏆 Monorepo
依赖管理 ✅ 统一版本,避免冲突 ❌ 版本碎片化 🏆 Monorepo
重构效率 ✅ 原子性操作,一次完成 ❌ 跨仓库协调复杂 🏆 Monorepo
构建速度 ✅ 增量构建,智能缓存 ❌ 重复构建 🏆 Monorepo
代码审查 ✅ 跨项目变更一个 PR ❌ 多个 PR 难以关联 🏆 Monorepo
工具配置 ✅ 一套配置,全局生效 ❌ 每个仓库重复配置 🏆 Monorepo
权限管理 ❌ 粒度较粗 ✅ 精细化权限控制 🏆 Multirepo
仓库大小 ❌ 单个仓库较大 ✅ 仓库小,克隆快 🏆 Multirepo
团队独立性 ❌ 需要协调 ✅ 团队完全独立 🏆 Multirepo

🎯 详细分析

1. 代码共享:Monorepo 的最大优势

Multirepo 的痛苦:

# 修改共享组件需要 4 步
cd shared-components
# 1. 修改代码
# 2. 发布新版本
npm version patch && npm publish

cd ../main-app
# 3. 更新依赖
npm update shared-components
# 4. 测试验证
npm test

Monorepo 的优雅:

// 直接引用,实时生效
import { Button } from '../shared-components/src/Button'
// 修改 Button 组件,所有引用立即生效!

2. 依赖管理:版本统一的威力

Multirepo 的版本地狱:

// mobile-app/package.json
"dependencies": {
  "lodash": "^4.17.20",
  "shared-utils": "^1.2.3"
}

// admin-dashboard/package.json
"dependencies": {
  "lodash": "^4.17.21",  // 😱 版本不一致!
  "shared-utils": "^1.2.1"  // 😱 版本落后!
}

Monorepo 的统一管理:

// 根 package.json
"devDependencies": {
  "lodash": "^4.17.21"  // ✅ 全局统一版本
}

3. 重构效率:原子性操作

场景: 重命名一个在多个包中使用的函数

Multirepo:

# 需要跨多个仓库协调
1. 在 utils 仓库中重命名函数
2. 发布新版本
3. 在 app-a 仓库中更新引用
4. 在 app-b 仓库中更新引用
5. 在 app-c 仓库中更新引用
# 如果某个步骤出错,整个系统可能不一致!

Monorepo:

# 一次性重构,原子操作
1. 全局搜索替换函数名
2. 一次 commit 完成所有修改
3. 所有包保持一致性

🏢 业界实践案例

Google:Monorepo 的鼻祖

  • 规模: 20亿行代码,9万个文件
  • 工具: 自研的 Blaze/Bazel
  • 效果: 统一的构建系统,高效的代码共享

Facebook/Meta:React 生态

facebook/react/
├── packages/
│   ├── react/
│   ├── react-dom/
│   ├── react-reconciler/
│   ├── scheduler/
│   └── shared/
  • 工具: Yarn Workspaces + Lerna
  • 效果: React 各个包版本同步,开发效率极高

Microsoft:TypeScript + VS Code

microsoft/vscode/
├── src/
├── extensions/
├── build/
└── test/
  • 规模: 100+ 扩展,统一管理
  • 效果: 扩展之间高度集成,用户体验一致

Vercel:现代化工具链

vercel/turbo/
├── crates/          # Rust 代码
├── packages/        # JavaScript 包
│   ├── turbo/
│   ├── eslint-config-turbo/
│   └── create-turbo/
  • 工具: 自研 Turborepo
  • 效果: 极致的构建性能优化

🤔 Monorepo 适合你吗?

✅ 适合 Monorepo 的场景

1. 组件库 + 应用项目

project/
├── packages/
│   ├── ui-components/    # 组件库
│   ├── utils/           # 工具库
│   ├── web-app/         # Web 应用
│   └── mobile-app/      # 移动应用

2. 微前端架构

micro-frontend/
├── packages/
│   ├── shell/           # 主应用
│   ├── module-user/     # 用户模块
│   ├── module-order/    # 订单模块
│   └── shared/          # 共享资源

3. 全栈项目

fullstack/
├── packages/
│   ├── frontend/        # 前端应用
│   ├── backend/         # 后端 API
│   ├── shared-types/    # 共享类型
│   └── database/        # 数据库脚本

❌ 不适合 Monorepo 的场景

1. 完全独立的项目

# 这些项目没有任何关联,强行放在一起没有意义
company/
├── e-commerce-site/     # 电商网站
├── blog-system/         # 博客系统
└── game-platform/       # 游戏平台

2. 不同技术栈的项目

# 技术栈差异太大,共享价值有限
mixed-tech/
├── react-web/           # React 项目
├── vue-admin/           # Vue 项目
├── flutter-mobile/      # Flutter 项目
└── python-api/          # Python 后端

3. 大型团队,严格权限控制

  • 团队规模 > 50人
  • 需要严格的代码访问权限
  • 不同项目的发布周期差异巨大

📊 决策矩阵

用这个表格来评估你的项目:

评估维度 权重 你的项目得分 (1-5) 加权得分
代码共享需求 25% ? ?
团队协作紧密度 20% ? ?
技术栈一致性 15% ? ?
发布周期同步性 15% ? ?
项目关联度 15% ? ?
团队规模适中 10% ? ?

评分标准:

  • 4-5分:强烈推荐 Monorepo
  • 3-4分:可以考虑 Monorepo
  • 1-3分:建议继续使用 Multirepo

🚀 我的项目:GDU Common 的选择

项目背景

我们团队需要开发一套企业级前端解决方案:

  • UI 组件库(给多个项目使用)
  • 工具函数库(通用工具)
  • 飞控 SDK(专业领域)
  • 文档站点(统一文档)

为什么选择 Monorepo?

✅ 强关联性

// UI 组件库依赖工具库
import { formatDate } from '@gdu-common/utils'

// 飞控 SDK 使用共享类型
import { BaseResponse } from '@gdu-common/shared'

✅ 统一发布

# 一个命令发布所有相关包
pnpm changeset publish

✅ 开发效率

# 修改工具函数,UI 组件立即生效,无需发布-安装流程

评估结果

维度 得分 说明
代码共享需求 5/5 UI 组件大量使用工具函数
团队协作紧密度 4/5 同一个前端团队维护
技术栈一致性 5/5 都是 Vue 3 + TypeScript
发布周期同步性 4/5 需要协调发布
项目关联度 5/5 高度关联
团队规模适中 5/5 5-10人团队

总分:4.6/5 → 非常适合 Monorepo!

🎭 Monorepo 的两面性

🌟 优势详解

1. 代码复用最大化

// 在 Monorepo 中,这样的复用变得非常简单
// packages/ui/src/Button.vue
import { throttle } from '@company/utils'
import { theme } from '@company/shared'

export default {
  setup() {
    const handleClick = throttle(() => {
      // 使用共享的节流函数
    }, 300)

    return { handleClick }
  },
}

2. 原子性提交

git log --oneline
abc1234 feat: 添加用户头像组件,更新相关工具函数和类型定义
# 一个 commit 包含了跨多个包的完整功能

3. 统一的工具链

// 根目录的 package.json
{
  "devDependencies": {
    "eslint": "^9.0.0", // 所有包共享
    "prettier": "^3.0.0", // 所有包共享
    "typescript": "^5.0.0" // 所有包共享
  }
}

4. 更好的可见性

# 一个命令查看所有项目状态
pnpm list -r --depth=0

# 一个命令运行所有测试
pnpm test -r

⚠️ 挑战和解决方案

1. 仓库体积大

问题: 单个仓库包含所有代码,克隆时间长

解决方案:

# 使用 Git 的部分克隆
git clone --filter=blob:none <repo-url>

# 或者使用 sparse-checkout 只检出需要的目录
git sparse-checkout set packages/ui-components

2. 构建时间长

问题: 需要构建多个项目

解决方案:

# 使用 Turborepo 的智能缓存
pnpm build  # 只构建变化的包

# 并行构建
turbo run build --parallel

3. 权限控制复杂

问题: 无法对不同包设置不同权限

解决方案:

  • 使用 GitHub 的 CODEOWNERS 文件
  • 配置分支保护规则
  • 使用 CI/CD 控制发布权限

4. CI/CD 复杂度

问题: 需要检测哪些包发生了变化

解决方案:

# 使用 Turborepo 的变更检测
- name: Build changed packages
  run: turbo run build --filter=[HEAD^1]

🌍 业界最佳实践

Google 的经验

"我们发现,当项目之间有共享代码时,Monorepo 能显著提高开发效率。但关键是要有好的工具支持。"

核心实践:

  • 统一的构建系统(Bazel)
  • 严格的代码审查流程
  • 自动化测试覆盖

Facebook 的教训

"早期我们也尝试过 Multirepo,但跨仓库的依赖管理成为了开发效率的瓶颈。"

关键改进:

  • 引入 Yarn Workspaces
  • 开发 Lerna 工具
  • 建立统一的发布流程

Microsoft 的平衡

"我们在 VS Code 项目中使用 Monorepo,但 Office 套件仍然使用 Multirepo。选择取决于项目特性。"

决策因素:

  • 项目关联度
  • 团队结构
  • 发布频率

🎯 实际案例:我的决策过程

项目需求分析

我们的 GDU Common 项目需要:

  1. UI 组件库 - 给多个业务项目使用
  2. 工具函数库 - 通用工具,组件库会使用
  3. 飞控 SDK - 专业领域,相对独立
  4. 文档站点 - 展示所有包的使用方法

关键决策点

✅ 为什么选择 Monorepo?

  1. 高度关联

    // UI 组件大量使用工具函数
    import { formatDate, debounce } from '@gdu-common/utils'
    
  2. 统一技术栈

    • 都是 Vue 3 + TypeScript
    • 都使用 Vite 构建
    • 共享相同的代码规范
  3. 同步发布需求

    # 组件库更新时,工具库也可能需要更新
    # 一次发布,保证版本一致性
    
  4. 团队规模适中

    • 5-8人的前端团队
    • 紧密协作,沟通成本低

❌ 为什么不选择 Multirepo?

  1. 跨仓库重构成本高

    • 修改一个共享接口需要更新多个仓库
    • 版本同步复杂
  2. 重复配置维护成本

    • 4个包 = 4套 ESLint/Prettier 配置
    • CI/CD 配置重复
  3. 开发体验差

    • 本地开发需要 npm link
    • 调试跨包问题困难

🔮 Monorepo 的未来趋势

1. 工具生态成熟

  • Turborepo、Nx、Rush 等专业工具
  • 各大云平台的 Monorepo 支持
  • IDE 的原生支持越来越好

2. 大厂推动

  • Google、Facebook、Microsoft 的成功实践
  • 开源项目的广泛采用
  • 最佳实践的不断完善

3. 开发体验优化

  • 更智能的缓存策略
  • 更快的增量构建
  • 更好的 IDE 集成

💡 关键建议

1. 从小开始

# 不要一开始就搭建复杂的 Monorepo
# 从 2-3 个相关包开始
my-monorepo/
├── packages/
│   ├── core/
│   ├── ui/
│   └── utils/

2. 选择合适的工具

  • 小型项目:pnpm workspace 就够了
  • 中型项目:+ Turborepo 加速构建
  • 大型项目:+ Nx 提供更多功能

3. 建立规范

  • 包命名规范
  • 版本管理策略
  • 代码审查流程
  • 发布流程规范

4. 渐进式迁移

# 不要一次性迁移所有项目
# 先迁移关联度最高的 2-3 个包
# 验证效果后再逐步扩展

🎉 总结

Monorepo 不是银弹,但在合适的场景下,它能显著提高开发效率和代码质量。关键是:

  1. 正确评估项目特性 - 使用决策矩阵
  2. 选择合适的工具 - 根据项目规模选择
  3. 建立完善的规范 - 避免混乱
  4. 渐进式实施 - 降低迁移风险

在下一篇文章中,我将详细对比各种 Monorepo 工具,分享我为什么最终选择了 pnpm + Turborepo 的组合,以及这个选择带来的实际效果。


🔗 系列文章


如果这篇文章对你有帮助,请点赞收藏,也欢迎在评论区分享你的 Monorepo 实践经验! 🙏

你的项目适合 Monorepo 吗?欢迎使用文中的决策矩阵评估一下,并在评论区分享你的评估结果!

🎨 SCSS 高级用法完全指南:从入门到精通

🚀 想让 CSS 写得更爽?本文手把手教你 SCSS 的各种实用技巧,让你的样式代码又好写又好管理!

📚 目录


为了实时查看,我这边使用工程化来练习:

企业微信截图_17604966056743.png

1. 变量与作用域

1.1 局部变量与全局变量

// 全局变量
$primary-color: #3498db;

.container {
  // 局部变量
  $padding: 20px;
  padding: $padding;

  .item {
    // 可以访问父级局部变量
    margin: $padding / 2;
    color: $primary-color;
  }
}

// $padding 在这里不可用

1.2 !global 标志

.element {
  $local-var: 10px;

  @if true {
    // 使用 !global 将局部变量提升为全局
    $local-var: 20px !global;
  }
}

// 现在可以在外部访问
.another {
  padding: $local-var; // 20px
}

1.3 !default 标志

// 设置默认值,如果变量已存在则不覆盖
$base-font-size: 16px !default;
$primary-color: #333 !default;

// 这在创建主题或库时非常有用

1.4 Map 变量

// 定义颜色系统
$colors: (
  primary: #3498db,
  secondary: #2ecc71,
  danger: #e74c3c,
  warning: #f39c12,
  info: #9b59b6,
);

// 使用 map-get 获取值
.button {
  background: map-get($colors, primary);

  &.danger {
    background: map-get($colors, danger);
  }
}

// 深层嵌套的 Map
$theme: (
  colors: (
    light: (
      bg: #ffffff,
      text: #333333,
    ),
    dark: (
      bg: #1a1a1a,
      text: #ffffff,
    ),
  ),
  spacing: (
    small: 8px,
    medium: 16px,
    large: 24px,
  ),
);

// 获取深层值
.dark-mode {
  background: map-get(map-get(map-get($theme, colors), dark), bg);
}

2. 嵌套与父选择器

2.1 父选择器 & 的高级用法

// BEM 命名法
.card {
  padding: 20px;

  &__header {
    font-size: 18px;
  }

  &__body {
    margin: 10px 0;
  }

  &--featured {
    border: 2px solid gold;
  }

  // 伪类
  &:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  }

  // 父选择器在后面
  .dark-theme & {
    background: #333;
  }
}

2.2 嵌套属性

.button {
  // 嵌套属性值
  font: {
    family: 'Helvetica', sans-serif;
    size: 14px;
    weight: bold;
  }

  border: {
    top: 1px solid #ccc;
    bottom: 2px solid #999;
    radius: 4px;
  }

  transition: {
    property: all;
    duration: 0.3s;
    timing-function: ease-in-out;
  }
}

2.3 @at-root 跳出嵌套

.parent {
  color: blue;

  @at-root .child {
    // 这会在根级别生成 .child 而不是 .parent .child
    color: red;
  }

  @at-root {
    .sibling-1 {
      color: green;
    }
    .sibling-2 {
      color: yellow;
    }
  }
}

3. Mixins 高级技巧

3.1 带参数的 Mixin

// 基础 Mixin
@mixin flex-center($direction: row) {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: $direction;
}

// 使用
.container {
  @include flex-center(column);
}

3.2 可变参数 (...)

// 接收任意数量的参数
@mixin box-shadow($shadows...) {
  -webkit-box-shadow: $shadows;
  -moz-box-shadow: $shadows;
  box-shadow: $shadows;
}

// 使用
.card {
  @include box-shadow(0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.05));
}

// 传递多个值
@mixin transition($properties...) {
  transition: $properties;
}

.button {
  @include transition(background 0.3s ease, transform 0.2s ease-out);
}

3.3 @content 指令

// 响应式 Mixin
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'mobile' {
    @media (max-width: 767px) {
      @content;
    }
  } @else if $breakpoint == 'tablet' {
    @media (min-width: 768px) and (max-width: 1023px) {
      @content;
    }
  } @else if $breakpoint == 'desktop' {
    @media (min-width: 1024px) {
      @content;
    }
  }
}

// 使用
.sidebar {
  width: 300px;

  @include respond-to('mobile') {
    width: 100%;
    display: none;
  }

  @include respond-to('tablet') {
    width: 200px;
  }
}

3.4 高级响应式 Mixin

$breakpoints: (
  xs: 0,
  sm: 576px,
  md: 768px,
  lg: 992px,
  xl: 1200px,
  xxl: 1400px,
);

@mixin media-breakpoint-up($name) {
  $min: map-get($breakpoints, $name);
  @if $min {
    @media (min-width: $min) {
      @content;
    }
  } @else {
    @content;
  }
}

@mixin media-breakpoint-down($name) {
  $max: map-get($breakpoints, $name) - 1px;
  @if $max {
    @media (max-width: $max) {
      @content;
    }
  }
}

// 使用
.container {
  padding: 15px;

  @include media-breakpoint-up(md) {
    padding: 30px;
  }

  @include media-breakpoint-up(lg) {
    padding: 45px;
  }
}

3.5 主题切换 Mixin

@mixin theme($theme-name) {
  @if $theme-name == 'light' {
    background: #ffffff;
    color: #333333;
  } @else if $theme-name == 'dark' {
    background: #1a1a1a;
    color: #ffffff;
  }
}


// 更灵活的主题系统
$themes: (
  light: (
    bg: #ffffff,
    text: #333333,
    primary: #3498db,
  ),
  dark: (
    bg: #1a1a1a,
    text: #ffffff,
    primary: #5dade2,
  ),
);

@mixin themed() {
  @each $theme, $map in $themes {
    .theme-#{$theme} & {
      $theme-map: $map !global;
      @content;
      $theme-map: null !global;
    }
  }
}

@function t($key) {
  @return map-get($theme-map, $key);
}

// 使用
.card {
  @include themed() {
    background: t(bg);
    color: t(text);
    border-color: t(primary);
  }
}

4. 函数的妙用

4.1 自定义函数

// 计算 rem
@function rem($pixels, $base: 16px) {
  @return ($pixels / $base) * 1rem;
}

.title {
  font-size: rem(24px); // 1.5rem
  margin-bottom: rem(16px); // 1rem
}

4.2 颜色操作函数

// 创建颜色变体
@function tint($color, $percentage) {
  @return mix(white, $color, $percentage);
}

@function shade($color, $percentage) {
  @return mix(black, $color, $percentage);
}

$primary: #3498db;

.button {
  background: $primary;

  &:hover {
    background: shade($primary, 20%);
  }

  &.light {
    background: tint($primary, 30%);
  }
}

4.3 字符串操作

@function str-replace($string, $search, $replace: '') {
  $index: str-index($string, $search);

  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index +
            str-length($search)), $search, $replace);
  }

  @return $string;
}

// 使用
$font-family: str-replace('Arial, sans-serif', 'Arial', 'Helvetica');

4.4 深度获取 Map 值

@function deep-map-get($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }
  @return $map;
}

$config: (
  theme: (
    colors: (
      primary: (
        base: #3498db,
        light: #5dade2,
      ),
    ),
  ),
);
    
.element {
  color: deep-map-get($config, theme, colors, primary, base);
}

5. 继承与占位符

5.1 基础继承

.message {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.success-message {
  @extend .message;
  border-color: #2ecc71;
  background: #d5f4e6;
}

.error-message {
  @extend .message;
  border-color: #e74c3c;
  background: #fadbd8;
}

5.2 占位符选择器 %

// 占位符不会单独生成 CSS
%flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

%text-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-title {
  @extend %text-truncate;
  font-size: 18px;
}

.modal {
  @extend %flex-center;
  min-height: 100vh;
}

5.3 多重继承

%bordered {
  border: 1px solid #ddd;
}

%rounded {
  border-radius: 8px;
}

%shadowed {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.card {
  @extend %bordered;
  @extend %rounded;
  @extend %shadowed;
  padding: 20px;
}

6. 控制指令

6.1 @if / @else

@mixin theme-color($theme) {
  @if $theme == 'light' {
    background: white;
    color: black;
  } @else if $theme == 'dark' {
    background: black;
    color: white;
  } @else {
    background: gray;
    color: white;
  }
}

.app {
  @include theme-color('dark');
}

6.2 @for 循环

// 生成网格系统
@for $i from 1 through 12 {
  .col-#{$i} {
    width: percentage($i / 12);
  }
}

// 生成间距工具类
$spacing: (5, 10, 15, 20, 25, 30);

@for $i from 1 through length($spacing) {
  $space: nth($spacing, $i);

  .m-#{$space} {
    margin: #{$space}px;
  }
  .p-#{$space} {
    padding: #{$space}px;
  }
  .mt-#{$space} {
    margin-top: #{$space}px;
  }
  .pt-#{$space} {
    padding-top: #{$space}px;
  }
  .mb-#{$space} {
    margin-bottom: #{$space}px;
  }
  .pb-#{$space} {
    padding-bottom: #{$space}px;
  }
}

6.3 @each 循环

// 遍历列表
$colors: primary, secondary, success, danger, warning, info;

@each $color in $colors {
  .btn-#{$color} {
    background: var(--#{$color}-color);
  }
}

// 遍历 Map
$social-colors: (
  facebook: #3b5998,
  twitter: #1da1f2,
  instagram: #e4405f,
  linkedin: #0077b5,
  youtube: #ff0000,
);

@each $name, $color in $social-colors {
  .btn-#{$name} {
    background-color: $color;

    &:hover {
      background-color: darken($color, 10%);
    }
  }
}

// 多重值遍历
$sizes: (small, 12px, 500, medium, 14px, 600, large, 16px, 700);

@each $size, $font-size, $font-weight in $sizes {
  .text-#{$size} {
    font-size: $font-size;
    font-weight: $font-weight;
  }
}

6.4 @while 循环

// 生成渐进式字体大小
$i: 6;
@while $i > 0 {
  h#{$i} {
    font-size: 2em - ($i * 0.2);
  }
  $i: $i - 1;
}

7. 模块化系统

7.1 @use 和 @forward

// _variables.scss
$primary-color: #3498db;
$secondary-color: #2ecc71;

// _mixins.scss
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

// _functions.scss
@function rem($px) {
  @return ($px / 16px) * 1rem;
}

// main.scss - 新的模块系统
@use 'variables' as vars;
@use 'mixins' as mix;
@use 'functions' as fn;

.container {
  @include mix.flex-center;
  color: vars.$primary-color;
  padding: fn.rem(20px);
}

7.2 命名空间

// _config.scss
$primary: #3498db;

@mixin button {
  padding: 10px 20px;
  border-radius: 4px;
}

// styles.scss
@use 'config' as cfg;

.btn {
  @include cfg.button;
  background: cfg.$primary;
}

// 或者移除命名空间前缀
@use 'config' as *;

.btn {
  @include button;
  background: $primary;
}

7.3 @forward 创建索引文件

// styles/_index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';

// main.scss
@use 'styles';

.element {
  color: styles.$primary-color;
  @include styles.flex-center;
}

8. 内置函数库

8.1 颜色函数

$base-color: #3498db;

.color-demo {
  // 颜色调整
  color: adjust-hue($base-color, 45deg);

  // 亮度
  background: lighten($base-color, 20%);
  border-color: darken($base-color, 15%);

  // 饱和度
  &.vibrant {
    background: saturate($base-color, 30%);
  }

  &.muted {
    background: desaturate($base-color, 20%);
  }

  // 透明度
  box-shadow: 0 2px 8px rgba($base-color, 0.3);
  border: 1px solid transparentize($base-color, 0.5);

  // 混合颜色
  &.mixed {
    background: mix(#3498db, #e74c3c, 50%);
  }

  // 补色
  &.complement {
    background: complement($base-color);
  }
}

8.2 数学函数

.math-demo {
  // 基础运算
  width: percentage(5 / 12); // 41.66667%
  padding: round(13.6px); // 14px
  margin: ceil(10.1px); // 11px
  height: floor(19.9px); // 19px

  // 最大最小值
  font-size: max(14px, 1rem);
  width: min(100%, 1200px);

  // 绝对值
  top: abs(-20px); // 20px

  // 随机数
  opacity: random(100) / 100;
}

8.3 列表函数

$list: 10px 20px 30px 40px;

.list-demo {
  // 获取长度
  $length: length($list); // 4

  // 获取元素
  padding-top: nth($list, 1); // 10px
  padding-right: nth($list, 2); // 20px

  // 索引
  $index: index($list, 20px); // 2

  // 追加
  $new-list: append($list, 50px);

  // 合并
  $merged: join($list, (60px 70px));
}

8.4 Map 函数

$theme: (
  primary: #3498db,
  secondary: #2ecc71,
  danger: #e74c3c,
);

.map-demo {
  // 获取值
  color: map-get($theme, primary);

  // 合并 Map
  $extended: map-merge(
    $theme,
    (
      success: #27ae60,
    )
  );

  // 检查键是否存在
  @if map-has-key($theme, primary) {
    background: map-get($theme, primary);
  }

  // 获取所有键
  $keys: map-keys($theme); // primary, secondary, danger

  // 获取所有值
  $values: map-values($theme);
}

8.5 字符串函数

$text: 'Hello World';

.string-demo {
  // 转大写
  content: to-upper-case($text); // "HELLO WORLD"

  // 转小写
  content: to-lower-case($text); // "hello world"

  // 字符串长度
  $length: str-length($text); // 11

  // 查找索引
  $index: str-index($text, 'World'); // 7

  // 切片
  content: str-slice($text, 1, 5); // "Hello"

  // 插入
  content: str-insert($text, ' Beautiful', 6); // "Hello Beautiful World"

  // 去引号
  font-family: unquote('"Arial"'); // Arial
}

9. 实战技巧

9.1 响应式字体大小

@function strip-unit($value) {
  @return $value / ($value * 0 + 1);
}

@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
  $u1: unit($min-vw);
  $u2: unit($max-vw);
  $u3: unit($min-font-size);
  $u4: unit($max-font-size);

  @if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 {
    & {
      font-size: $min-font-size;

      @media screen and (min-width: $min-vw) {
        font-size: calc(
          #{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} *
            ((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)})
        );
      }

      @media screen and (min-width: $max-vw) {
        font-size: $max-font-size;
      }
    }
  }
}

h1 {
  @include fluid-type(320px, 1200px, 24px, 48px);
}

9.2 深色模式切换

$themes: (
  light: (
    bg: #ffffff,
    text: #333333,
    border: #e0e0e0,
    primary: #3498db,
  ),
  dark: (
    bg: #1a1a1a,
    text: #f0f0f0,
    border: #404040,
    primary: #5dade2,
  ),
);

@mixin themed-component {
  @each $theme-name, $theme-colors in $themes {
    [data-theme='#{$theme-name}'] & {
      $theme-map: $theme-colors !global;
      @content;
      $theme-map: null !global;
    }
  }
}

@function theme-color($key) {
  @return map-get($theme-map, $key);
}

.card {
  @include themed-component {
    background: theme-color(bg);
    color: theme-color(text);
    border: 1px solid theme-color(border);
  }

  &__button {
    @include themed-component {
      background: themed-component {
      background: theme-color(primary);
      color: theme-color(bg);
    }
  }
}

9.3 原子化 CSS 生成器

$spacing-map: (
  0: 0,
  1: 0.25rem,
  2: 0.5rem,
  3: 0.75rem,
  4: 1rem,
  5: 1.25rem,
  6: 1.5rem,
  8: 2rem,
  10: 2.5rem,
  12: 3rem,
  16: 4rem,
  20: 5rem,
);

$directions: (
  '': '',
  't': '-top',
  'r': '-right',
  'b': '-bottom',
  'l': '-left',
  'x': (
    '-left',
    '-right',
  ),
  'y': (
    '-top',
    '-bottom',
  ),
);

@each $size-key, $size-value in $spacing-map {
  @each $dir-key, $dir-value in $directions {
    // Margin
    .m#{$dir-key}-#{$size-key} {
      @if type-of($dir-value) == 'list' {
        @each $d in $dir-value {
          margin#{$d}: $size-value;
        }
      } @else {
        margin#{$dir-value}: $size-value;
      }
    }

    // Padding
    .p#{$dir-key}-#{$size-key} {
      @if type-of($dir-value) == 'list' {
        @each $d in $dir-value {
          padding#{$d}: $size-value;
        }
      } @else {
        padding#{$dir-value}: $size-value;
      }
    }
  }
}

9.4 三角形生成器

@mixin triangle($direction, $size, $color) {
  width: 0;
  height: 0;
  border: $size solid transparent;

  @if $direction == 'up' {
    border-bottom-color: $color;
  } @else if $direction == 'down' {
    border-top-color: $color;
  } @else if $direction == 'left' {
    border-right-color: $color;
  } @else if $direction == 'right' {
    border-left-color: $color;
  }
}

.tooltip {
  position: relative;

  &::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    @include triangle(down, 8px, #333);
  }
}

9.5 网格系统生成器

$grid-columns: 12;
$grid-gutter-width: 30px;
$container-max-widths: (
  sm: 540px,
  md: 720px,
  lg: 960px,
  xl: 1140px,
  xxl: 1320px,
);

@mixin make-container($padding-x: $grid-gutter-width / 2) {
  width: 100%;
  padding-right: $padding-x;
  padding-left: $padding-x;
  margin-right: auto;
  margin-left: auto;
}

@mixin make-row($gutter: $grid-gutter-width) {
  display: flex;
  flex-wrap: wrap;
  margin-right: -$gutter / 2;
  margin-left: -$gutter / 2;
}

@mixin make-col($size, $columns: $grid-columns) {
  flex: 0 0 auto;
  width: percentage($size / $columns);
  padding-right: $grid-gutter-width / 2;
  padding-left: $grid-gutter-width / 2;
}

.container {
  @include make-container;

  @each $breakpoint, $width in $container-max-widths {
    @include media-breakpoint-up($breakpoint) {
      max-width: $width;
    }
  }
}
.row {
  @include make-row;
}

@for $i from 1 through $grid-columns {
  .col-#{$i} {
    @include make-col($i);
  }
}

9.6 长阴影效果

@function long-shadow($length, $color, $opacity) {
  $shadow: '';

  @for $i from 0 through $length {
    $shadow: $shadow +
      '#{$i}px #{$i}px rgba(#{red($color)}, #{green($color)}, #{blue($color)}, #{$opacity})';

    @if $i < $length {
      $shadow: $shadow + ', ';
    }
  }

  @return unquote($shadow);
}

.text-shadow {
  text-shadow: long-shadow(50, #000, 0.05);
}

9.7 动画关键帧生成器

@mixin keyframes($name) {
  @keyframes #{$name} {
    @content;
  }
}

@include keyframes(fadeIn) {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

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

9.8 清除浮动

@mixin clearfix {
  &::after {
    content: '';
    display: table;
    clear: both;
  }
}

.container {
  @include clearfix;
}

🎯 总结

SCSS 的高级特性让我们能够:

  1. 提高代码复用性 - 通过 mixin、函数和继承
  2. 增强可维护性 - 使用变量、模块化和命名空间
  3. 提升开发效率 - 利用循环、条件判断自动生成样式
  4. 保持代码整洁 - 嵌套、占位符和模块系统
  5. 创建强大的工具库 - 自定义函数和 mixin 集合

最佳实践建议

  1. 变量命名要语义化

    // Good
    $primary-color: #3498db;
    $spacing-unit: 8px;
    
    // Bad
    $blue: #3498db;
    $var1: 8px;
    
  2. 避免嵌套层级过深(建议不超过 3-4 层)

    // Good
    .card {
      &__header {
      }
      &__body {
      }
    }
    
    // Bad - 嵌套太深
    .card {
      .wrapper {
        .inner {
          .content {
            .text {
            }
          }
        }
      }
    }
    
  3. 优先使用 @use 而不是 @import

// Modern
@use 'variables';
@use 'mixins';

// Legacy
@import 'variables';
@import 'mixins';
  1. 使用占位符代替类继承

    // Good
    %btn-base {
    }
    .btn {
      @extend %btn-base;
    }
    
    // Less optimal
    .btn-base {
    }
    .btn {
      @extend .btn-base;
    }
    
  2. 合理组织文件结构 styles/ ├── abstracts/ │ ├── _variables.scss │ ├── _functions.scss │ └── _mixins.scss ├── base/ │ ├── _reset.scss │ └── _typography.scss ├── components/ │ ├── _buttons.scss │ └── _cards.scss ├── layout/ │ ├── _header.scss │ └── _footer.scss └── main.scss


📚 参考资源


如果这篇文章对你有帮助,欢迎点赞收藏! 👍

有任何问题或补充,欢迎在评论区讨论~ 💬

❌