Aller au contenu

Nuxt.js Feuille de chaleur

Nuxt.js - Le cadre de vision intuitive

Nuxt.js est un cadre libre et open-source avec un moyen intuitif et extensible de créer des applications web et des sites Web de type sûr, performant et de qualité de production avec Vue.js.

Copier toutes les commandes Générer PDF

Sommaire

  • [Installation] (LINK_0)
  • Création de projets
  • Structure du projet
  • [Route] (LINK_0)
  • [Pages et mises en page] (LINK_0)
  • [Composants] (LINK_0)
  • [Recherche de données] (LINK_0)
  • [Gestion de l'État] (LINK_0)
  • [Stylage] (LINK_0)
  • [Plugins] (LINK_0)
  • [Modules] (LINK_0)
  • [Appel d'offres à l'aide d'un serveur] (LINK_0)
  • [itinéraires API] (LINK_0)
  • [Déploiement] (LINK_0)
  • [Tests] (LINK_0)
  • [Optimisation du rendement] (LINK_0)
  • [Dépannage] (LINK_0)
  • [Meilleures pratiques] (LINK_0)

Installation

Préalables

# Node.js version requirements
node --version  # Should be 16.10 or later

# Check npm version
npm --version

# Update npm if needed
npm install -g npm@latest

Créer l'application Nuxt.js

# Create new Nuxt.js app
npx nuxi@latest init my-app

# Navigate to project
cd my-app

# Install dependencies
npm install

# Start development server
npm run dev
```_

### Autre installation Méthodes
```bash
# Using yarn
yarn create nuxt-app my-app

# Using pnpm
pnpm dlx nuxi@latest init my-app

# Using specific template
npx nuxi@latest init my-app --template ui
npx nuxi@latest init my-app --template content
npx nuxi@latest init my-app --template module
```_

### Installation manuelle
```bash
# Create project directory
mkdir my-nuxt-app
cd my-nuxt-app

# Initialize package.json
npm init -y

# Install Nuxt.js
npm install nuxt

# Install Vue dependencies
npm install vue@latest

# Add scripts to package.json

Paquet.json Scripts

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  }
}

Création de projets

Modèles de démarrage

# Official templates
npx nuxi@latest init my-app --template ui          # Nuxt UI
npx nuxi@latest init my-app --template content     # Nuxt Content
npx nuxi@latest init my-app --template module      # Nuxt Module
npx nuxi@latest init my-app --template layer       # Nuxt Layer

# Community templates
npx nuxi@latest init my-app --template docus       # Documentation
npx nuxi@latest init my-app --template minimal     # Minimal setup

Commandes de développement

# Start development server
npm run dev

# Start with specific port
npm run dev -- --port 3001

# Start with specific host
npm run dev -- --host 0.0.0.0

# Start with HTTPS
npm run dev -- --https

# Start with tunnel
npm run dev -- --tunnel

Construire des commandes

# Build for production
npm run build

# Generate static site
npm run generate

# Preview production build
npm run preview

# Analyze bundle
npm run build -- --analyze

Structure du projet

Structure Nuxt 3

my-nuxt-app/
├── .nuxt/                 # Auto-generated files
├── .output/               # Build output
├── assets/                # Uncompiled assets
├── components/            # Vue components
├── composables/           # Vue composables
├── content/               # Content files (if using @nuxt/content)
├── layouts/               # Layout components
├── middleware/            # Route middleware
├── pages/                 # File-based routing
├── plugins/               # Plugins
├── public/                # Static files
├── server/                # Server-side code
│   ├── api/              # API routes
│   └── middleware/       # Server middleware
├── stores/                # Pinia stores
├── utils/                 # Utility functions
├── app.vue               # Main app component
├── nuxt.config.ts        # Nuxt configuration
├── package.json
└── tsconfig.json

Fichiers de configuration

// nuxt.config.ts
export default defineNuxtConfig({
  // Basic configuration
  devtools: { enabled: true },

  // CSS framework
  css: ['~/assets/css/main.css'],

  // Modules
  modules: [
    '@nuxtjs/tailwindcss',
    '@pinia/nuxt',
    '@nuxt/content'
  ],

  // Runtime config
  runtimeConfig: {
    // Private keys (only available on server-side)
    apiSecret: '123',
    // Public keys (exposed to client-side)
    public: {
      apiBase: '/api'
    }
  },

  // App configuration
  app: {
    head: {
      title: 'My Nuxt App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' }
      ]
    }
  }
});

Routage

Routage basé sur des fichiers

# Route structure
pages/
├── index.vue             # / (root)
├── about.vue             # /about
├── blog/
│   ├── index.vue         # /blog
│   └── [slug].vue        # /blog/[slug]
├── users/
│   ├── index.vue         # /users
│   ├── [id].vue          # /users/[id]
│   └── [id]/
│       └── edit.vue      # /users/[id]/edit
└── [...slug].vue         # Catch-all route

Routes dynamiques

<!-- pages/blog/[slug].vue -->
<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

<script setup>
// Get route parameters
const route = useRoute();
const slug = route.params.slug;

// Fetch data based on slug
const { data: post } = await $fetch(`/api/posts/${slug}`);

// Define page meta
definePageMeta({
  title: 'Blog Post',
  description: 'Read our latest blog post'
});
</script>

Routes nichées

<!-- pages/users/[id].vue -->
<template>
  <div>
    <h1>User Profile</h1>
    <NuxtPage />
  </div>
</template>

<!-- pages/users/[id]/index.vue -->
<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

<!-- pages/users/[id]/edit.vue -->
<template>
  <div>
    <h2>Edit User</h2>
    <form @submit="updateUser">
      <!-- Form fields -->
    </form>
  </div>
</template>
<template>
  <nav>
    <!-- Static navigation -->
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>

    <!-- Dynamic navigation -->
    <NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>

    <!-- External links -->
    <NuxtLink to="https://nuxtjs.org" external>Nuxt.js</NuxtLink>

    <!-- Programmatic navigation -->
    <button @click="navigateTo('/dashboard')">Dashboard</button>
    <button @click="navigateTo('/login', { replace: true })">Login</button>
  </nav>
</template>

<script setup>
// Programmatic navigation
const router = useRouter();

function goToProfile() {
  router.push('/profile');
}

function goBack() {
  router.back();
}

// Navigation guards
definePageMeta({
  middleware: 'auth'
});
</script>

Route Middleware

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useAuthUser();

  if (!user.value) {
    return navigateTo('/login');
  }
});

// middleware/admin.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // Global middleware runs on every route change
  console.log('Navigating to:', to.path);
});

Pages et mises en page

Mise en page par défaut

<!-- layouts/default.vue -->
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
        <NuxtLink to="/blog">Blog</NuxtLink>
      </nav>
    </header>

    <main>
      <slot />
    </main>

    <footer>
      <p>&copy; 2024 My Nuxt App</p>
    </footer>
  </div>
</template>

<style scoped>
header {
  background: #f8f9fa;
  padding: 1rem;
}

nav a {
  margin-right: 1rem;
  text-decoration: none;
}

main {
  min-height: 80vh;
  padding: 2rem;
}

footer {
  background: #343a40;
  color: white;
  text-align: center;
  padding: 1rem;
}
</style>

Mise en page personnalisée

<!-- layouts/admin.vue -->
<template>
  <div class="admin-layout">
    <aside class="sidebar">
      <nav>
        <NuxtLink to="/admin">Dashboard</NuxtLink>
        <NuxtLink to="/admin/users">Users</NuxtLink>
        <NuxtLink to="/admin/posts">Posts</NuxtLink>
      </nav>
    </aside>

    <main class="content">
      <slot />
    </main>
  </div>
</template>

<style scoped>
.admin-layout {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: 250px;
  background: #2c3e50;
  color: white;
  padding: 1rem;
}

.content {
  flex: 1;
  padding: 2rem;
}
</style>

Utilisation des mises en page dans les pages

<!-- pages/admin/index.vue -->
<template>
  <div>
    <h1>Admin Dashboard</h1>
    <div class="stats">
      <div class="stat">
        <h3>Users</h3>
        <p>{{ stats.users }}</p>
      </div>
      <div class="stat">
        <h3>Posts</h3>
        <p>{{ stats.posts }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
// Use custom layout
definePageMeta({
  layout: 'admin',
  middleware: 'admin-auth'
});

// Fetch dashboard stats
const { data: stats } = await $fetch('/api/admin/stats');
</script>

Page Meta et référencement

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

<script setup>
const route = useRoute();
const { data: post } = await $fetch(`/api/posts/${route.params.slug}`);

// Dynamic page meta
useHead({
  title: post.title,
  meta: [
    { name: 'description', content: post.excerpt },
    { name: 'keywords', content: post.tags.join(', ') },
    { property: 'og:title', content: post.title },
    { property: 'og:description', content: post.excerpt },
    { property: 'og:image', content: post.image },
    { property: 'og:url', content: `https://example.com/blog/${post.slug}` }
  ]
});

// Structured data
useJsonld({
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.excerpt,
  author: {
    '@type': 'Person',
    name: post.author.name
  },
  datePublished: post.publishedAt
});
</script>

Composantes

Composants importés automatiquement

<!-- components/TheHeader.vue -->
<template>
  <header class="header">
    <div class="container">
      <NuxtLink to="/" class="logo">
        <img src="/logo.png" alt="Logo" />
      </NuxtLink>

      <nav class="nav">
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
        <NuxtLink to="/blog">Blog</NuxtLink>
      </nav>
    </div>
  </header>
</template>

<script setup>
// Component is auto-imported and can be used as <TheHeader />
</script>

Composants imbriqués

<!-- components/blog/PostCard.vue -->
<template>
  <article class="post-card">
    <img :src="post.image" :alt="post.title" />
    <div class="content">
      <h3>{{ post.title }}</h3>
      <p>{{ post.excerpt }}</p>
      <NuxtLink :to="`/blog/${post.slug}`" class="read-more">
        Read More
      </NuxtLink>
    </div>
  </article>
</template>

<script setup>
interface Post {
  id: string;
  title: string;
  excerpt: string;
  image: string;
  slug: string;
}

defineProps<{
  post: Post;
}>();
</script>

<!-- Usage: <BlogPostCard :post="post" /> -->

Composables

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState<User | null>('auth.user', () => null);

  const login = async (credentials: LoginCredentials) => {
    const { data } = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials
    });

    user.value = data.user;
    await navigateTo('/dashboard');
  };

  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' });
    user.value = null;
    await navigateTo('/');
  };

  const isAuthenticated = computed(() => !!user.value);

  return {
    user: readonly(user),
    login,
    logout,
    isAuthenticated
  };
};

// Usage in components
const { user, login, logout, isAuthenticated } = useAuth();

État mondial avec utilisationÉtat

// composables/useCounter.ts
export const useCounter = () => {
  const count = useState('counter', () => 0);

  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => count.value = 0;

  return {
    count: readonly(count),
    increment,
    decrement,
    reset
  };
};

Collecte de données

Récupération de données côté serveur

<template>
  <div>
    <h1>Posts</h1>
    <div v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.excerpt }}</p>
    </div>
  </div>
</template>

<script setup>
// Fetch data on server-side
const { data: posts } = await $fetch('/api/posts');

// Alternative with useFetch
const { data: posts, pending, error } = await useFetch('/api/posts');

// With reactive parameters
const page = ref(1);
const { data: posts } = await useFetch('/api/posts', {
  query: { page }
});
</script>

Collecte de données côté client

<template>
  <div>
    <button @click="loadPosts">Load Posts</button>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <div v-for="post in posts" :key="post.id">
        <h2>{{ post.title }}</h2>
      </div>
    </div>
  </div>
</template>

<script setup>
const posts = ref([]);
const pending = ref(false);
const error = ref(null);

const loadPosts = async () => {
  pending.value = true;
  error.value = null;

  try {
    posts.value = await $fetch('/api/posts');
  } catch (err) {
    error.value = err.message;
  } finally {
    pending.value = false;
  }
};
</script>

Récupération de données avancée

<script setup>
// Lazy loading
const { data: posts, pending } = await useLazyFetch('/api/posts');

// With caching
const { data: user } = await useFetch(`/api/users/${userId}`, {
  key: `user-${userId}`,
  server: true
});

// With transformation
const { data: posts } = await useFetch('/api/posts', {
  transform: (data: any[]) => data.map(post => ({
    ...post,
    formattedDate: new Date(post.createdAt).toLocaleDateString()
  }))
});

// With error handling
const { data: posts, error } = await useFetch('/api/posts', {
  onResponseError({ request, response, options }) {
    console.error('Response error:', response.status);
  },
  onRequestError({ request, options, error }) {
    console.error('Request error:', error);
  }
});

// Refresh data
const { data: posts, refresh } = await useFetch('/api/posts');

// Refresh manually
await refresh();
</script>

Récupération de données avec Pinia

// stores/posts.ts
export const usePostsStore = defineStore('posts', () => {
  const posts = ref([]);
  const loading = ref(false);
  const error = ref(null);

  const fetchPosts = async () => {
    loading.value = true;
    error.value = null;

    try {
      const data = await $fetch('/api/posts');
      posts.value = data;
    } catch (err) {
      error.value = err.message;
    } finally {
      loading.value = false;
    }
  };

  const addPost = async (post: CreatePostData) => {
    const newPost = await $fetch('/api/posts', {
      method: 'POST',
      body: post
    });
    posts.value.unshift(newPost);
  };

  return {
    posts: readonly(posts),
    loading: readonly(loading),
    error: readonly(error),
    fetchPosts,
    addPost
  };
});

Administration de l ' État

Configuration Pinia

# Install Pinia
npm install pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
});

Magasin de Pinia

// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null);
  const token = ref<string | null>(null);

  // Getters
  const isAuthenticated = computed(() => !!user.value);
  const isAdmin = computed(() => user.value?.role === 'admin');

  // Actions
  const login = async (credentials: LoginCredentials) => {
    try {
      const response = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials
      });

      user.value = response.user;
      token.value = response.token;

      // Store in cookie
      const tokenCookie = useCookie('auth-token');
      tokenCookie.value = response.token;

      return response;
    } catch (error) {
      throw error;
    }
  };

  const logout = async () => {
    try {
      await $fetch('/api/auth/logout', { method: 'POST' });
    } finally {
      user.value = null;
      token.value = null;

      const tokenCookie = useCookie('auth-token');
      tokenCookie.value = null;
    }
  };

  const fetchUser = async () => {
    try {
      const userData = await $fetch('/api/auth/me');
      user.value = userData;
    } catch (error) {
      // Token might be invalid
      logout();
    }
  };

  return {
    user: readonly(user),
    token: readonly(token),
    isAuthenticated,
    isAdmin,
    login,
    logout,
    fetchUser
  };
});

Utilisation des magasins dans les composants

<template>
  <div>
    <div v-if="isAuthenticated">
      <p>Welcome, {{ user.name }}!</p>
      <button @click="logout">Logout</button>
    </div>
    <div v-else>
      <form @submit.prevent="handleLogin">
        <input v-model="email" type="email" placeholder="Email" required />
        <input v-model="password" type="password" placeholder="Password" required />
        <button type="submit" :disabled="loading">Login</button>
      </form>
    </div>
  </div>
</template>

<script setup>
const authStore = useAuthStore();
const { user, isAuthenticated } = storeToRefs(authStore);

const email = ref('');
const password = ref('');
const loading = ref(false);

const handleLogin = async () => {
  loading.value = true;

  try {
    await authStore.login({
      email: email.value,
      password: password.value
    });

    await navigateTo('/dashboard');
  } catch (error) {
    console.error('Login failed:', error);
  } finally {
    loading.value = false;
  }
};

const logout = async () => {
  await authStore.logout();
  await navigateTo('/');
};
</script>

État persistant

// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref([]);

  // Persist to localStorage
  const persistedItems = useCookie('cart-items', {
    default: () => [],
    serializer: {
      read: (value: string) => {
        try {
          return JSON.parse(value);
        } catch {
          return [];
        }
      },
      write: (value: any[]) => JSON.stringify(value)
    }
  });

  // Initialize from persisted data
  onMounted(() => {
    items.value = persistedItems.value;
  });

  // Watch for changes and persist
  watch(items, (newItems) => {
    persistedItems.value = newItems;
  }, { deep: true });

  const addItem = (product: Product) => {
    const existingItem = items.value.find(item => item.id === product.id);

    if (existingItem) {
      existingItem.quantity++;
    } else {
      items.value.push({ ...product, quantity: 1 });
    }
  };

  const removeItem = (productId: string) => {
    const index = items.value.findIndex(item => item.id === productId);
    if (index > -1) {
      items.value.splice(index, 1);
    }
  };

  const total = computed(() => {
    return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  });

  return {
    items: readonly(items),
    total,
    addItem,
    removeItem
  };
});

Style

CSS et SCSS

<template>
  <div class="component">
    <h1 class="title">Hello World</h1>
    <p class="description">This is a Nuxt component</p>
  </div>
</template>

<style scoped>
.component {
  padding: 2rem;
  background: #f8f9fa;
}

.title {
  color: #2c3e50;
  font-size: 2rem;
  margin-bottom: 1rem;
}

.description {
  color: #7f8c8d;
  line-height: 1.6;
}
</style>

<!-- SCSS support -->
<style lang="scss" scoped>
$primary-color: #3498db;
$secondary-color: #2c3e50;

.component {
  padding: 2rem;

  .title {
    color: $primary-color;
    font-size: 2rem;

    &:hover {
      color: darken($primary-color, 10%);
    }
  }

  .description {
    color: $secondary-color;
  }
}
</style>

Tailfind CSS

# Install Tailwind CSS
npm install --save-dev @nuxtjs/tailwindcss
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss']
});
<template>
  <div class="min-h-screen bg-gray-100">
    <header class="bg-white shadow">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
          <div class="flex items-center">
            <h1 class="text-xl font-semibold text-gray-900">My App</h1>
          </div>
          <nav class="flex space-x-8">
            <NuxtLink 
              to="/" 
              class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
            >
              Home
            </NuxtLink>
            <NuxtLink 
              to="/about" 
              class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
            >
              About
            </NuxtLink>
          </nav>
        </div>
      </div>
    </header>

    <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
      <slot />
    </main>
  </div>
</template>

CSS Modules

<template>
  <div :class="$style.container">
    <h1 :class="$style.title">CSS Modules</h1>
    <p :class="$style.description">Scoped and modular CSS</p>
  </div>
</template>

<style module>
.container {
  padding: 2rem;
  background: #f8f9fa;
}

.title {
  color: #2c3e50;
  font-size: 2rem;
}

.description {
  color: #7f8c8d;
}
</style>

Styles mondiaux

/* assets/css/main.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

/* Custom global styles */
body {
  font-family: 'Inter', sans-serif;
}

.btn {
  @apply px-4 py-2 rounded font-medium transition-colors;
}

.btn-primary {
  @apply bg-blue-600 text-white hover:bg-blue-700;
}

.btn-secondary {
  @apply bg-gray-600 text-white hover:bg-gray-700;
}
// nuxt.config.ts
export default defineNuxtConfig({
  css: ['~/assets/css/main.css']
});

Greffons

Plugins côté client

// plugins/vue-toastification.client.ts
import Toast, { POSITION } from 'vue-toastification';
import 'vue-toastification/dist/index.css';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(Toast, {
    position: POSITION.TOP_RIGHT,
    timeout: 3000,
    closeOnClick: true,
    pauseOnFocusLoss: true,
    pauseOnHover: true,
    draggable: true,
    draggablePercent: 0.6,
    showCloseButtonOnHover: false,
    hideProgressBar: false,
    closeButton: 'button',
    icon: true,
    rtl: false
  });
});

Plugins côté serveur

// plugins/api.server.ts
export default defineNuxtPlugin(async (nuxtApp) => {
  // Server-side initialization
  const config = useRuntimeConfig();

  // Setup API client
  const api = $fetch.create({
    baseURL: config.public.apiBase,
    headers: {
      'Authorization': `Bearer ${config.apiSecret}`
    }
  });

  // Provide to app
  return {
    provide: {
      api
    }
  };
});

Plugins universels

// plugins/pinia-persistence.ts
export default defineNuxtPlugin(() => {
  // This runs on both client and server
  const nuxtApp = useNuxtApp();

  // Setup Pinia persistence
  if (process.client) {
    nuxtApp.$pinia.use(({ store }) => {
      // Persist store state
      const stored = localStorage.getItem(`pinia-${store.$id}`);
      if (stored) {
        store.$patch(JSON.parse(stored));
      }

      store.$subscribe((mutation, state) => {
        localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state));
      });
    });
  }
});

Plugin avec Composables

// plugins/auth.ts
export default defineNuxtPlugin(async () => {
  const { $fetch } = useNuxtApp();
  const user = useState('auth.user', () => null);

  // Check for existing session
  if (process.client) {
    const token = useCookie('auth-token');

    if (token.value) {
      try {
        const userData = await $fetch('/api/auth/me', {
          headers: {
            Authorization: `Bearer ${token.value}`
          }
        });
        user.value = userData;
      } catch (error) {
        // Invalid token, clear it
        token.value = null;
      }
    }
  }

  return {
    provide: {
      auth: {
        user,
        login: async (credentials: any) => {
          const response = await $fetch('/api/auth/login', {
            method: 'POST',
            body: credentials
          });

          user.value = response.user;
          const token = useCookie('auth-token');
          token.value = response.token;

          return response;
        },
        logout: () => {
          user.value = null;
          const token = useCookie('auth-token');
          token.value = null;
        }
      }
    }
  };
});

Modules

Installation de modules

# Popular Nuxt modules
npm install @nuxtjs/tailwindcss
npm install @nuxt/content
npm install @pinia/nuxt
npm install @nuxtjs/google-fonts
npm install @nuxt/image
npm install @vueuse/nuxt

Configuration du module

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/tailwindcss',
    '@nuxt/content',
    '@pinia/nuxt',
    '@nuxtjs/google-fonts',
    '@nuxt/image',
    '@vueuse/nuxt'
  ],

  // Module-specific configuration
  googleFonts: {
    families: {
      Inter: [400, 500, 600, 700],
      'Fira Code': [400, 500]
    }
  },

  content: {
    highlight: {
      theme: 'github-dark'
    }
  },

  image: {
    cloudinary: {
      baseURL: 'https://res.cloudinary.com/demo/image/upload/'
    }
  }
});

Module de contenu

<!-- pages/blog/[...slug].vue -->
<template>
  <div>
    <ContentDoc />
  </div>
</template>

<!-- Alternative with custom rendering -->
<template>
  <article>
    <header>
      <h1>{{ data.title }}</h1>
      <p>{{ data.description }}</p>
      <time>{{ formatDate(data.date) }}</time>
    </header>

    <ContentRenderer :value="data" />
  </article>
</template>

<script setup>
const { data } = await useAsyncData('blog-post', () => 
  queryContent('/blog').where({ _path: useRoute().path }).findOne()
);

if (!data.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Post not found'
  });
}

const formatDate = (date: string) => {
  return new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};
</script>

Module d'image

<template>
  <div>
    <!-- Optimized images -->
    <NuxtImg
      src="/hero.jpg"
      alt="Hero image"
      width="800"
      height="600"
      loading="lazy"
    />

    <!-- Responsive images -->
    <NuxtPicture
      src="/responsive.jpg"
      alt="Responsive image"
      :img-attrs="{
        class: 'responsive-image',
        style: 'display: block; margin: auto;'
      }"
    />

    <!-- Cloudinary integration -->
    <NuxtImg
      provider="cloudinary"
      src="/sample.jpg"
      width="400"
      height="300"
      :modifiers="{ quality: 'auto', format: 'auto' }"
    />
  </div>
</template>

Rendu à l'aide du serveur

SSR Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true, // Enable SSR (default)

  // Nitro configuration
  nitro: {
    prerender: {
      routes: ['/sitemap.xml', '/robots.txt']
    }
  },

  // Route rules
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },

    // Product page generated on-demand, revalidates in background
    '/products/**': { isr: true },

    // Blog post cached for 1 hour
    '/blog/**': { isr: 3600 },

    // Admin dashboard renders only on client-side
    '/admin/**': { ssr: false },

    // API routes
    '/api/**': { cors: true }
  }
});

Rendu hybride

<!-- pages/products/[id].vue -->
<template>
  <div>
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
    <p>Price: ${{ product.price }}</p>

    <!-- Client-side only component -->
    <ClientOnly>
      <ProductReviews :product-id="product.id" />
      <template #fallback>
        <div>Loading reviews...</div>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
// This runs on server for SSR
const route = useRoute();
const { data: product } = await $fetch(`/api/products/${route.params.id}`);

// Define route rules
defineRouteRules({
  isr: 3600 // Regenerate every hour
});
</script>

Génération statique de sites

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: [
        '/',
        '/about',
        '/contact'
      ]
    }
  }
});
# Generate static site
npm run generate

# Preview generated site
npm run preview

ISR (Régénération statique progressive)

<script setup>
// Enable ISR with 1 hour revalidation
defineRouteRules({
  isr: 3600
});

// Fetch data that will be cached
const { data: posts } = await $fetch('/api/posts');
</script>

Routes des API

Routes API de base

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  // Get query parameters
  const query = getQuery(event);
  const page = parseInt(query.page as string) || 1;
  const limit = parseInt(query.limit as string) || 10;

  // Fetch users from database
  const users = await getUsersFromDB({ page, limit });

  return {
    users,
    pagination: {
      page,
      limit,
      total: users.length
    }
  };
});

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  // Get request body
  const body = await readBody(event);

  // Validate input
  if (!body.email || !body.name) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email and name are required'
    });
  }

  // Create user
  const user = await createUser(body);

  // Set response status
  setResponseStatus(event, 201);

  return user;
});

Routes des API dynamiques

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  if (!id) {
    throw createError({
      statusCode: 400,
      statusMessage: 'User ID is required'
    });
  }

  const user = await getUserById(id);

  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found'
    });
  }

  return user;
});

// server/api/users/[id].put.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  const body = await readBody(event);

  const updatedUser = await updateUser(id, body);

  return updatedUser;
});

// server/api/users/[id].delete.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  await deleteUser(id);

  setResponseStatus(event, 204);
  return null;
});

Milieux de travail

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // Only apply to API routes
  if (!event.node.req.url?.startsWith('/api/')) {
    return;
  }

  // Skip auth for public routes
  const publicRoutes = ['/api/auth/login', '/api/auth/register'];
  if (publicRoutes.includes(event.node.req.url)) {
    return;
  }

  // Check authorization header
  const authorization = getHeader(event, 'authorization');

  if (!authorization) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Authorization header required'
    });
  }

  const token = authorization.replace('Bearer ', '');

  try {
    const user = await verifyToken(token);
    event.context.user = user;
  } catch (error) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid token'
    });
  }
});

Téléchargement de fichier

// server/api/upload.post.ts
import { writeFile } from 'fs/promises';
import { join } from 'path';

export default defineEventHandler(async (event) => {
  const form = await readMultipartFormData(event);

  if (!form) {
    throw createError({
      statusCode: 400,
      statusMessage: 'No file uploaded'
    });
  }

  const file = form.find(item => item.name === 'file');

  if (!file) {
    throw createError({
      statusCode: 400,
      statusMessage: 'File field is required'
    });
  }

  // Generate unique filename
  const filename = `${Date.now()}-${file.filename}`;
  const filepath = join(process.cwd(), 'public/uploads', filename);

  // Save file
  await writeFile(filepath, file.data);

  return {
    filename,
    url: `/uploads/${filename}`,
    size: file.data.length
  };
});

Déploiement

Déploiement de Vercel

# Install Vercel CLI
npm install -g vercel

# Deploy to Vercel
vercel

# Deploy to production
vercel --prod
// vercel.json
{
  "framework": "nuxtjs",
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm install"
}

Déploiement net

# netlify.toml
[build]
  command = "npm run build"
  publish = ".output/public"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[build.environment]
  NODE_VERSION = "18"

Déploiement Docker

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy source code
COPY . .

# Build application
RUN npm run build

# Expose port
EXPOSE 3000

# Start application
CMD ["node", ".output/server/index.mjs"]

Hébergement statique

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: ['/sitemap.xml']
    }
  },

  // For static hosting
  ssr: false, // Disable SSR for SPA mode

  // Or use static generation
  nitro: {
    prerender: {
      crawlLinks: true
    }
  }
});

Variables d'environnement

# .env
NUXT_SECRET_KEY=your-secret-key
NUXT_PUBLIC_API_URL=https://api.example.com

# .env.production
NUXT_SECRET_KEY=production-secret-key
NUXT_PUBLIC_API_URL=https://api.production.com
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Private keys (only available on server-side)
    secretKey: process.env.NUXT_SECRET_KEY,

    // Public keys (exposed to client-side)
    public: {
      apiUrl: process.env.NUXT_PUBLIC_API_URL
    }
  }
});

Essais

Vitest Setup

# Install testing dependencies
npm install --save-dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom'
  }
});

Essai des composants

// tests/components/Button.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Button from '~/components/Button.vue';

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: { text: 'Hello world' }
    });

    expect(wrapper.text()).toContain('Hello world');
  });

  it('emits click event', async () => {
    const wrapper = mount(Button);

    await wrapper.trigger('click');

    expect(wrapper.emitted()).toHaveProperty('click');
  });
});

E2E Test avec Playwright

# Install Playwright
npm install --save-dev @playwright/test
npx playwright install
// tests/e2e/home.spec.ts
import { test, expect } from '@playwright/test';

test('homepage', async ({ page }) => {
  await page.goto('/');

  await expect(page).toHaveTitle(/Nuxt/);
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});

Optimisation des performances

Analyse de l'ensemble

# Analyze bundle
npm run build -- --analyze

# Or use webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer

Fractionnement du code

<script setup>
// Lazy load components
const LazyComponent = defineAsyncComponent(() => import('~/components/Heavy.vue'));

// Lazy load pages
definePageMeta({
  mode: 'spa' // Client-side only
});
</script>

Optimisation de l'image

<template>
  <div>
    <!-- Optimized images -->
    <NuxtImg
      src="/hero.jpg"
      alt="Hero"
      width="800"
      height="600"
      loading="lazy"
      placeholder
    />

    <!-- WebP format -->
    <NuxtImg
      src="/image.jpg"
      alt="Image"
      format="webp"
      quality="80"
    />
  </div>
</template>

Stratégies de mise en cache

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Cache homepage for 1 hour
    '/': { isr: 3600 },

    // Cache blog posts for 1 day
    '/blog/**': { isr: 86400 },

    // Prerender at build time
    '/about': { prerender: true }
  }
});

Dépannage

Questions communes

Mismatch d'hydratation

<template>
  <div>
    <!-- Use ClientOnly for client-specific content -->
    <ClientOnly>
      <div>{{ new Date().toLocaleString() }}</div>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </ClientOnly>
  </div>
</template>

Fuites de mémoire

<script setup>
// Proper cleanup
const interval = setInterval(() => {
  console.log('tick');
}, 1000);

onUnmounted(() => {
  clearInterval(interval);
});
</script>

Créer des erreurs

# Clear Nuxt cache
rm -rf .nuxt .output

# Clear node_modules
rm -rf node_modules package-lock.json
npm install

# Check for TypeScript errors
npx nuxi typecheck

Meilleures pratiques

Structure du projet

# Recommended structure
components/
├── ui/              # Basic UI components
├── forms/           # Form components
└── layout/          # Layout components

composables/
├── useAuth.ts       # Authentication
├── useApi.ts        # API calls
└── useState.ts      # State management

utils/
├── validation.ts    # Validation helpers
├── formatting.ts    # Formatting utilities
└── constants.ts     # App constants

Rendement

<script setup>
// Use computed for expensive operations
const expensiveValue = computed(() => {
  return heavyCalculation(props.data);
});

// Use lazy loading for heavy components
const HeavyChart = defineAsyncComponent(() => import('~/components/HeavyChart.vue'));

// Optimize images
</script>

<template>
  <div>
    <NuxtImg
      src="/image.jpg"
      alt="Image"
      loading="lazy"
      placeholder
    />
  </div>
</template>

SEO

<script setup>
// Dynamic meta tags
const route = useRoute();
const { data: post } = await $fetch(`/api/posts/${route.params.slug}`);

useSeoMeta({
  title: post.title,
  ogTitle: post.title,
  description: post.excerpt,
  ogDescription: post.excerpt,
  ogImage: post.image,
  twitterCard: 'summary_large_image'
});
</script>

Résumé

Nuxt.js est un puissant cadre Vue.js qui fournit une solution complète pour construire des applications web modernes. Les principales caractéristiques sont les suivantes :

  • ** Routage basé sur les fichiers**: routage automatique basé sur la structure des fichiers
  • ** Rendu à l'aide du serveur**: SSR intégré avec options de rendu hybride
  • Importations automatiques: Importations automatiques de composants et de composants
  • ** Récupération de données** : Données puissantes récupérées avec cache et réactivité
  • Modules: Riche écosystème de modules pour une fonctionnalité étendue
  • Performance: Optimisations intégrées et fractionnement des codes

Pour obtenir des résultats optimaux, utiliser le rendu côté serveur pour un meilleur référencement, utiliser des composants pour une logique réutilisable, mettre en œuvre des stratégies de cache appropriées et suivre les conventions Nuxt.js pour le routage et la récupération de données.