💚 Vue 3 & Nuxt 3
Работа с Vue 3 (Composition API) и Nuxt 3 (SSR) через Claude Code: chrome-devtools MCP, SSR-правила, Pinia, VeeValidate, Vitest и Playwright e2e тесты.
🌐 chrome-devtools MCP — UI инспекция
Для работы над UI-компонентами Claude Code использует chrome-devtools MCP вместо чтения HTML/CSS файлов вслепую. Это позволяет видеть реальный DOM, CSS и консоль.
НИКОГДА не смешивать chrome-devtools и Playwright в одной сессии! chrome-devtools — для UI-разработки. Playwright — только для e2e тестов в отдельной сессии. Смешение приводит к конфликту портов и зависанию.
// .mcp.json для Nuxt/Vue проекта
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest"],
"_comment": "UI-работа: инспекция, снимки, консоль. НЕ смешивать с playwright!"
}
}
}
Workflow UI разработки
// 1. Запустить dev сервер:
mcp__docker__docker_exec({
container: "nuxt-frontend-1",
command: "pnpm dev"
})
// или напрямую: pnpm dev (порт 3000)
// 2. Открыть Chrome с dev tools MCP
// chrome --remote-debugging-port=9222
// 3. Инспекция DOM через MCP:
mcp__chrome-devtools__inspect({ selector: ".payment-form" })
// 4. Скриншот для проверки:
mcp__chrome-devtools__screenshot({ path: "E:/tmp/payment-form.png" })
// 5. Консольные ошибки:
mcp__chrome-devtools__console_errors()
⚡ Vue 3 Composition API — стандарты
Правильная структура SFC
<!-- components/PaymentForm.vue -->
<script setup lang="ts">
// Порядок: imports → defineProps → defineEmits → реактивность → computed → methods → lifecycle
import { ref, computed, onMounted } from 'vue'
import { usePaymentStore } from '~/stores/payment'
// TypeScript interface для props
interface Props {
initialAmount?: number
currency: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
submit: [amount: number]
cancel: []
}>()
const store = usePaymentStore()
const amount = ref(props.initialAmount ?? 0)
const formattedAmount = computed(() =>
new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: props.currency
}).format(amount.value)
)
function handleSubmit() {
emit('submit', amount.value)
}
onMounted(() => {
store.fetchRates()
})
</script>
<template>
<!-- CSS только Tailwind — никаких кастомных классов -->
<form @submit.prevent="handleSubmit" class="flex flex-col gap-4">
<input v-model="amount" type="number" class="border rounded px-3 py-2" />
<span class="text-gray-500">{{ formattedAmount }}</span>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Оплатить</button>
</form>
</template>
🌐 SSR — критические правила Nuxt 3
❌ Нарушение SSR
// Это упадёт на сервере!
const token = localStorage.getItem('token')
const w = window.innerWidth
const ua = navigator.userAgent
✅ SSR-безопасно
// С guard для клиента:
if (import.meta.client) {
const token = localStorage.getItem('token')
}
// или через useRuntimeConfig:
const config = useRuntimeConfig()
| Правило | Плохо | Хорошо |
|---|---|---|
| Browser API | localStorage, window, document |
Обернуть в if (import.meta.client) {} |
| Env переменные | process.env.API_URL |
useRuntimeConfig().apiUrl |
| Composables | Прямые fetch без хука | useFetch() или useAsyncData() |
| Lifecycle hooks | mounted для данных |
onMounted только для browser-specific логики |
🍍 Pinia — store паттерн
// stores/payment.ts
import { defineStore } from 'pinia'
import type { Payment } from '~/types'
// Composition API синтаксис (не Options API!)
export const usePaymentStore = defineStore('payment', () => {
const payments = ref<Payment[]>([])
const loading = ref(false)
const total = computed(() =>
payments.value.reduce((sum, p) => sum + p.amount, 0)
)
async function fetchPayments() {
loading.value = true
try {
payments.value = await $fetch('/api/v1/payments')
} finally {
loading.value = false
}
}
return { payments, loading, total, fetchPayments }
})
🧪 Vitest — тестирование компонентов
// tests/PaymentForm.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import PaymentForm from '~/components/PaymentForm.vue'
describe('PaymentForm', () => {
it('emits submit with correct amount', async () => {
// Arrange
const wrapper = mount(PaymentForm, {
props: { currency: 'RUB', initialAmount: 1000 }
})
// Act
await wrapper.find('form').trigger('submit')
// Assert
expect(wrapper.emitted('submit')?.[0]).toEqual([1000])
})
})
Playwright — только для e2e. Vitest + @vue/test-utils — для unit/component тестов. Playwright запускается в отдельной сессии Claude, без chrome-devtools MCP.
📏 ESLint Flat Config
// eslint.config.js — flat config (v9+)
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
export default [
...pluginVue.configs['flat/recommended'],
...vueTsConfigs.recommended,
{
rules: {
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'error', // XSS защита
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
}
}
]