03 / 06 · Продвинутый

💚 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

⚠️
Частая ошибка новичков: Claude Code может сгенерировать код с прямым обращением к localStorage, window или document — особенно если он написан без учёта SSR-контекста Nuxt. Такой код упадёт на сервере с ошибкой «window is not defined». Всегда оборачивайте browser-специфичный код в if (import.meta.client) {} и проверяйте каждый сгенерированный компонент перед деплоем.
Нарушение 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 логики
⚠️
Ловушка для продвинутых — ошибка гидрации (Hydration Mismatch): если компонент рендерится по-разному на сервере и клиенте, Vue выбросит предупреждение и перерисует компонент с нуля — что уничтожит SEO-преимущество SSR. Частые причины: Date.now() в шаблоне, Math.random(), условие на typeof window !== 'undefined' непосредственно в шаблоне, разные данные от API при первой загрузке. Решение: использовать <ClientOnly> компонент для нестабильных частей UI, либо useHydration() composable.
💡
Тонкий момент — useFetch vs $fetch: оба делают HTTP-запросы, но работают по-разному. useFetch()composable, который работает и на сервере (SSR) и на клиенте, дедуплицирует запросы и интегрируется с Nuxt payload. $fetch — простая обёртка над ofetch, всегда выполняется там где вызывается (на сервере при SSR — плохо для клиент-специфичных запросов). Правило: для данных которые нужны при первой загрузке — только useFetch или useAsyncData.

🍍 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', } } ]