{{-- --}}
npm create @vite-pwa/pwa@latest my-vue-app -- --template vue
@endverbatim
npm install vite-plugin-pwa -D
// vite.config.ts
import path from 'node:path'
import { VitePWA } from 'vite-plugin-pwa';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwind from 'tailwindcss'
import autoprefixer from 'autoprefixer'
// https://vitejs.dev/config/
export default defineConfig({
css: {
postcss: {
plugins: [tailwind(), autoprefixer()],
},
},
plugins: [vue(), VitePWA({
registerType: 'prompt',
injectRegister: false,
pwaAssets: {P
disabled: false,
config: true,
},
manifest: {
name: 'elan_app',
short_name: 'elan_app',
description: 'Application Elan maintenance',
theme_color: '#ffffff',
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico}'],
cleanupOutdatedCaches: true,
clientsClaim: true,
},
devOptions: {
enabled: false,
navigateFallback: 'index.html',
suppressWarnings: true,
type: 'module',
},
})],
// base: '/elan_app/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Le Service Worker est automatiquement généré par Vite PWA. Voici les principales configurations : (toujours basé sur notre application elan_app)
VitePWA({
// Définit comment l'application demande l'installation de la PWA
// 'prompt' : affiche une fenêtre de confirmation à l'utilisateur
// Autres options : 'autoUpdate', 'force'
registerType: 'prompt',
// Désactive l'injection automatique du script d'enregistrement du Service Worker
// Si false, on doit l'enregistrer manuellement dans votre code
injectRegister: false,
// Configuration des ressources PWA (icônes, splash screens, etc.)
pwaAssets: {
disabled: false, // Active la génération des assets
config: true, // Génère automatiquement la configuration des assets
},
// Configuration du manifeste web (informations de base de la PWA)
manifest: {
name: 'elan_app', // Nom complet de l'application
short_name: 'elan_app', // Nom court pour l'écran d'accueil
description: 'Application Elan maintenance', // Description de l'app
theme_color: '#ffffff', // Couleur principale de l'interface
},
// Configuration de Workbox (bibliothèque pour gérer le cache)
workbox: {
// Définit quels fichiers seront mis en cache
globPatterns: ['**/*.{js,css,html,svg,png,ico}'],
// Supprime les anciennes versions du cache lors des mises à jour
cleanupOutdatedCaches: true,
// Permet au Service Worker de prendre le contrôle immédiatement
clientsClaim: true,
},
// Options spécifiques pour le développement
devOptions: {
enabled: false, // Désactive la PWA en développement
navigateFallback: 'index.html', // Page par défaut en cas d'erreur
suppressWarnings: true, // Supprime les avertissements dans la console
type: 'module', // Utilise les modules ES6
},
})
Pour gérer l'installation de la PWA :
// PWABadge.vue
<script setup lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/vue'
const { needRefresh, updateServiceWorker } = useRegisterSW({
immediate: true,
onRegistered(r) {
console.log('SW Registered:', r)
},
onRegisterError(error) {
console.log('SW registration error', error)
},
})
const updateApp = async () => {
await updateServiceWorker()
}
const closeToast = () => {
needRefresh.value = false
}
</script>
<template>
<div v-if="needRefresh" class="pwa-toast">
<div class="message">
Nouvelle version disponible. Voulez-vous mettre à jour ?
</div>
<div class="buttons">
<button @click="updateApp" class="update-button">Mettre à jour</button>
<button @click="closeToast" class="close-button">Fermer</button>
</div>
</div>
</template>
Pour gérer l'installation de Pinia :
npm install pinia
Pinia est une bibliothèque de gestion d'état pour Vue.js, offrant une alternative plus légère et intuitive à Vuex. Elle permet de créer des stores pour gérer l'état global de l'application de manière efficace et typesafe.
Dans notre projet, Pinia est installé et configuré comme suit :
import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'
import router from './router.ts'
import '@fortawesome/fontawesome-free/css/all.css'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { useAuthStore } from './stores/auth.ts'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
Nous utilisons également le plugin pinia-plugin-persistedstate pour persister l'état de certains stores entre les rechargements de page.
Un store Pinia est généralement défini dans un fichier séparé. Par exemple, voici comment pourrait être défini notre store d'authentification :
import { defineStore } from 'pinia'
import { config } from '@/config';
import axios from 'axios'
// Création d'une instance axios avec une configuration de base
const apiClient = axios.create({
baseURL: config.apiUrl,
withCredentials: true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
// Définition des interfaces pour le typage
interface User {
id: number;
email: string;
}
interface AuthState {
user: User | null;
error: string | null;
clientLogo: string | null;
}
// Définition du store d'authentification
export const useAuthStore = defineStore({
id: "auth", // Identifiant unique du store
// État initial du store
state: (): AuthState => ({
user: null,
error: null,
clientLogo: null
} as AuthState),
// Getters pour accéder à l'état de manière calculée
getters: {
// Vérifie si un utilisateur est authentifié
isAuthenticated: (state): boolean => !!state.user
},
// Actions pour modifier l'état du store
actions: {
// Récupère le logo du client depuis l'API
async fetchClientLogo(): Promise<void> {
try {
const response = await apiClient.get('/client-logo');
this.clientLogo = response.data.logo_url;
} catch (error) {
console.error('Erreur lors de la récupération du logo client:', error);
this.clientLogo = null;
}
},
// Gère la connexion de l'utilisateur
async login(email: string, password: string): Promise<boolean> {
try {
const response = await apiClient.post('/vue/login', { email, password });
const { access_token, user } = response.data;
// Stocke le token dans le localStorage
localStorage.setItem('token', access_token);
// Met à jour l'état du store
this.user = user;
this.error = null;
return true;
} catch (error: any) {
this.error = error.response?.data?.message || 'Une erreur est survenue lors de la connexion';
console.error('Erreur de connexion:', this.error);
return false;
}
},
// Vérifie l'authentification de l'utilisateur
async checkAuth(): Promise<void> {
const token = localStorage.getItem('token');
if (token) {
try {
const response = await apiClient.get('/verifyToken', {
headers: { Authorization: `Bearer ${token}` }
});
this.user = response.data.user;
} catch (error) {
// Si le token n'est pas valide, réinitialise l'état
this.user = null;
localStorage.removeItem('token');
}
} else {
this.user = null;
}
},
// Déconnecte l'utilisateur
logout(): void {
this.user = null;
this.error = null;
localStorage.removeItem('token');
},
// Efface les erreurs
clearError() {
this.error = null;
},
},
// Configuration de la persistance du store
persist: {
key: 'auth', // Clé utilisée pour le stockage
storage: localStorage, // Utilise le localStorage pour la persistance
}
})
Dans nos composants Vue, nous pouvons utiliser les stores Pinia comme suit :
<script setup>
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
// Utilisation de l'état
console.log(authStore.isAuthenticated)
// Appel d'une action
authStore.login(userData)
</script>
Dans notre application, nous utilisons Pinia pour gérer l'état d'authentification et les notifications. Par exemple, dans App.vue, nous observons les changements d'authentification et gérons les notifications :
// App.vue
<script setup>
// Gestion des notifications
onMounted(() => {
if (authStore.isAuthenticated) {
notificationsStore.initSSE();
}
});
onUnmounted(() => {
notificationsStore.closeSSE();
});
// Surveillance des changements d'authentification
watch(() => authStore.isAuthenticated, (isAuthenticated) => {
isAuthenticated ? notificationsStore.initSSE() : notificationsStore.closeSSE();
});
// Surveillance des nouvelles notifications
watch(() => notificationsStore.lastNotification, (newNotification) => {
if (newNotification?.data?.message && newNotification.isNew) {
toast({
title: "Nouvelle notification",
description: newNotification.data.message,
duration: 5000,
});
}
});
</script>
Cette approche nous permet de réagir aux changements d'état de manière réactive.
Assets
// manifest.json
"icons": [
{
"src": "src/assets/elan_192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/src/assets/elan_512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/src/assets/elan_180.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any maskable"
}
],
<!-- index.html -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no">
<title>Elan Maintenance Application</title>
<link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="/src/assets/elan_192.png">
<link rel="apple-touch-icon" sizes="152x152" href="/src/assets/elan_152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/src/assets/elan_180.png">
<link rel="apple-touch-icon" sizes="167x167" href="/src/assets/elan_167.png">
<link rel="apple-touch-startup-image" href="/src/assets/splash.png">
</head>
Performance
// vite.config.ts
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico}'],
cleanupOutdatedCaches: true,
clientsClaim: true,
},
Dans notre application nous n'avons pas encore les 2 derniers points.
// PWABadge.vue
<script setup lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/vue'
const { needRefresh, updateServiceWorker } = useRegisterSW({
immediate: true,
onRegistered(r) {
console.log('SW Registered:', r)
},
onRegisterError(error) {
console.log('SW registration error', error)
},
})
const updateApp = async () => {
await updateServiceWorker()
}
const closeToast = () => {
needRefresh.value = false
}
</script>
<template>
<div v-if="needRefresh" class="pwa-toast">
<div class="message">
Nouvelle version disponible. Voulez-vous mettre à jour ?
</div>
<div class="buttons">
<button @click="updateApp" class="update-button">Mettre à jour</button>
<button @click="closeToast" class="close-button">Fermer</button>
</div>
</div>
</template>
Notre application utilise un manifest.json personnalisé qui inclut des configurations pour les icônes, les raccourcis, et les paramètres de partage. Voir le fichier manifest.json pour plus de détails.
Nous utilisons deux composants principaux pour gérer les mises à jour de l'application et le mode hors ligne :
Notre application utilise une configuration dynamique pour l'URL de l'API en fonction de l'environnement de déploiement. Voir le fichier src/config.js pour plus de détails.
L'initialisation de notre PWA est liée à l'authentification de l'utilisateur. Nous vérifions l'authentification avant de monter l'application.
Notre application inclut une logique pour gérer les notifications en temps réel via SSE (Server-Sent Events). Cette logique est implémentée dans le composant principal App.vue.
Notre projet utilise plusieurs dépendances spécifiques pour la PWA, notamment :
@vite-pwa/assets-generator : Pour la génération d'assets PWAvite-plugin-pwa : Pour la configuration de la PWA avec Viteworkbox-window est une bibliothèque qui fait partie de l'écosystème Workbox, spécialement conçue pour faciliter l'interaction entre votre application web et le service worker. (Ensemble de modules destinés à s'exécuter dans le contexte de fenêtre, c'est-à-dire à l'intérieur de vos pages Web PWA.)Ces ajouts permettront de mieux refléter les spécificités de votre projet dans la documentation.
npm create vite@latest nom-du-projet -- --template vue-tsnpm run devnpm run buildnpm run previewnpm install pinia// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(pinia)
npm install vue-router@4// router.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from './stores/auth.ts'
const router = createRouter({
history: createWebHistory(),
routes
})
app.use(router)
npm install axios// store/auth.ts store/interventions.js store/notifications.js
import axios from 'axios'
const apiClient = axios.create({
baseURL: config.apiUrl,
withCredentials: true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
npm install vite-plugin-pwa -Dvite.config.ts :// vite.config.ts
import path from 'node:path'
import { VitePWA } from 'vite-plugin-pwa';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwind from 'tailwindcss'
import autoprefixer from 'autoprefixer'
// https://vitejs.dev/config/
export default defineConfig({
css: {
postcss: {
plugins: [tailwind(), autoprefixer()],
},
},
plugins: [vue(), VitePWA({
registerType: 'prompt',
injectRegister: false,
pwaAssets: {
disabled: false,
config: true,
},
manifest: {
name: 'elan_app',
short_name: 'elan_app',
description: 'Application Elan maintenance',
theme_color: '#ffffff',
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico}'],
cleanupOutdatedCaches: true,
clientsClaim: true,
},
devOptions: {
enabled: false,
navigateFallback: 'index.html',
suppressWarnings: true,
type: 'module',
},
})],
// base: '/elan_app/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
vite.config.ts :// vite.config.ts
import tailwind from 'tailwindcss'
import autoprefixer from 'autoprefixer'
export default defineConfig({
css: {
postcss: {
plugins: [tailwind(), autoprefixer()],
},
},
})
npm install @fortawesome/fontawesome-free// main.ts
import '@fortawesome/fontawesome-free/css/all.css'
npm install pinia-plugin-persistedstate// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// store/location.ts
import { defineStore } from 'pinia';
export const useLocationStore = defineStore('location', {
state: () => ({
currentPosition: null,
watchId: null,
error: null,
}),
actions: {
startTracking() {
if ("geolocation" in navigator) {
this.watchId = navigator.geolocation.watchPosition(
this.updatePosition,
this.handleError,
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
} else {
this.error = "La géolocalisation n'est pas supportée par ce navigateur.";
}
},
stopTracking() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
}
},
updatePosition(position) {
this.currentPosition = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
this.error = null;
},
handleError(error) {
console.warn("Erreur de géolocalisation:", error.message);
this.error = error.message;
},
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Rayon de la Terre en km
const dLat = this.deg2rad(lat2 - lat1);
const dLon = this.deg2rad(lon2 - lon1);
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(this.deg2rad(lat1)) * Math.cos(this.deg2rad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // Distance en km
},
deg2rad(deg) {
return deg * (Math.PI/180);
},
getDistanceToInterventions(interventions) {
if (!this.currentPosition) {
// Retourner les interventions sans distance si la position n'est pas disponible
return interventions.map(intervention => ({
...intervention,
distance: null
}));
}
return interventions.map(intervention => ({
...intervention,
distance: this.calculateDistance(
this.currentPosition.latitude,
this.currentPosition.longitude,
intervention.sit_lat,
intervention.sit_lng
)
}));
}
},
});
Ces commandes et configurations représentent les principales bibliothèques et outils utilisés dans notre projet Vue.js avec Vite.
# Build de production
npm run build
# Build de développement
npm run dev
# Test de la PWA en local
npm run preview
# Générer les icônes (avec sharp)
npm run generate-pwa-icons