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 變數。
坑三:淡入動畫在手機端別依賴 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 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,
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,強制全團隊遵守。效果立竿見影——新專案接手的人不需要從頭踩坑,直接照規範來就行。
這篇文章就是那份規範的白話版。
你能避開的坑,就不要去踩一遍。