Progressive Web Apps (PWA)

PWAs with Vue.js

18 min Lesson 23 of 30

Building PWAs with Vue.js

Vue.js provides first-class support for Progressive Web Apps through the Vue CLI PWA plugin. This lesson covers how to create, configure, and deploy production-ready PWAs with Vue.

Creating a Vue PWA Project

The Vue CLI makes it incredibly easy to scaffold a new PWA project with all the necessary configuration.

# Install Vue CLI globally (if not installed) npm install -g @vue/cli # Create a new project with PWA support vue create my-vue-pwa # Select "Manually select features" # Choose: Babel, PWA, Router, Vuex (optional) # Or add PWA to existing project vue add pwa # Project structure: src/ main.js App.vue registerServiceWorker.js public/ index.html manifest.json img/icons/ vue.config.js

Understanding @vue/cli-plugin-pwa

The Vue CLI PWA plugin is built on top of Workbox and provides automatic service worker generation with sensible defaults.

// package.json - Dependencies added by plugin { "dependencies": { "register-service-worker": "^1.7.2" }, "devDependencies": { "@vue/cli-plugin-pwa": "~5.0.0" } }
Pro Tip: The Vue PWA plugin automatically handles service worker registration, precaching, and runtime caching with minimal configuration needed.

Service Worker Registration

Vue CLI creates a registerServiceWorker.js file that handles the service worker lifecycle.

// src/registerServiceWorker.js import { register } from 'register-service-worker'; if (process.env.NODE_ENV === 'production') { register(`${process.env.BASE_URL}service-worker.js`, { ready() { console.log( 'App is being served from cache by a service worker.\n' + 'For more details, visit https://goo.gl/AFskqB' ); }, registered() { console.log('Service worker has been registered.'); }, cached() { console.log('Content has been cached for offline use.'); }, updatefound() { console.log('New content is downloading.'); }, updated(registration) { console.log('New content is available; please refresh.'); // Dispatch custom event for update notification document.dispatchEvent( new CustomEvent('swUpdated', { detail: registration }) ); }, offline() { console.log('No internet connection found. App is running in offline mode.'); }, error(error) { console.error('Error during service worker registration:', error); } }); }

Configuring PWA in vue.config.js

Customize your PWA behavior through the vue.config.js configuration file.

// vue.config.js module.exports = { pwa: { name: 'My Vue PWA', themeColor: '#4DBA87', msTileColor: '#000000', appleMobileWebAppCapable: 'yes', appleMobileWebAppStatusBarStyle: 'black', // Workbox plugin options workboxPluginMode: 'GenerateSW', // or 'InjectManifest' workboxOptions: { // Runtime caching rules runtimeCaching: [ { urlPattern: new RegExp('^https://api\\.example\\.com/'), handler: 'NetworkFirst', options: { networkTimeoutSeconds: 10, cacheName: 'api-cache', expiration: { maxEntries: 50, maxAgeSeconds: 5 * 60 // 5 minutes }, cacheableResponse: { statuses: [0, 200] } } }, { urlPattern: new RegExp('\\.(?:png|jpg|jpeg|svg|gif)$'), handler: 'CacheFirst', options: { cacheName: 'image-cache', expiration: { maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days } } }, { urlPattern: new RegExp('^https://fonts\\.googleapis\\.com/'), handler: 'StaleWhileRevalidate', options: { cacheName: 'google-fonts-stylesheets' } }, { urlPattern: new RegExp('^https://fonts\\.gstatic\\.com/'), handler: 'CacheFirst', options: { cacheName: 'google-fonts-webfonts', expiration: { maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year }, cacheableResponse: { statuses: [0, 200] } } } ], // Skip waiting to activate new service worker immediately skipWaiting: true, clientsClaim: true, // Exclude specific files from precaching exclude: [/\.map$/, /_redirects/], // Maximum file size to precache (2MB) maximumFileSizeToCacheInBytes: 2 * 1024 * 1024 }, // Manifest options manifestOptions: { name: 'My Vue Progressive Web App', short_name: 'Vue PWA', description: 'A powerful PWA built with Vue.js', start_url: '.', display: 'standalone', background_color: '#ffffff', theme_color: '#4DBA87', icons: [ { src: './img/icons/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' }, { src: './img/icons/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' }, { src: './img/icons/android-chrome-maskable-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, { src: './img/icons/android-chrome-maskable-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' } ] }, // Icon paths iconPaths: { faviconSVG: 'img/icons/favicon.svg', favicon32: 'img/icons/favicon-32x32.png', favicon16: 'img/icons/favicon-16x16.png', appleTouchIcon: 'img/icons/apple-touch-icon-152x152.png', maskIcon: 'img/icons/safari-pinned-tab.svg', msTileImage: 'img/icons/msapplication-icon-144x144.png' } } };
Important: Service workers only work in production builds. Use npm run build and serve the dist folder to test PWA features: npx serve -s dist

Update Notification Component

Create a component to notify users when a new version is available.

<!-- src/components/UpdateNotification.vue --> <template> <div v-if="updateExists" class="update-notification"> <p>A new version is available!</p> <button @click="refreshApp">Update Now</button> </div> </template> <script> export default { name: 'UpdateNotification', data() { return { refreshing: false, registration: null, updateExists: false }; }, created() { document.addEventListener('swUpdated', this.updateAvailable, { once: true }); navigator.serviceWorker?.addEventListener('controllerchange', () => { if (this.refreshing) return; this.refreshing = true; window.location.reload(); }); }, methods: { updateAvailable(event) { this.registration = event.detail; this.updateExists = true; }, refreshApp() { this.updateExists = false; if (!this.registration || !this.registration.waiting) return; this.registration.waiting.postMessage({ type: 'SKIP_WAITING' }); } } }; </script> <style scoped> .update-notification { position: fixed; bottom: 20px; right: 20px; background: #4DBA87; color: white; padding: 15px 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); z-index: 1000; } .update-notification button { margin-left: 10px; padding: 8px 16px; background: white; color: #4DBA87; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } </style>

Offline Detection Composable (Vue 3)

Create a composable function for detecting online/offline status in Vue 3.

// src/composables/useOnlineStatus.js import { ref, onMounted, onUnmounted } from 'vue'; export function useOnlineStatus() { const isOnline = ref(navigator.onLine); const updateOnlineStatus = () => { isOnline.value = navigator.onLine; }; onMounted(() => { window.addEventListener('online', updateOnlineStatus); window.addEventListener('offline', updateOnlineStatus); }); onUnmounted(() => { window.removeEventListener('online', updateOnlineStatus); window.removeEventListener('offline', updateOnlineStatus); }); return { isOnline }; } // Usage in component <template> <div> <div v-if="!isOnline" class="offline-banner"> You are currently offline </div> <!-- Component content --> </div> </template> <script setup> import { useOnlineStatus } from '@/composables/useOnlineStatus'; const { isOnline } = useOnlineStatus(); </script>

Using InjectManifest Mode

For advanced use cases, you can use InjectManifest mode to have full control over the service worker.

// vue.config.js module.exports = { pwa: { workboxPluginMode: 'InjectManifest', workboxOptions: { swSrc: 'src/service-worker.js', swDest: 'service-worker.js' } } }; // src/service-worker.js import { precacheAndRoute } from 'workbox-precaching'; import { registerRoute } from 'workbox-routing'; import { CacheFirst, NetworkFirst } from 'workbox-strategies'; // Precache files precacheAndRoute(self.__WB_MANIFEST); // Custom caching strategies registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ cacheName: 'api-cache', networkTimeoutSeconds: 10 }) ); registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'images-cache' }) ); // Listen for skip waiting message self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } });
Exercise:
  1. Create a new Vue project with PWA support
  2. Customize the vue.config.js with custom caching strategies
  3. Create the UpdateNotification component
  4. Implement the useOnlineStatus composable
  5. Add custom icons to the public/img/icons folder
  6. Build the project and test offline functionality
  7. Test the update notification by making changes and rebuilding
  8. Inspect service worker and cache in Chrome DevTools
Best Practice: Use workboxPluginMode: 'GenerateSW' for most use cases as it provides automatic optimization. Only use InjectManifest when you need advanced custom logic in your service worker.