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:
- Create a new Vue project with PWA support
- Customize the vue.config.js with custom caching strategies
- Create the UpdateNotification component
- Implement the useOnlineStatus composable
- Add custom icons to the public/img/icons folder
- Build the project and test offline functionality
- Test the update notification by making changes and rebuilding
- 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.