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/
几条强制规定:
- 组件文件用 PascalCase(
ProductCard.vue),路由和CSS class用 kebab-case(product-detail) - composable 统一
use前缀(useProducts.js) - i18n key 用点号分层(
product.title,不是productTitle) - 超过 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,强制全团队遵守。效果立竿见影——新项目接手的人不需要从头踩坑,直接照规范来就行。
这篇文章就是那份规范的白话版。
你能避开的坑,就不要去踩一遍。