Vue.js est un framework JavaScript progressif pour construire des interfaces utilisateur. Composants réutilisables, réactivité, Vue Router pour SPA, Vuex/Pinia pour state management. Vue 3 apporte Composition API et meilleures performances.

Installation et Setup

Bash
# Créer projet avec Vite (rapide)
npm create vite@latest mon-app -- --template vue
cd mon-app
npm install
npm run dev

# Ou avec Vue CLI
npm install -g @vue/cli
vue create mon-app
cd mon-app
npm run serve

# Ajouter Vue Router
npm install vue-router@4

# Ajouter Pinia (state management)
npm install pinia

Composants Vue

Structure d'un composant

Vue
<template>
  <div class="hello">
    <h1>{{ message }}</h1>
    <button @click="increment">Count: {{ count }}</button>
    <p>Double: {{ doubleCount }}</p>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      message: 'Bonjour Vue!',
      count: 0
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<style scoped>
.hello {
  padding: 20px;
}

h1 {
  color: #42b983;
}
</style>

Composition API (Vue 3)

Vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="increment">Count: {{ count }}</button>
    <p>Double: {{ doubleCount }}</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const message = ref('Bonjour Vue!')
const count = ref(0)

const doubleCount = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

Directives Vue

Vue
<template>
  <!-- v-bind : lier attribut -->
  <img :src="imageUrl" :alt="imageAlt">
  <div :class="{ active: isActive }"></div>
  <div :style="{ color: textColor, fontSize: size + 'px' }"></div>

  <!-- v-on @ : événements -->
  <button @click="handleClick">Cliquer</button>
  <input @input="handleInput" @keyup.enter="submit">
  <form @submit.prevent="onSubmit"></form>

  <!-- v-model : two-way binding -->
  <input v-model="username" type="text">
  <textarea v-model="message"></textarea>
  <input v-model="checked" type="checkbox">
  <select v-model="selected">
    <option value="a">A</option>
    <option value="b">B</option>
  </select>

  <!-- v-if / v-else / v-show -->
  <p v-if="isVisible">Visible</p>
  <p v-else>Caché</p>
  <p v-show="showElement">Toggle display</p>

  <!-- v-for : boucles -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>

  <!-- v-for avec index -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }}: {{ item.name }}
  </li>

  <!-- v-for sur objet -->
  <div v-for="(value, key) in user" :key="key">
    {{ key }}: {{ value }}
  </div>
</template>

Props et Events

Composant enfant avec props

Vue
<!-- UserCard.vue -->
<template>
  <div class="card">
    <h3>{{ name }}</h3>
    <p>{{ email }}</p>
    <button @click="handleClick">Contacter</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

// Props
const props = defineProps({
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true
  },
  age: {
    type: Number,
    default: 18
  }
})

// Events
const emit = defineEmits(['contact'])

function handleClick() {
  emit('contact', { name: props.name, email: props.email })
}
</script>

Composant parent

Vue
<template>
  <UserCard
    name="Nicolas Lema"
    email="nicolas@example.com"
    :age="25"
    @contact="onContact"
  />
</template>

<script setup>
import UserCard from './UserCard.vue'

function onContact(user) {
  console.log('Contact:', user)
}
</script>

Cycle de Vie

Vue
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'

// Avant montage (équivalent created)
console.log('Setup s\'exécute')

// Après montage DOM
onMounted(() => {
  console.log('Composant monté')
  // Appels API, initialisation
  fetchData()
})

// Après mise à jour
onUpdated(() => {
  console.log('Composant mis à jour')
})

// Avant démontage
onUnmounted(() => {
  console.log('Composant démonté')
  // Nettoyage: event listeners, timers...
})
</script>

Watchers

Vue
<script setup>
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const user = ref({ name: 'Nicolas', age: 25 })

// Watch simple
watch(searchQuery, (newVal, oldVal) => {
  console.log(`Query changed from ${oldVal} to ${newVal}`)
  performSearch(newVal)
})

// Watch avec options
watch(searchQuery, (newVal) => {
  performSearch(newVal)
}, {
  immediate: true,  // Exécuter immédiatement
  deep: true        // Watch profond (objets/arrays)
})

// Watch objet (deep)
watch(user, (newUser) => {
  console.log('User changed:', newUser)
}, { deep: true })

// watchEffect: exécute immédiatement et re-exécute si dépendances changent
watchEffect(() => {
  console.log('Search:', searchQuery.value)
  // Se ré-exécute automatiquement si searchQuery change
})
</script>

Vue Router

Configuration

JavaScript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import UserProfile from '@/views/UserProfile.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/user/:id',
    name: 'UserProfile',
    component: UserProfile,
    props: true  // Passer params comme props
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),  // Lazy loading
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guard
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

export default router

Utilisation

Vue
<template>
  <nav>
    <!-- Liens navigation -->
    <router-link to="/">Accueil</router-link>
    <router-link :to="{ name: 'About' }">À propos</router-link>
    <router-link :to="`/user/${userId}`">Profil</router-link>
  </nav>

  <!-- Vue chargée ici -->
  <router-view />
</template>

<script setup>
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

// Navigation programmatique
function goToUser(id) {
  router.push(`/user/${id}`)
  // OU
  router.push({ name: 'UserProfile', params: { id } })
}

// Accéder aux params
const userId = route.params.id

// Accéder aux query params
const page = route.query.page
</script>

State Management avec Pinia

Créer un store

JavaScript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isAuthenticated = computed(() => !!token.value)
  const userName = computed(() => user.value?.name)

  // Actions
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials)
    })
    
    const data = await response.json()
    
    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isAuthenticated,
    userName,
    login,
    logout
  }
})

Utiliser le store

Vue
<template>
  <div v-if="userStore.isAuthenticated">
    <p>Bonjour {{ userStore.userName }}</p>
    <button @click="userStore.logout">Déconnexion</button>
  </div>
  <div v-else>
    <button @click="handleLogin">Connexion</button>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

async function handleLogin() {
  await userStore.login({
    email: 'user@example.com',
    password: 'password'
  })
}
</script>

Appels API

Vue
<template>
  <div v-if="loading">Chargement...</div>
  <div v-else-if="error">Erreur: {{ error }}</div>
  <div v-else>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(false)
const error = ref(null)

async function fetchUsers() {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch('/api/users')
    
    if (!response.ok) {
      throw new Error('Erreur réseau')
    }
    
    users.value = await response.json()
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchUsers()
})
</script>

Composable réutilisable

JavaScript
// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  async function fetch() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(url)
      if (!response.ok) throw new Error(response.statusText)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  return { data, loading, error, fetch }
}

// Utilisation
const { data: users, loading, error, fetch } = useFetch('/api/users')
fetch()

Bonnes Pratiques Vue

Composition API

Utiliser Composition API (script setup) pour meilleure réutilisabilité

Composables

Extraire logique réutilisable dans composables (use*)

Props validation

Toujours définir types et required pour props

Keys unique

:key unique dans v-for (pas index si modifiable)

Scoped styles

Utiliser scoped pour éviter conflits CSS

Lazy loading

Routes lazy-loadées pour meilleures performances