20 KiB
20 KiB
Vue.js 生態系工具配置指南
🛠️ 開發工具配置
VS Code 配置
推薦擴展插件
// .vscode/extensions.json
{
"recommendations": [
// Vue.js 開發核心
"Vue.volar", // Vue Language Features
"Vue.vscode-typescript-vue-plugin", // TypeScript Vue Plugin
// 程式碼品質
"esbenp.prettier-vscode", // Prettier代碼格式化
"dbaeumer.vscode-eslint", // ESLint檢查
"stylelint.vscode-stylelint", // CSS/SCSS檢查
// TypeScript支援
"ms-vscode.vscode-typescript-next", // TypeScript語言服務
// 開發輔助
"formulahendry.auto-rename-tag", // 自動重命名標籤
"bradlc.vscode-tailwindcss", // Tailwind CSS支援
"christian-kohler.path-intellisense", // 路徑智能提示
"ms-vscode.vscode-json", // JSON支援
// Git工具
"eamodio.gitlens", // Git增強
"mhutchie.git-graph", // Git圖形化
// 測試工具
"ZixuanChen.vitest-explorer", // Vitest測試瀏覽器
// 其他實用工具
"PKief.material-icon-theme", // 圖標主題
"GitHub.copilot", // AI編程助手
"ms-vscode.live-server" // Live Server
]
}
VS Code設定
// .vscode/settings.json
{
// 編輯器設定
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
// Vue.js相關設定
"vue.inlayHints.missingProps": true,
"vue.inlayHints.inlineHandlerLeading": true,
"vue.inlayHints.optionsWrapper": true,
// TypeScript設定
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
// Prettier設定
"prettier.configPath": ".prettierrc",
"prettier.requireConfig": true,
// ESLint設定
"eslint.validate": ["vue", "typescript", "javascript"],
"eslint.format.enable": true,
// 檔案關聯
"files.associations": {
"*.vue": "vue"
},
// Emmet設定
"emmet.includeLanguages": {
"vue-html": "html",
"vue": "html"
},
// 自動儲存
"files.autoSave": "onFocusChange",
// 排除檔案
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/.nuxt": true,
"**/coverage": true
},
// 終端機設定
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.fontSize": 14
}
Prettier 配置
// .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "none",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore",
"overrides": [
{
"files": "*.vue",
"options": {
"parser": "vue"
}
},
{
"files": "*.json",
"options": {
"parser": "json"
}
}
]
}
StyleLint 配置
// .stylelintrc.json
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-standard-vue"
],
"rules": {
"string-quotes": "single",
"selector-class-pattern": null,
"custom-property-pattern": null,
"scss/dollar-variable-pattern": null,
"scss/at-mixin-pattern": null,
"scss/at-function-pattern": null,
"value-keyword-case": null,
"function-name-case": null,
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"variants",
"responsive",
"screen"
]
}
]
},
"ignoreFiles": [
"node_modules/**/*",
"dist/**/*",
"coverage/**/*"
]
}
Husky Git Hooks 配置
安裝和配置
# 安裝 Husky
npm install --save-dev husky lint-staged
# 初始化 Husky
npx husky install
# 創建 pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
# 創建 commit-msg hook (選用)
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
lint-staged 配置
// package.json 中的 lint-staged 配置
{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,vue}": [
"stylelint --fix",
"prettier --write"
],
"*.{md,json}": [
"prettier --write"
]
}
}
Commitlint 配置 (選用)
// .commitlintrc.json
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat", // 新功能
"fix", // bug修復
"docs", // 文檔
"style", // 格式化
"refactor", // 重構
"perf", // 性能優化
"test", // 測試
"build", // 建構
"ci", // CI/CD
"chore", // 其他
"revert" // 回退
]
],
"subject-case": [0]
}
}
🧪 測試工具配置
Vitest 配置
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
test: {
// 測試環境
globals: true,
environment: 'happy-dom',
// 設置檔案
setupFiles: ['tests/setup.ts'],
// 包含和排除
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
// 覆蓋率配置
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary', 'html', 'lcov'],
reportsDirectory: './coverage',
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*',
'src/main.ts',
'src/router/index.ts'
],
threshold: {
global: {
branches: 75,
functions: 75,
lines: 75,
statements: 75
}
}
},
// 監控模式
watchExclude: ['**/node_modules/**', '**/dist/**'],
// 測試超時
testTimeout: 10000,
hookTimeout: 10000,
// 並發設置
threads: true,
maxThreads: 4,
minThreads: 1
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'~': path.resolve(__dirname, 'src')
}
}
})
測試設置檔案
// tests/setup.ts
import { config } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { Quasar } from 'quasar'
// Vue Test Utils 全域配置
config.global.plugins = [
createPinia(),
[Quasar, {}]
]
// 模擬 ResizeObserver (Quasar需要)
global.ResizeObserver = class ResizeObserver {
constructor(cb: any) {}
observe() {}
unobserve() {}
disconnect() {}
}
// 模擬 IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor(cb: any, options?: any) {}
observe() {}
unobserve() {}
disconnect() {}
}
// 模擬 matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}))
})
Cypress E2E 測試配置
// cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: true,
setupNodeEvents(on, config) {
// 任務配置
on('task', {
log(message) {
console.log(message)
return null
}
})
},
// 測試檔案配置
specPattern: 'tests/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'tests/e2e/support/e2e.ts',
fixturesFolder: 'tests/e2e/fixtures',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
// 環境變數
env: {
apiUrl: 'http://localhost:5000/api',
testUser: {
email: 'test@example.com',
password: 'testpassword123'
}
}
},
component: {
devServer: {
framework: 'vue',
bundler: 'vite'
}
}
})
📦 建構和部署工具
GitHub Actions CI/CD
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
jobs:
# 代碼檢查和測試
quality-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint check
run: npm run lint
- name: Style lint check
run: npm run lint:style
- name: Run unit tests
run: npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
# E2E測試
e2e-test:
runs-on: ubuntu-latest
needs: quality-check
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Run E2E tests
uses: cypress-io/github-action@v6
with:
start: npm run preview
wait-on: 'http://localhost:4173'
# 建構和部署
build-deploy:
runs-on: ubuntu-latest
needs: quality-check
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
VITE_API_BASE_URL: ${{ secrets.PRODUCTION_API_URL }}
VITE_STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }}
- name: Build Docker image
run: docker build -t dramaling-web:${{ github.sha }} .
- name: Deploy to production
run: |
echo "Deploying to production server..."
# 部署邏輯
Docker 配置
開發環境 Dockerfile
# Dockerfile.dev
FROM node:18-alpine
WORKDIR /app
# 安裝依賴
COPY package*.json ./
RUN npm ci
# 複製源代碼
COPY . .
# 開放端口
EXPOSE 3000
# 開發模式啟動
CMD ["npm", "run", "dev"]
生產環境 Dockerfile
# Dockerfile
# 建構階段
FROM node:18-alpine AS builder
WORKDIR /app
# 複製依賴文件
COPY package*.json ./
# 安裝依賴(包含 devDependencies)
RUN npm ci
# 複製源代碼
COPY . .
# 建構應用
RUN npm run build
# 生產階段
FROM nginx:alpine AS production
# 安裝工具
RUN apk add --no-cache curl
# 複製建構結果
COPY --from=builder /app/dist /usr/share/nginx/html
# 複製 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# 非 root 用戶
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nextjs -u 1001
USER nodejs
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
環境配置管理
開發環境配置
# scripts/dev-setup.sh
#!/bin/bash
echo "🔧 設置開發環境..."
# 檢查 Node.js 版本
node_version=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$node_version" -lt 18 ]; then
echo "❌ Node.js 版本需要 >= 18,當前版本: $(node -v)"
exit 1
fi
# 安裝依賴
echo "📦 安裝依賴..."
npm ci
# 設置 Git hooks
echo "🪝 設置 Git hooks..."
npm run prepare
# 創建環境變數檔案
if [ ! -f .env.local ]; then
echo "📝 創建本地環境變數檔案..."
cp .env.development .env.local
fi
# 運行初始化檢查
echo "🧪 運行檢查..."
npm run type-check
npm run lint
npm run test --run
echo "✅ 開發環境設置完成!"
echo "運行 'npm run dev' 開始開發"
部署腳本
# scripts/deploy.sh
#!/bin/bash
set -e
echo "🚀 開始部署流程..."
# 環境變數檢查
if [ -z "$PRODUCTION_API_URL" ]; then
echo "❌ 缺少 PRODUCTION_API_URL 環境變數"
exit 1
fi
# 建構應用
echo "🏗️ 建構應用..."
npm run build
# 運行測試
echo "🧪 運行測試..."
npm run test --run
# Docker 建構
echo "🐳 建構 Docker 映像..."
docker build -t dramaling-web:latest .
# 部署到服務器
echo "🌐 部署到生產環境..."
# 這裡會是實際的部署命令
echo "✅ 部署完成!"
🔧 開發工具腳本
package.json 腳本
{
"scripts": {
// 開發相關
"dev": "vite --host",
"dev:https": "vite --https --host",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
// 測試相關
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest --coverage",
"test:e2e": "cypress run",
"test:e2e:dev": "cypress open",
// 代碼檢查
"lint": "eslint . --ext .vue,.ts,.tsx --fix",
"lint:check": "eslint . --ext .vue,.ts,.tsx",
"lint:style": "stylelint **/*.{css,scss,vue} --fix",
"type-check": "vue-tsc --noEmit",
// 建構和部署
"build:analyze": "vite build --mode analyze",
"build:docker": "docker build -t dramaling-web .",
"deploy:staging": "npm run build && ./scripts/deploy-staging.sh",
"deploy:production": "npm run build && ./scripts/deploy-production.sh",
// 開發工具
"clean": "rm -rf node_modules dist coverage .nuxt",
"fresh-install": "npm run clean && npm install",
"update-deps": "npx npm-check-updates -u && npm install",
"prepare": "husky install",
// 代碼生成
"generate:component": "node scripts/generate-component.js",
"generate:page": "node scripts/generate-page.js",
"generate:store": "node scripts/generate-store.js"
}
}
開發輔助腳本
組件生成腳本
// scripts/generate-component.js
const fs = require('fs')
const path = require('path')
const componentName = process.argv[2]
if (!componentName) {
console.log('請提供組件名稱: npm run generate:component ComponentName')
process.exit(1)
}
const componentDir = path.join('src/components/business', componentName)
const componentFile = path.join(componentDir, `${componentName}.vue`)
const testFile = path.join('tests/unit/components', `${componentName}.test.ts`)
// 創建目錄
if (!fs.existsSync(componentDir)) {
fs.mkdirSync(componentDir, { recursive: true })
}
// 組件模板
const componentTemplate = `<template>
<div class="${componentName.toLowerCase()}">
<h2>{{ title }}</h2>
<slot />
</div>
</template>
<script setup lang="ts">
interface Props {
title: string
}
const props = defineProps<Props>()
</script>
<style scoped lang="scss">
.${componentName.toLowerCase()} {
// 樣式
}
</style>
`
// 測試模板
const testTemplate = `import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import ${componentName} from '@/components/business/${componentName}/${componentName}.vue'
describe('${componentName}', () => {
it('renders correctly', () => {
const wrapper = mount(${componentName}, {
props: {
title: 'Test Title'
}
})
expect(wrapper.find('h2').text()).toBe('Test Title')
})
})
`
// 創建文件
fs.writeFileSync(componentFile, componentTemplate)
fs.writeFileSync(testFile, testTemplate)
console.log(`✅ 組件 ${componentName} 創建成功!`)
console.log(`📁 組件文件: ${componentFile}`)
console.log(`🧪 測試文件: ${testFile}`)
📊 性能監控配置
Bundle 分析配置
// vite.config.ts 中的 bundle 分析
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
// 其他插件...
// Bundle 分析器
process.env.ANALYZE === 'true' && visualizer({
filename: 'dist/bundle-analysis.html',
open: true,
gzipSize: true,
brotliSize: true
})
].filter(Boolean)
})
性能監控
// src/utils/performance.ts
export class PerformanceMonitor {
private static instance: PerformanceMonitor
static getInstance() {
if (!this.instance) {
this.instance = new PerformanceMonitor()
}
return this.instance
}
// 頁面載入性能
measurePageLoad(routeName: string) {
if (typeof window === 'undefined') return
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
const metrics = {
route: routeName,
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: this.getFirstPaint(),
firstContentfulPaint: this.getFirstContentfulPaint()
}
this.sendMetrics('page_load', metrics)
}
// 用戶互動性能
measureInteraction(actionName: string, startTime: number) {
const duration = performance.now() - startTime
this.sendMetrics('user_interaction', {
action: actionName,
duration
})
}
// 獲取 Web Vitals
private getFirstPaint(): number {
const paintEntries = performance.getEntriesByType('paint')
const fpEntry = paintEntries.find(entry => entry.name === 'first-paint')
return fpEntry ? fpEntry.startTime : 0
}
private getFirstContentfulPaint(): number {
const paintEntries = performance.getEntriesByType('paint')
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint')
return fcpEntry ? fcpEntry.startTime : 0
}
private sendMetrics(type: string, data: any) {
// 發送到分析服務
console.log(`[Performance] ${type}:`, data)
// 實際實作可以發送到 Google Analytics、Mixpanel 等
if (window.gtag) {
window.gtag('event', type, data)
}
}
}
🔗 IDE整合配置
WebStorm 配置
<!-- .idea/jsLibraryMappings.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{dramaling-web/node_modules}" />
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>
代碼模板
// .vscode/vue.code-snippets
{
"Vue Composition Component": {
"prefix": "vcomp",
"body": [
"<template>",
" <div class=\"${1:component-name}\">",
" ${2}",
" </div>",
"</template>",
"",
"<script setup lang=\"ts\">",
"interface Props {",
" ${3}",
"}",
"",
"const props = defineProps<Props>()",
"${4}",
"</script>",
"",
"<style scoped lang=\"scss\">",
".${1:component-name} {",
" ${5}",
"}",
"</style>"
],
"description": "Vue Composition API component template"
},
"Pinia Store": {
"prefix": "pstore",
"body": [
"import { defineStore } from 'pinia'",
"",
"export const use${1:StoreName}Store = defineStore('${2:storeName}', () => {",
" const ${3:state} = ref(${4:initialValue})",
" ",
" const ${5:getter} = computed(() => {",
" return ${3:state}.value",
" })",
" ",
" const ${6:action} = async () => {",
" ${7}",
" }",
" ",
" return {",
" ${3:state}: readonly(${3:state}),",
" ${5:getter},",
" ${6:action}",
" }",
"})"
],
"description": "Pinia store template"
}
}
文檔狀態: 🟢 完成工具配置指南
最後更新: 2025-09-09
下次更新: 根據開發需求調整工具配置