**Progressive Web App (PWA)** — это веб-приложение, которое работает как нативное: может быть установлено на рабочий стол, работает офлайн, отправляет push-уведомления. Три ключевых компонента PWA: HTTPS, Web App Manifest и Service Worker.
npm install -D vite-plugin-pwa// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate', // автообновление SW
manifest: {
name: 'Моё Vue приложение',
short_name: 'VueApp',
description: 'Описание приложения',
theme_color: '#4DBA87',
background_color: '#ffffff',
display: 'standalone', // как нативное приложение
icons: [
{ src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 300 },
},
},
],
},
}),
],
})Manifest.json описывает приложение для браузера и ОС. Без него браузер не предложит установку:
{
"name": "Моё Vue приложение",
"short_name": "VueApp",
"start_url": "/",
"display": "standalone",
"theme_color": "#4DBA87",
"background_color": "#ffffff",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "purpose": "maskable" }
]
}Service Worker — это JavaScript-файл, работающий в фоне отдельно от страницы. Он перехватывает сетевые запросы:
Cache First — сначала кэш, затем сеть (иконки, шрифты)
Network First — сначала сеть, при ошибке — кэш (API данные)
Stale While Revalidate — сразу кэш, параллельно обновляет (страницы)
Cache Only — только кэш (офлайн-режим)
Network Only — только сеть (без кэширования)// src/composables/useInstallPrompt.ts
import { ref, onMounted } from 'vue'
export function useInstallPrompt() {
const installPrompt = ref(null)
const isInstallable = ref(false)
onMounted(() => {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault() // не показываем автоматический баннер
installPrompt.value = e
isInstallable.value = true
})
})
async function install() {
if (!installPrompt.value) return
const result = await installPrompt.value.prompt()
if (result.outcome === 'accepted') {
isInstallable.value = false
installPrompt.value = null
}
}
return { isInstallable, install }
}// src/composables/useUpdateSW.ts
import { useRegisterSW } from 'virtual:pwa-register/vue'
export function useUpdateSW() {
const { needRefresh, updateServiceWorker } = useRegisterSW({
onRegistered(sw) {
console.log('SW зарегистрирован:', sw)
},
onRegisterError(error) {
console.error('Ошибка регистрации SW:', error)
},
})
return { needRefresh, updateServiceWorker }
}<!-- Показываем уведомление об обновлении -->
<template>
<div v-if="needRefresh" class="update-banner">
Доступно обновление!
<button @click="updateServiceWorker()">Обновить</button>
</div>
</template>При правильной настройке Workbox, приложение будет работать даже без интернета: статические файлы (HTML, CSS, JS) берутся из кэша. Для отображения офлайн-статуса:
import { useOnline } from '@vueuse/core'
const isOnline = useOnline()Эмуляция Service Worker: кэширование запросов со стратегиями Cache First, Network First и Stale While Revalidate
// ============================================
// Эмуляция Service Worker — стратегии кэширования
// ============================================
// Service Worker перехватывает fetch-запросы и решает:
// взять из кэша или сходить в сеть.
// Здесь мы симулируем эту логику в виде обычного класса.
// Симуляция кэша (аналог Cache API)
class Cache {
constructor(name) {
this.name = name
this._store = new Map()
}
async put(url, response) {
this._store.set(url, { response, timestamp: Date.now() })
console.log(` [Cache "${this.name}"] сохранён: ${url}`)
}
async match(url) {
return this._store.get(url) || null
}
async delete(url) {
this._store.delete(url)
}
get size() { return this._store.size }
}
// Симуляция сети
async function networkFetch(url, options = {}) {
const { latency = 100, shouldFail = false } = options
await new Promise(r => setTimeout(r, latency))
if (shouldFail) {
throw new Error('Network error: offline')
}
return {
url,
body: { data: `Свежие данные из сети для ${url}`, time: Date.now() },
ok: true,
clone() { return { ...this } },
}
}
// ============================================
// Стратегии кэширования
// ============================================
class ServiceWorkerStrategies {
constructor() {
this.caches = {}
}
getCache(name) {
if (!this.caches[name]) this.caches[name] = new Cache(name)
return this.caches[name]
}
// Cache First: сначала кэш, при промахе — сеть
async cacheFirst(url, cacheName, networkOptions) {
const cache = this.getCache(cacheName)
const cached = await cache.match(url)
if (cached) {
console.log(` [CacheFirst] HIT: ${url}`)
return cached.response
}
console.log(` [CacheFirst] MISS: ${url} — идём в сеть`)
const response = await networkFetch(url, networkOptions)
await cache.put(url, response.clone())
return response
}
// Network First: сначала сеть, при ошибке — кэш
async networkFirst(url, cacheName, networkOptions) {
const cache = this.getCache(cacheName)
try {
console.log(` [NetworkFirst] запрос к сети: ${url}`)
const response = await networkFetch(url, networkOptions)
await cache.put(url, response.clone())
return response
} catch (err) {
console.log(` [NetworkFirst] сеть недоступна: ${err.message}`)
const cached = await cache.match(url)
if (cached) {
console.log(` [NetworkFirst] возвращаем из кэша: ${url}`)
return cached.response
}
throw new Error(`Нет ни сети, ни кэша для ${url}`)
}
}
// Stale While Revalidate: сразу кэш + фоновое обновление
async staleWhileRevalidate(url, cacheName, networkOptions) {
const cache = this.getCache(cacheName)
const cached = await cache.match(url)
// Запускаем обновление в фоне
const updatePromise = networkFetch(url, networkOptions)
.then(response => {
cache.put(url, response.clone())
console.log(` [SWR] фоновое обновление завершено: ${url}`)
})
.catch(err => console.log(` [SWR] фоновое обновление провалилось: ${err.message}`))
if (cached) {
console.log(` [SWR] возвращаем из кэша сразу: ${url}`)
// Обновление идёт в фоне
return cached.response
}
// Кэша нет — ждём сеть
console.log(` [SWR] кэша нет, ждём сеть: ${url}`)
return await updatePromise.then(() => cache.match(url).then(c => c.response))
}
}
// ============================================
// Демонстрация
// ============================================
async function demo() {
const sw = new ServiceWorkerStrategies()
console.log('=== Cache First (статические файлы) ===')
await sw.cacheFirst('/assets/logo.png', 'static-v1', { latency: 50 })
await sw.cacheFirst('/assets/logo.png', 'static-v1', { latency: 50 }) // из кэша
console.log('\n=== Network First (API) ===')
await sw.networkFirst('/api/users', 'api-cache', { latency: 30 })
await sw.networkFirst('/api/users', 'api-cache', { latency: 30, shouldFail: true }) // кэш
console.log('\n=== Network First — офлайн без кэша ===')
try {
await sw.networkFirst('/api/new-endpoint', 'api-cache', { shouldFail: true })
} catch (e) {
console.log(` Ошибка: ${e.message}`)
}
console.log('\n=== Stale While Revalidate (страницы) ===')
// Сначала заполняем кэш
const cache = sw.getCache('pages-v1')
await cache.put('/about', { body: 'Старые данные страницы /about', ok: true, clone() { return this } })
await sw.staleWhileRevalidate('/about', 'pages-v1', { latency: 200 })
console.log('\n=== Статистика кэшей ===')
for (const [name, cache] of Object.entries(sw.caches)) {
console.log(` Cache "${name}": ${cache.size} записей`)
}
}
demo()**Progressive Web App (PWA)** — это веб-приложение, которое работает как нативное: может быть установлено на рабочий стол, работает офлайн, отправляет push-уведомления. Три ключевых компонента PWA: HTTPS, Web App Manifest и Service Worker.
npm install -D vite-plugin-pwa// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate', // автообновление SW
manifest: {
name: 'Моё Vue приложение',
short_name: 'VueApp',
description: 'Описание приложения',
theme_color: '#4DBA87',
background_color: '#ffffff',
display: 'standalone', // как нативное приложение
icons: [
{ src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 300 },
},
},
],
},
}),
],
})Manifest.json описывает приложение для браузера и ОС. Без него браузер не предложит установку:
{
"name": "Моё Vue приложение",
"short_name": "VueApp",
"start_url": "/",
"display": "standalone",
"theme_color": "#4DBA87",
"background_color": "#ffffff",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "purpose": "maskable" }
]
}Service Worker — это JavaScript-файл, работающий в фоне отдельно от страницы. Он перехватывает сетевые запросы:
Cache First — сначала кэш, затем сеть (иконки, шрифты)
Network First — сначала сеть, при ошибке — кэш (API данные)
Stale While Revalidate — сразу кэш, параллельно обновляет (страницы)
Cache Only — только кэш (офлайн-режим)
Network Only — только сеть (без кэширования)// src/composables/useInstallPrompt.ts
import { ref, onMounted } from 'vue'
export function useInstallPrompt() {
const installPrompt = ref(null)
const isInstallable = ref(false)
onMounted(() => {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault() // не показываем автоматический баннер
installPrompt.value = e
isInstallable.value = true
})
})
async function install() {
if (!installPrompt.value) return
const result = await installPrompt.value.prompt()
if (result.outcome === 'accepted') {
isInstallable.value = false
installPrompt.value = null
}
}
return { isInstallable, install }
}// src/composables/useUpdateSW.ts
import { useRegisterSW } from 'virtual:pwa-register/vue'
export function useUpdateSW() {
const { needRefresh, updateServiceWorker } = useRegisterSW({
onRegistered(sw) {
console.log('SW зарегистрирован:', sw)
},
onRegisterError(error) {
console.error('Ошибка регистрации SW:', error)
},
})
return { needRefresh, updateServiceWorker }
}<!-- Показываем уведомление об обновлении -->
<template>
<div v-if="needRefresh" class="update-banner">
Доступно обновление!
<button @click="updateServiceWorker()">Обновить</button>
</div>
</template>При правильной настройке Workbox, приложение будет работать даже без интернета: статические файлы (HTML, CSS, JS) берутся из кэша. Для отображения офлайн-статуса:
import { useOnline } from '@vueuse/core'
const isOnline = useOnline()Эмуляция Service Worker: кэширование запросов со стратегиями Cache First, Network First и Stale While Revalidate
// ============================================
// Эмуляция Service Worker — стратегии кэширования
// ============================================
// Service Worker перехватывает fetch-запросы и решает:
// взять из кэша или сходить в сеть.
// Здесь мы симулируем эту логику в виде обычного класса.
// Симуляция кэша (аналог Cache API)
class Cache {
constructor(name) {
this.name = name
this._store = new Map()
}
async put(url, response) {
this._store.set(url, { response, timestamp: Date.now() })
console.log(` [Cache "${this.name}"] сохранён: ${url}`)
}
async match(url) {
return this._store.get(url) || null
}
async delete(url) {
this._store.delete(url)
}
get size() { return this._store.size }
}
// Симуляция сети
async function networkFetch(url, options = {}) {
const { latency = 100, shouldFail = false } = options
await new Promise(r => setTimeout(r, latency))
if (shouldFail) {
throw new Error('Network error: offline')
}
return {
url,
body: { data: `Свежие данные из сети для ${url}`, time: Date.now() },
ok: true,
clone() { return { ...this } },
}
}
// ============================================
// Стратегии кэширования
// ============================================
class ServiceWorkerStrategies {
constructor() {
this.caches = {}
}
getCache(name) {
if (!this.caches[name]) this.caches[name] = new Cache(name)
return this.caches[name]
}
// Cache First: сначала кэш, при промахе — сеть
async cacheFirst(url, cacheName, networkOptions) {
const cache = this.getCache(cacheName)
const cached = await cache.match(url)
if (cached) {
console.log(` [CacheFirst] HIT: ${url}`)
return cached.response
}
console.log(` [CacheFirst] MISS: ${url} — идём в сеть`)
const response = await networkFetch(url, networkOptions)
await cache.put(url, response.clone())
return response
}
// Network First: сначала сеть, при ошибке — кэш
async networkFirst(url, cacheName, networkOptions) {
const cache = this.getCache(cacheName)
try {
console.log(` [NetworkFirst] запрос к сети: ${url}`)
const response = await networkFetch(url, networkOptions)
await cache.put(url, response.clone())
return response
} catch (err) {
console.log(` [NetworkFirst] сеть недоступна: ${err.message}`)
const cached = await cache.match(url)
if (cached) {
console.log(` [NetworkFirst] возвращаем из кэша: ${url}`)
return cached.response
}
throw new Error(`Нет ни сети, ни кэша для ${url}`)
}
}
// Stale While Revalidate: сразу кэш + фоновое обновление
async staleWhileRevalidate(url, cacheName, networkOptions) {
const cache = this.getCache(cacheName)
const cached = await cache.match(url)
// Запускаем обновление в фоне
const updatePromise = networkFetch(url, networkOptions)
.then(response => {
cache.put(url, response.clone())
console.log(` [SWR] фоновое обновление завершено: ${url}`)
})
.catch(err => console.log(` [SWR] фоновое обновление провалилось: ${err.message}`))
if (cached) {
console.log(` [SWR] возвращаем из кэша сразу: ${url}`)
// Обновление идёт в фоне
return cached.response
}
// Кэша нет — ждём сеть
console.log(` [SWR] кэша нет, ждём сеть: ${url}`)
return await updatePromise.then(() => cache.match(url).then(c => c.response))
}
}
// ============================================
// Демонстрация
// ============================================
async function demo() {
const sw = new ServiceWorkerStrategies()
console.log('=== Cache First (статические файлы) ===')
await sw.cacheFirst('/assets/logo.png', 'static-v1', { latency: 50 })
await sw.cacheFirst('/assets/logo.png', 'static-v1', { latency: 50 }) // из кэша
console.log('\n=== Network First (API) ===')
await sw.networkFirst('/api/users', 'api-cache', { latency: 30 })
await sw.networkFirst('/api/users', 'api-cache', { latency: 30, shouldFail: true }) // кэш
console.log('\n=== Network First — офлайн без кэша ===')
try {
await sw.networkFirst('/api/new-endpoint', 'api-cache', { shouldFail: true })
} catch (e) {
console.log(` Ошибка: ${e.message}`)
}
console.log('\n=== Stale While Revalidate (страницы) ===')
// Сначала заполняем кэш
const cache = sw.getCache('pages-v1')
await cache.put('/about', { body: 'Старые данные страницы /about', ok: true, clone() { return this } })
await sw.staleWhileRevalidate('/about', 'pages-v1', { latency: 200 })
console.log('\n=== Статистика кэшей ===')
for (const [name, cache] of Object.entries(sw.caches)) {
console.log(` Cache "${name}": ${cache.size} записей`)
}
}
demo()Реализуй класс `SimpleCache` с методами: `set(key, value, ttl)` — сохраняет значение с временем жизни в миллисекундах (если ttl не указан, хранится вечно); `get(key)` — возвращает значение если не истёк TTL, иначе null и удаляет запись; `has(key)` — true если ключ есть и не истёк; `delete(key)` — удаляет запись; `size` — количество актуальных записей.
В конструкторе: this._store = new Map(). В set: this._store.set(key, { value, expiresAt: ttl !== undefined ? Date.now() + ttl : Infinity }). В get: проверь entry.expiresAt <= Date.now() — если да, вызови this.delete(key) и верни null. В size: считай только записи, у которых expiresAt > Date.now().
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке