PWA (Progressive Web App)
Documentation Vite-PWA
Une Progressive Web App (PWA) est une application qui combine le meilleur du web et du mobile. Elle s'installe sur l'écran d'accueil, fonctionne hors connexion et offre une expérience fluide proche d'une app native.
Cette page couvre la chaîne complète : Service Worker, manifest, origines front/API, démarrage hors ligne, mise à jour et installation.
Important
Deux points sont sources d'erreur récurrente et sont traités en détail plus bas : le fallback de navigation (sans lui, l'application ne redémarre pas hors ligne) et la contrainte de même origine entre le front et l'API.
Les deux modes de Service Worker
vite-plugin-pwa propose deux stratégies. SmartMaker utilise les deux, selon les projets. Il faut savoir laquelle est en place avant de toucher à la configuration, car les options ne sont pas interchangeables.
| Mode | Qui écrit le Service Worker | Où se configure le cache |
|---|---|---|
generateSW (défaut) |
le plugin, entièrement | bloc workbox de vite.config.js |
injectManifest |
vous, dans mobile/src/sw.js |
dans votre sw.js, en code Workbox |
Attention
En mode injectManifest, les options workbox.runtimeCaching, skipWaiting et clientsClaim placées dans vite.config.js sont purement et simplement ignorées. Elles n'existent qu'en mode generateSW. C'est la cause la plus fréquente du symptôme "ma configuration VitePWA ne fonctionne pas".
État des projets de référence :
| Projet | Mode | Manifest |
|---|---|---|
| smartboot (squelette) | injectManifest |
dynamique (SmartAuth) |
| smartInterventions | injectManifest |
statique (Vite) |
| capfullpos | generateSW |
dynamique (SmartAuth) |
| offlinepropale | generateSW |
dynamique (SmartAuth) |
Un projet créé avec SmartBoot démarre donc en mode injectManifest.
Mode generateSW
// vite.config.js
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
cleanupOutdatedCaches: true,
maximumFileSizeToCacheInBytes: 3000000,
skipWaiting: true,
clientsClaim: true,
runtimeCaching: [
{
urlPattern: /\/api\/(home|profile)/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 },
cacheableResponse: { statuses: [0, 200] },
networkTimeoutSeconds: 10,
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 },
},
},
],
},
injectRegister: "auto",
includeAssets: ["favicon.ico", "assets/*", "favicon.png", "apple-touch-icon.png"],
manifest: false,
})
Avec appType: 'spa', le plugin ajoute automatiquement un navigateFallback vers index.html. Le redémarrage hors ligne sur une route cliente fonctionne donc sans rien écrire de plus.
Mode injectManifest
C'est le mode du squelette SmartBoot. Il est choisi parce que le Service Worker doit porter les gestionnaires Web Push de SmartCommon, ce que generateSW ne permet pas.
// vite.config.js
VitePWA({
registerType: 'autoUpdate',
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.js',
injectManifest: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
maximumFileSizeToCacheInBytes: 3000000,
},
// Le SW est enregistre a la main dans src/main.jsx via virtual:pwa-register
injectRegister: false,
includeAssets: ["favicon.ico", "assets/*", "favicon.png", "apple-touch-icon.png"],
manifest: false,
})
Points à retenir :
globPatternspasse dansinjectManifest, pas dansworkboxinjectRegister: falseparce que l'enregistrement est fait manuellement (voir plus bas)- tout le comportement de cache vit maintenant dans
mobile/src/sw.js
Le Service Worker en mode injectManifest
Le fichier mobile/src/sw.js appartient au projet. Le plugin n'y injecte que la liste des fichiers à précacher, via self.__WB_MANIFEST.
Squelette complet
// mobile/src/sw.js
import {
precacheAndRoute,
cleanupOutdatedCaches,
createHandlerBoundToURL,
} from "workbox-precaching";
import { registerRoute, NavigationRoute } from "workbox-routing";
import { NetworkFirst, CacheFirst } from "workbox-strategies";
import { ExpirationPlugin } from "workbox-expiration";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { registerPushHandlers } from "@cap-rel/smartcommon/sw";
// 1. Precache des assets du build (obligatoire en injectManifest)
precacheAndRoute(self.__WB_MANIFEST);
// 2. Equivalent de cleanupOutdatedCaches: true
cleanupOutdatedCaches();
// 3. Fallback de navigation SPA (voir l'encadre ci-dessous)
registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html"), {
denylist: [/^\/api\//, /^\/api\.php\//, /\/[^/?]+\.[^/?]+$/],
}));
// 4. Runtime caching (equivalent de workbox.runtimeCaching)
registerRoute(
/\/api\/(home|profile)/,
new NetworkFirst({
cacheName: "api-cache",
networkTimeoutSeconds: 10,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 }),
],
})
);
registerRoute(
/\.(?:png|jpg|jpeg|svg|gif|webp)$/,
new CacheFirst({
cacheName: "images-cache",
plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 })],
})
);
// 5. Equivalent de skipWaiting + clientsClaim
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
// 6. Web Push (gestionnaires partages fournis par smartcommon)
registerPushHandlers({
defaultIcon: "/images/pwa-192x192.png",
defaultBadge: "/images/pwa-64x64.png",
});
Le fallback de navigation, indispensable hors ligne
Attention
C'est le piège numéro un du mode injectManifest. Contrairement à generateSW, aucun navigateFallback n'est ajouté implicitement. Le squelette SmartBoot fournit désormais cette route ; si vous reprenez un projet créé avant, ou si vous écrivez votre sw.js de zéro, vérifiez qu'elle est bien présente.
Sans cette route, seul index.html est précaché, pas les chemins clients. Un rechargement hors ligne sur /interventions ou /produit/12 ne trouve rien dans le precache, part au réseau et échoue sur net::ERR_INTERNET_DISCONNECTED. Symptôme observé : écran blanc au redémarrage hors ligne, alors que le Service Worker est bien actif et que le cache est rempli.
registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html"), {
// Ne jamais detourner les appels API ni les requetes de fichier
// (tout ce qui porte une extension) : seules les vraies navigations
// applicatives retombent sur le shell.
denylist: [/^\/api\//, /^\/api\.php\//, /\/[^/?]+\.[^/?]+$/],
}));
La denylist est indispensable : sans elle, le Service Worker renverrait index.html en réponse à des appels API ou à des téléchargements de fichiers.
Note
Les deux motifs d'API ne font pas doublon. Une PWA déployée dans pwa/ est construite avec VITE_API_URL=/api.php/ : ses appels commencent par /api.php/, que le motif /^\/api\// ne couvre pas. Seul le motif générique "tout ce qui porte une extension" les attrapait, ce qui est fragile.
Implémentation de référence : smartInterventions/mobile/src/sw.js et le sw.js du squelette SmartBoot.
Enregistrement du Service Worker
En mode injectManifest avec injectRegister: false, l'enregistrement est explicite dans mobile/src/main.jsx :
import { registerSW } from "virtual:pwa-register";
registerSW({
immediate: true,
});
Note
Ne pas laisser injectRegister: "auto" en même temps qu'un appel manuel à registerSW : le Service Worker serait enregistré deux fois.
Manifest
Deux approches coexistent dans SmartMaker. Choisissez avant de commencer, elles ne se combinent pas.
| Approche | Configuration Vite | Lien dans index.html | Personnalisation |
|---|---|---|---|
| Manifest dynamique SmartAuth | manifest: false |
explicite, obligatoire | constantes Dolibarr, par entité |
| Manifest statique Vite | bloc manifest: { ... } |
généré, à retirer | dans le code, au build |
Manifest dynamique servi par SmartAuth
C'est le choix du squelette SmartBoot et de la majorité des modules. Le manifest est produit par le PwaController de SmartAuth à partir des constantes Dolibarr du module, ce qui permet à chaque client d'avoir son propre nom et ses propres icônes sans rebuilder l'application.
Dans mobile/index.html :
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="api.php/manifest.webmanifest">
<link rel="icon" type="image/png" href="api.php/icon/64">
<link rel="apple-touch-icon" href="api.php/icon/192">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Dans vite.config.js : manifest: false.
Routes SmartAuth
| Route | Description |
|---|---|
GET api.php/manifest.webmanifest |
manifest JSON généré dynamiquement |
GET api.php/icon/{size} |
icône PWA à la taille demandée (64, 192 ou 512) |
Attention
L'URL est bien manifest.webmanifest, extension comprise. Un lien vers api.php/manifest renvoie une erreur de routage.
Ces deux routes ne sont pas protégées : le navigateur doit pouvoir les charger avant toute authentification.
Constantes Dolibarr
Le préfixe est le nom du module en majuscules.
| Constante | Description | Défaut |
|---|---|---|
{MODULE}_PWA_NAME |
nom complet de l'application | nom de la société, sinon nom du module |
{MODULE}_PWA_DESCRIPTION |
description | vide |
{MODULE}_PWA_BG_COLOR |
couleur de fond | #ffffff |
{MODULE}_PWA_THEME_COLOR |
couleur du thème | #000000 |
Note
Il n'existe pas de constante pour le nom court : short_name est dérivé automatiquement des 12 premiers caractères de {MODULE}_PWA_NAME.
Champs non configurables
Le manifest dynamique fixe ces valeurs et ne les expose pas en constantes :
"id": "/",
"scope": "/",
"start_url": "/",
"display": "standalone",
"prefer_related_applications": false
Astuce
La question revient souvent : "display": "standalone" est déjà actif. Une application installée depuis un manifest dynamique s'ouvre donc bien dans sa propre fenêtre, sans barre d'adresse. Il n'y a rien à ajouter.
Ce choix est assumé et ne bougera pas : le manifest dynamique ne sert qu'à ce qui dépend réellement de l'instance Dolibarr, c'est-à-dire le nom, la description, les couleurs et les icônes. display, start_url, scope et orientation sont des propriétés de l'application, pas du client qui l'héberge : elles appartiennent au build. Les exposer en constantes reviendrait à confier au paramétrage Dolibarr des décisions que seul l'auteur de l'application peut prendre, et à multiplier les combinaisons à tester.
Si vous avez besoin de modifier display, start_url, orientation ou d'ajouter des shortcuts, passez au manifest statique.
Attention
id et scope ne doivent pas être modifiés à la légère. id est la clé sur laquelle le navigateur reconnaît une application déjà installée. Le changer fait de votre application une nouvelle application aux yeux du navigateur : les installations existantes ne sont plus reconnues, et la détection décrite plus bas cesse de fonctionner pour ceux qui avaient déjà installé.
Auto-déclaration pour la détection d'installation
Le manifest dynamique se déclare lui-même :
"related_applications": [
{ "platform": "webapp", "url": "https://votre-app.example.fr/pwa/api.php/manifest.webmanifest" }
]
C'est ce qui rend navigator.getInstalledRelatedApps() exploitable : sans cette entrée, l'appel renvoie une liste vide sur Android et le bandeau d'installation est reproposé à des utilisateurs qui ont déjà l'application sur leur écran d'accueil.
L'URL est absolue et construite depuis la requête en cours, jamais avec dol_buildpath() : l'application est servie sur son propre virtualhost, que Dolibarr ne connaît pas. L'en-tête Host étant fourni par le client, il est validé ; s'il est inutilisable, related_applications est simplement omis plutôt que de publier une URL pointant vers une origine choisie par un tiers.
Rien à faire de votre côté : c'est servi automatiquement. Si vous passez au manifest statique, en revanche, cette entrée est à écrire vous-même.
Icônes
Le PwaController cherche l'icône dans cet ordre :
- icône personnalisée envoyée depuis l'administration du module, dans
{dir_output}/pwa/icon_{taille}.png - icône livrée avec le module, dans
pwa/images/pwa-{taille}x{taille}.png - à défaut, un carré bleu généré avec les initiales du module (nécessite GD)
Les tailles servies sont 64, 192 et 512. Toute autre valeur retombe sur 512.
Manifest statique généré par Vite
C'est le choix de smartInterventions. Il convient quand le manifest est le même pour tous les clients et que vous voulez la main sur tous les champs.
// vite.config.js
VitePWA({
// ...
manifest: {
name: "SmartInterventions",
short_name: "SmartInterventions",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#dc2626",
icons: [
{ src: 'images/pwa-64x64.png', sizes: '64x64', type: 'image/png' },
{ src: 'images/pwa-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
{ src: 'images/pwa-512x512.png', sizes: '512x512', type: 'image/png' },
],
},
})
Dans ce cas, retirez le <link rel="manifest"> de index.html : Vite l'injecte lui-même. Deux liens concurrents produisent un manifest ignoré ou incohérent.
Origine du front et origine de l'API
C'est la question qui bloque le plus souvent en développement.
La règle
Un manifest doit être servi depuis la même origine que le document HTML qui le référence. Un front sur http://localhost:5173 et une API sur https://dolibarr.local sont deux origines distinctes : le navigateur refuse le manifest et affiche un message du type "Le manifest doit avoir la même origine que la page".
La même contrainte s'applique au Service Worker : il ne contrôle que son origine.
Important
Il n'est donc pas possible de faire cohabiter durablement un front et une API sur deux domaines différents pour une PWA. La solution n'est pas de configurer CORS, c'est de ramener les deux sur la même origine.
En production : même origine par construction
La PWA est servie depuis le dossier pwa/ du module, sur le même hôte que Dolibarr :
https://erp.client.fr/custom/monmodule/pwa/ <- le front
https://erp.client.fr/custom/monmodule/pwa/api.php/ <- l'API
Le Makefile de build force d'ailleurs l'URL de l'API en relatif juste avant de builder :
echo "VITE_API_URL=/api.php/" >| ./mobile/.env
puis restaure la valeur d'origine du .env pour ne pas casser l'environnement de développement. La PWA de production n'a donc aucune URL absolue de backend.
En développement : deux serveurs, deux origines
En développement, vite dev sert le front sur le port 5173 et l'API reste sur le Dolibarr local. Deux origines, donc :
api.php/manifest.webmanifestest introuvable sur le port 5173, puisque rien ne sertapi.phpà cette adresse- pointer le lien vers
https://dolibarr.local/custom/monmodule/pwa/api.php/manifest.webmanifestfait rejeter le manifest pour cause d'origine différente
La solution est de proxifier l'API depuis le serveur de développement Vite, pour que tout soit servi depuis localhost:5173.
Avec le squelette SmartBoot, il n'y a rien à coder : le proxy est déjà en place et n'attend qu'une variable d'environnement.
# mobile/.env
# Repertoire qui CONTIENT api.php, sans slash final
VITE_DEV_PROXY_TARGET=https://dolibarr.local/custom/monmodule/pwa
Si la variable est absente ou vide, aucun proxy n'est enregistré et le comportement du serveur de développement est inchangé.
Pour un projet qui n'est pas issu du squelette, la configuration équivalente :
// vite.config.js
import { defineConfig, loadEnv } from "vite";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const proxyTarget = env.VITE_DEV_PROXY_TARGET;
return {
server: proxyTarget ? {
proxy: {
'/api.php': {
target: proxyTarget,
changeOrigin: true,
secure: false,
},
},
} : undefined,
// ...
};
});
En hébergement mutualisé : DoliProxy
Pour les déploiements où la PWA n'est pas servie par le Dolibarr du client, DoliProxy fournit un mode hosted_pwa : le client accède à {client}.{module}.doliproxy.fr, le proxy sert les fichiers statiques de la PWA sur / et proxifie /api/* vers le Dolibarr du client. Une seule origine, donc ni problème de CORS ni problème de manifest.
La PWA lit alors son config.json au démarrage, qui contient "apiUrl": "/api/". Elle ne connaît jamais l'URL réelle du backend.
Et le mode preview ?
npm run build puis npm run preview sert le build sur le port 4173. C'est utile mais partiel :
| Ce que preview permet de valider | Ce qu'il ne permet pas |
|---|---|
| enregistrement du Service Worker | le manifest dynamique (pas d'api.php) |
| contenu du precache | les appels API réels |
| fallback de navigation hors ligne | les icônes servies par SmartAuth |
La validation complète se fait sur la PWA déployée dans pwa/ et servie par Dolibarr.
Démarrer hors ligne
Faire fonctionner une application déjà chargée sans réseau est facile. La faire démarrer sans réseau demande que quatre conditions soient réunies.
1. Le bundle est entièrement précaché
globPatterns doit couvrir tous les types de fichiers du build :
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}']
L'extension json n'est pas décorative : elle précache les fichiers de traduction de public/locales/. Sans eux, l'application démarre hors ligne mais affiche les clés de traduction brutes.
2. La taille limite est suffisante
maximumFileSizeToCacheInBytes: 3000000
Attention
Tout fichier plus gros que cette limite est silencieusement exclu du precache. Si le bundle principal dépasse la limite, l'application est inutilisable hors ligne et rien ne le signale au build. capfullpos a rencontré exactement ce cas et a dû monter la limite à 5 Mo. Vérifiez la taille de vos assets dans pwa/assets/ et gardez de la marge.
3. Le fallback de navigation est en place
En generateSW, il est implicite. En injectManifest, il faut l'écrire (voir plus haut). Sans lui, un rechargement hors ligne sur une route autre que la racine donne un écran blanc.
4. Les données métier sont dans IndexedDB
Le Service Worker cache les fichiers, pas les données. Les données métier doivent être en base locale via la classe Db de SmartCommon et les hooks useDb<Feature>. Voir Stockage de données et Synchronisation offline.
Détecter l'état de la connexion
import { useOnlineStatus } from '@cap-rel/smartcommon';
const MyComponent = () => {
const {
isOnline, // true si le navigateur est en ligne
isServerReachable, // true/false/null selon le health check
lastOnline, // timestamp de la derniere connexion
checkNow, // verification manuelle
} = useOnlineStatus({
healthCheckUrl: '/api/health',
healthCheckInterval: 30000,
stabilityDelay: 2000,
timeout: 5000,
});
return !isOnline ? <div>Mode hors connexion</div> : null;
};
Synchronisation
import { useSyncClient } from '@cap-rel/smartcommon';
const sync = useSyncClient({
apiUrl: import.meta.env.VITE_API_URL,
getAccessToken: () => api.user?.accessToken,
scope: ['items'],
autoSync: true,
syncInterval: 60000,
});
await sync.create('items', { label: 'Nouveau' });
await sync.sync(); // push + pull
console.log(sync.pendingCount); // operations en attente
console.log(sync.isSyncing); // synchronisation en cours
Voir Synchronisation offline pour le détail, y compris la résolution de conflits.
Mise à jour de l'application
registerType
| Valeur | Comportement |
|---|---|
autoUpdate |
le nouveau Service Worker prend la main sans demander |
prompt |
le nouveau Service Worker attend une action explicite |
autoUpdate suppose que le Service Worker appelle skipWaiting() et clients.claim(). En mode injectManifest, c'est à vous de les écrire (section 5 du squelette de sw.js ci-dessus).
usePWAUpdate
import { usePWAUpdate } from '@cap-rel/smartcommon';
const MyApp = () => {
const {
updateAvailable, // true quand une mise a jour est prete
updateActivated, // true quand la mise a jour est activee
checkForUpdates, // verification manuelle
applyUpdate, // appliquer (skip waiting + reload)
reloadPage, // recharger la page
} = usePWAUpdate({
autoReload: false,
checkInterval: 0,
onUpdateAvailable: () => {},
onUpdateActivated: () => {},
});
return updateAvailable ? <button onClick={applyUpdate}>Mettre à jour</button> : null;
};
UpdatePrompt
Composant prêt à l'emploi. Trois variantes :
| Variante | Description |
|---|---|
toast |
notification en bas de l'écran (défaut) |
banner |
bandeau fixe en haut ou en bas |
modal |
fenêtre modale centrée |
import { UpdatePrompt } from '@cap-rel/smartcommon';
const App = () => (
<Provider config={appConfig}>
<UpdatePrompt
variant="toast"
checkInterval={60000}
labels={{
title: "Mise à jour disponible",
message: "Une nouvelle version est disponible.",
reloadButton: "Rafraîchir",
dismissButton: "Plus tard",
}}
/>
<Router />
</Provider>
);
Le Provider accepte aussi une prop pwaUpdate qui monte UpdatePrompt automatiquement. Voir Configuration du Provider.
Afficher la version qui tourne
Convention SmartMaker : chaque build porte un numéro incrémental, injecté par le Makefile dans VITE_APP_VERSION au format <version du module>.<numéro de build> (par exemple 2.0.1.42, suffixé -dev pour un build de debug).
// mobile/src/utils/constants/vite.js
export const APP_VERSION = import.meta.env.VITE_APP_VERSION;
À afficher discrètement en pied de page de connexion, dans les réglages et dans l'AboutModal. C'est le premier élément à demander à un utilisateur qui signale un comportement inattendu.
Mise à jour et données en attente de synchronisation
Une question revient souvent : que deviennent les opérations faites hors ligne quand le Service Worker se met à jour et que la page recharge ?
Elles survivent. La file d'attente n'est pas dans le cache du Service Worker : useSyncClient la persiste dans une base IndexedDB dédiée (smartauth_sync), avec ses tables pending_changes, pending_conflicts et local_tombstones. Vider le cache du Service Worker ou activer une nouvelle version n'y touche pas.
Attention
Ce qui détruit la file, ce sont les actions qui effacent les données du site : "Clear site data" dans les DevTools, la suppression des données du navigateur, la désinstallation de la PWA. Avant de conseiller l'une de ces manipulations à un utilisateur en dépannage, vérifiez sync.pendingCount.
Diagnostiquer un client bloqué sur une ancienne version
- DevTools, Application > Service Workers : un Service Worker en état
waitingsignale une mise à jour prête mais non activée - vérifier que
cleanupOutdatedCaches()est bien appelé, sinon les anciens caches s'accumulent - en dernier recours, Unregister puis rechargement avec vidage du cache
Proposer l'installation
Depuis SmartCommon 1.0.379, le hook useInstallPrompt et le composant InstallPrompt gèrent la détection de l'installation et l'invitation à installer.
import { InstallPrompt } from "@cap-rel/smartcommon";
<InstallPrompt
labels={{
title: t("installPrompt.title"),
message: t("installPrompt.message"),
installButton: t("installPrompt.install"),
dismissButton: t("installPrompt.later"),
gotItButton: t("installPrompt.gotIt"),
}}
/>
À monter une seule fois, haut dans l'arbre, à côté de <UpdatePrompt /> et à l'intérieur du provider d'API. Le composant ne rend rien tant qu'il n'y a rien à proposer.
Attention
Contrairement à la plupart des composants de SmartCommon, InstallPrompt n'a pas de bundle de traductions : ses libellés par défaut n'existent qu'en anglais et locales.fr.InstallPrompt n'existe pas. Une application francophone doit donc passer ses propres labels. Les clés des boutons sont installButton, dismissButton et gotItButton.
Quelques réalités à connaître :
- sur iOS,
beforeinstallpromptn'existe pas et il n'y a pas de prompt natif : seules des instructions manuelles sont possibles - "la session n'est pas en mode standalone" ne signifie pas "l'application n'est pas installée"
navigator.getInstalledRelatedApps()ne répond que si le manifest déclarerelated_applicationspointant vers lui-même. Le manifest dynamique de smartauth le fait ; avec un manifest statique, c'est à vous de l'écrire
Le détail complet est dans la documentation interne PWA_INSTALL.md.
Build et déploiement
# Build de production
npm run build
# Previsualiser le build
npm run preview
# Le build genere :
# - dist/index.html
# - dist/assets/*.js
# - dist/assets/*.css
# - dist/sw.js (Service Worker)
Déploiement dans le module Dolibarr :
cd mobile
npm run build
cp -r dist/* ../pwa/
ou, avec le Makefile du module :
make pwa
La cible make pwa fait plus qu'un npm run build : elle incrémente le numéro de build, force VITE_API_URL en relatif, dérive la liste des langues des sous-dossiers de public/locales/, puis restaure le .env de développement.
Vérifier une PWA
Dans les DevTools
- Application > Manifest : le manifest est chargé, sans erreur d'origine, et les icônes s'affichent
- Application > Service Workers : le Service Worker est
activated and running - Application > Cache Storage : le precache contient le bundle,
index.htmlet les fichiers delocales/
Tester le démarrage hors ligne
C'est le test qui compte, et il doit être fait dans cet ordre :
- charger l'application en ligne, attendre que le Service Worker soit actif
- naviguer vers une page interne, par exemple
/interventions - cocher Offline dans l'onglet Network
- recharger la page (et non simplement naviguer)
Si un écran blanc apparaît à cette étape, le fallback de navigation manque.
Lighthouse
Onglet Lighthouse des DevTools, catégorie Progressive Web App. Critères attendus : HTTPS (ou localhost), manifest valide avec icônes, Service Worker enregistré, fonctionnement hors connexion, design responsive.
Pièges à connaître
| Symptôme | Cause |
|---|---|
| "ma configuration VitePWA ne fait rien" | options workbox utilisées en mode injectManifest |
| écran blanc au rechargement hors ligne | NavigationRoute absente du sw.js |
| manifest introuvable en 404 | lien vers api.php/manifest au lieu de api.php/manifest.webmanifest |
| "le manifest doit avoir la même origine" | front et API sur deux origines, pas de proxy Vite |
| application inutilisable hors ligne sans erreur | bundle plus gros que maximumFileSizeToCacheInBytes |
| clés de traduction brutes hors ligne | json absent de globPatterns |
| Service Worker enregistré deux fois | injectRegister: "auto" et appel manuel à registerSW |
| manifest ignoré ou incohérent | <link rel="manifest"> conservé avec un manifest statique Vite |