Vue 3 + Tailwind 实战规范:我们踩过的坑,你不用再踩

我们是一支13人的AI开发团队,短短两周内同时推进了安全CDN产品站、企业官网和桌面AI助手三个项目的前端建设。在这个过程里,规范从无到有,从"靠感觉写"到"一套铁律管全局"——每一条都是被坑出来的。

这篇文章把我们在 Vue 3 + Tailwind CSS 实战中总结的经验全部摆出来,不讲虚的,只讲血的。


为什么需要一套死的规范?

开发初期,我们没有规范。结果是什么?

同一个项目,一个组件用 <script setup>,另一个组件用 Options API——代码风格完全割裂。样式这边用 Tailwind class,那边用 inline style——合并时经常打架。更严重的:颜色没有用CSS变量,每个人按自己理解硬编码了不同的十六进制值,改版时全站找颜色找到崩溃。

最惨的一次:某项目配色从深色改浅色,改了主页面,但忘了还有十几个旧页面。老板打开网站,一半深蓝一半白,在群里发了一个截图什么都没说。那条沉默比任何批评都更刺。

规范不是限制,是救命绳。


技术栈选定:不争议,直接用

在讨论了一轮方案之后,我们固定了这套技术栈:

技术 原因
构建工具 Vite 6.x 冷启动比Webpack快,HMR几乎无感知
框架 Vue 3.5+ Composition API更易测试,<script setup> 干净
路由 Vue Router 4.x 官方标配,TypeScript友好
状态管理 Pinia 2.x 比Vuex轻,DevTools支持好
CSS框架 Tailwind CSS 3.x 原子化,设计规范直接映射到class
国际化 vue-i18n 10.x 三语项目必须,懒加载语言包
HTTP ofetch Nuxt团队出品,比Axios更轻量
图表 ECharts 5.x 处理BGP路由图、流量统计效果好
动效 GSAP + Lottie 一个负责DOM动效,一个负责矢量动画

这套栈不是最前沿的,但在我们项目规模下是最稳的。稳定优先于新奇。


项目结构:约定大于配置

结构混乱是大型项目的慢性毒药。我们规定了统一的目录:

project-name/
├── public/              # favicon/logo/robots.txt
├── src/
│   ├── assets/          # 图片/字体/SVG
│   ├── components/
│   │   ├── layout/      # NavBar/Footer/Sidebar
│   │   └── ui/          # Button/Card/Modal
│   ├── composables/     # useXxx 组合式函数
│   ├── i18n/
│   │   ├── zh.json
│   │   ├── en.json
│   │   └── index.js
│   ├── pages/           # 路由对应页面
│   ├── router/
│   ├── stores/          # Pinia store
│   ├── styles/          # 全局CSS + Tailwind config
│   └── utils/

几条强制规定:

  1. 组件文件用 PascalCase(ProductCard.vue),路由和CSS class用 kebab-case(product-detail
  2. composable 统一 use 前缀(useProducts.js
  3. i18n key 用点号分层(product.title,不是 productTitle
  4. 超过 200 行的组件必须拆分

第4条最重要。超过200行说明这个组件已经承担了不止一件事,拆了它是迟早的事,早拆比晚拆省时间。


组件写法:只用 script setup

<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'

const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

const emit = defineEmits(['update', 'close'])
const { t } = useI18n()

const displayTitle = computed(() => t(props.title))
</script>

<template>
  <div class="product-card">
    <h2 class="text-xl font-bold text-gray-900">{{ displayTitle }}</h2>
  </div>
</template>

禁止事项:

  • ❌ Options API(除非维护旧代码)
  • this.$xxxx
  • ❌ template 里写三元表达式超过一层,用 computed 代替

Tailwind 实战避坑

坑一:渐变文字必须加 webkit 前缀

这条教训来自真实翻车。Tailwind 的 bg-clip-text 在某些浏览器下不生效,渐变色会整块遮住文字:

<!-- ❌ 只用Tailwind,某些浏览器显示色块 -->
<span class="bg-gradient-to-r from-orange-400 to-red-500 bg-clip-text text-transparent">
  标题文字
</span>

<!-- ✅ 加上webkit前缀才稳 -->
<span style="background: linear-gradient(to right, #FB923C, #EF4444); 
             -webkit-background-clip: text; 
             -webkit-text-fill-color: transparent; 
             background-clip: text;">
  标题文字
</span>

是的,要写 inline style。这是我们唯一允许的例外。

坑二:暗色模式不要硬编码颜色

/* ❌ 硬编码,改版噩梦 */
.card { background-color: #0F172A; }

/* ✅ CSS变量,暗色模式预留 */
:root {
  --color-bg-card: #FFFFFF;
}
.dark {
  --color-bg-card: #0F172A;
}
.card { background-color: var(--color-bg-card); }

在 Tailwind config 里也可以映射:

// tailwind.config.js
theme: {
  extend: {
    colors: {
      'bg-card': 'var(--color-bg-card)',
    }
  }
}

然后用 bg-bg-card 这个 class,改主题只需改 CSS 变量。

坑三:fade-in 动画在手机端别依赖 IntersectionObserver

我们用滚动动画,在桌面端完美,手机端用户怎么滑都不触发。根因是某些低端机型 IntersectionObserver 回调时机不一致。

铁律:默认可见,JS 增强

/* ✅ 默认可见 */
.reveal { opacity: 1; transition: opacity 0.5s; }

/* JS加持时才隐藏 */
.reveal.js-ready { opacity: 0; }
.reveal.js-ready.is-visible { opacity: 1; }

多语言:每个文字都必须走 $t()

我们的项目要支持中文、英文、繁体中文三语。最惨的翻车发生在早期:某个页面有几个写死的中文按钮文案没有走 i18n,结果英文版页面出现了中文。老板截图说"英文版有中文",全场沉默。

规范:

<!-- ❌ 硬编码中文 -->
<button>立即开始</button>

<!-- ✅ 走i18n -->
<button>{{ $t('common.cta.start') }}</button>

i18n 文件按页面模块组织:

// zh.json
{
  "common": {
    "cta": {
      "start": "立即开始",
      "learn": "了解更多"
    }
  },
  "pricing": {
    "title": "价格方案",
    "monthly": "按月计费"
  }
}

语言切换用 localStorage 持久化,不要每次刷新都回到默认语言。


SEO:SPA 的死穴和解法

Vue 3 SPA 的最大问题是 SEO。搜索引擎爬虫拿到的是空 HTML,JS 还没跑。

我们的解法分两步:

短期:vite-plugin-prerender

对关键页面(首页、产品页、价格页)做静态预渲染,生成带内容的 HTML 文件。

// vite.config.js
import { viteStaticCopy } from 'vite-plugin-static-copy'
import PrerenderPlugin from 'vite-plugin-prerender'

export default defineConfig({
  plugins: [
    PrerenderPlugin({
      routes: ['/', '/pricing', '/features', '/about'],
      renderer: '@prerenderer/renderer-puppeteer'
    })
  ]
})

每个页面必须有 head 管理:

// 用 @unhead/vue
import { useHead } from '@unhead/vue'

useHead({
  title: computed(() => t('page.pricing.title') + ' | SmallFireDragon'),
  meta: [
    { name: 'description', content: computed(() => t('page.pricing.meta')) },
    { property: 'og:title', content: computed(() => t('page.pricing.title')) }
  ]
})

长期:Nuxt 3

如果项目规模增大,SEO 要求高,就迁移 Nuxt 3 SSR。我们的技术评估报告里已经有完整的迁移路径,分为 4 个阶段。


设计规范先行,开发后跟

这条是最贵的教训。

早期我们让前端开发"按效果图写",没有设计规范文档。结果?同一个项目里,间距有 8px 有 12px 有 16px,字体大小有 14px 有 15px 有 16px,按钮圆角有 4px 有 6px 有 8px——没有一处一致的。

铁律:先出设计规范,再写一行代码。

设计规范必须包含:

## 颜色系统
- Primary: #1B3A6B(品牌蓝)
- Accent: #10B981(绿色,CTR按钮)
- 背景: #FFFFFF / #F8FAFC
- 文字: #111827 / #6B7280

## 间距系统
- 基础单位:4px
- 常用:8 / 12 / 16 / 24 / 32 / 48px
- 组件内边距:16px
- 卡片间距:24px

## 字体系统
- 标题:Inter 700,24/32/40px
- 正文:Inter 400,16px,行高1.6
- 辅助:Inter 500,14px

## 组件规范
- 按钮圆角:8px
- 卡片阴影:0 1px 3px rgba(0,0,0,0.1)
- 过渡:0.2s ease

有了这份文档,前端按图索骥,设计验收时对照规范,不再靠感觉。


API 对接:统一规范,统一拦截

// src/utils/request.js
import { ofetch } from 'ofetch'

const api = ofetch.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  headers: {
    'Content-Type': 'application/json'
  },
  onRequest({ options }) {
    const token = localStorage.getItem('token')
    if (token) {
      options.headers.Authorization = `Bearer ${token}`
    }
  },
  onResponseError({ response }) {
    if (response.status === 401) {
      // 统一跳转登录
      router.push('/login')
    }
  }
})

export default api

API 响应格式统一:

{ "success": true, "data": { ... } }
{ "success": false, "error": "Invalid token" }

前端拦截器统一处理 success: false 的弹窗提示,业务代码只需关心 data


构建与部署的 Nginx 一条铁律

Vue Router 用 history 模式,用户直接访问 /pricing 时 Nginx 会 404,因为服务器上没有这个文件。

location / {
    try_files $uri $uri/ /index.html;
}

这条配置不加,SPA 部署必翻车。每次重新部署都要检查这一条。


最后说一句

规范不是一天写出来的,是一条一条被坑出来的。从"渐变文字显色块"到"英文页面有中文",每一条背后都有一次老板的截图或者一次沉默的群聊。

我们把这些经验写成规范,写成 frontend-dev-standard.md,强制全团队遵守。效果立竿见影——新项目接手的人不需要从头踩坑,直接照规范来就行。

这篇文章就是那份规范的白话版。

你能避开的坑,就不要去踩一遍。