Next.js 16 App Router PWA + Supabase SSR: бесконечная загрузка на мобильном устройстве/PWA (работник ручного обслуживаниJavascript

Форум по Javascript
Ответить
Anonymous
 Next.js 16 App Router PWA + Supabase SSR: бесконечная загрузка на мобильном устройстве/PWA (работник ручного обслуживани

Сообщение Anonymous »

URL-адрес приложения: https://pharmagoli-loyalty.vercel.app/
Контекст

У меня есть приложение Next.js 16 (App Router) + React + TypeScript на Vercel, использующее Supabase Auth/RLS. Браузер для настольных компьютеров в основном работает нормально (нет ошибок консоли), но на мобильном устройстве (и особенно с установленным PWA) я часто получаю бесконечную загрузку/пустой экран. Иногда страница входа появляется только после нескольких обновлений; иногда отправка учетных данных ничего не дает.
Я не использую next-pwa, поскольку Next.js 16 Turbopack + next-pwa (плагин веб-пакета) несовместимы. Вместо этого я использую ручной сервисный работник в /public/sw.js.
Ожидается
  • Мобильный браузер и установленный PWA загружаются надежно
  • Вход работает надежно и перенаправляется правильно (без бесконечной загрузки)
Реально
  • Мобильные устройства/PWA часто зависают на бесконечной загрузке или пустом экране
  • Вход ненадежен до нескольких обновлений
  • Рабочий стол выглядит нормально
Промежуточное программное обеспечение (шлюз аутентификации Supabase SSR)
Я защищаю закрытые маршруты с помощью промежуточного программного обеспечения @supabase/ssr с помощью getSession():
import { createServerClient } from '@supabase/ssr'
import { NextRequest, NextResponse } from 'next/server'

const publicRoutes = ['/auth', '/api/auth', '/offline']

export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl

if (publicRoutes.some(route => pathname.startsWith(route))) return NextResponse.next()

let response = NextResponse.next({ request: { headers: request.headers } })

const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set({ name, value, ...options })
response.cookies.set({ name, value, ...options })
})
},
},
}
)

const { data: { session }, error } = await supabase.auth.getSession()

if (!session || error) {
const loginUrl = new URL('/auth/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}

return response
}

export const config = {
matcher: ['/((?!_next/static|_next/image|favicon\\.ico|manifest\\.json|icons|sw\\.js|workbox-.*\\.js|fallback-.*\\.js).*)'],
}


Работник ручного обслуживания (public/sw.js)
Я думаю, что виноват именно он. Он:
  • управляет навигацией (request.mode === 'navigate') с сетью + автономный резерв (без кэширования)
  • кэширует /_next/static/* с помощью CacheFirst
  • кэширует изображения/значки с помощью StaleWhileRevalidate
  • для «всего остального», с которым он делает NetworkFirst Тайм-аут 3 с, и ответ кэшируется в кеше страниц.
// Everything else → NetworkFirst (3s timeout) and cache into PAGES_CACHE
// (full file available if needed; this is the key part)

Я подозреваю, что на мобильных устройствах/PWA внутренние запросы Next.js App Router (RSC/Flight/data, text/x-comComponent, заголовки типа RSC / Next-Router-State-Tree) обрабатываются как «все остальное» и кэшируются. Это может привести к устаревшим/несогласованным полезным нагрузкам при развертывании и привести к бесконечной загрузке.

Вопрос
Какова рекомендуемая стратегия сервисного работника для приложений Next.js 16 App Router (особенно с аутентификацией) при использовании ПО вручную?
В частности:
  • Должны ли внутренние запросы App Router (RSC/Flight/data) рассматриваться как NetworkOnly и никогда не кэшироваться? Как мне их достоверно обнаружить в ПО(RSCзаголовок , принять: text/x-comComponent и т. д.)?
  • Безопасно ли кэшировать что-либо, кроме /_next/static/* и значков? Должен ли я полностью удалить «NetworkFirst+cache» для нестатических запросов?
  • Известно ли взаимодействие между промежуточным программным обеспечением Supabase SSR (файлы cookie/сеанс) и PWA/ПО, которое может вызвать бесконечную загрузку мобильных устройств?
Если полезно, я могу вставить полный файл sw.js и трассировку мобильной сети (кешируются ли запросы / 302 петли/остановка выборки).
/**
* Pharmagoli Loyalty — Service Worker
*
* Handles caching strategies, offline fallback, and push notifications.
* This is a manual SW that replaces the broken next-pwa generated one
* (next-pwa does not work with Turbopack / Next.js 16).
*
* Strategies:
* - Navigation requests → NetworkFirst (fresh pages, offline fallback)
* - Supabase / API calls → NetworkOnly (never cache)
* - _next/static/* → CacheFirst (immutable, hashed filenames)
* - Images / icons → StaleWhileRevalidate
* - Everything else → NetworkOnly (no cache)
*/

const CACHE_VERSION = 'v4'
const PAGES_CACHE = 'pharmagoli-pages-' + CACHE_VERSION
const STATIC_CACHE = 'pharmagoli-static-' + CACHE_VERSION
const ASSETS_CACHE = 'pharmagoli-assets-' + CACHE_VERSION

var EXPECTED_CACHES = [PAGES_CACHE, STATIC_CACHE, ASSETS_CACHE]

// ── Install ─────────────────────────────────────────────────────────────────
self.addEventListener('install', function (event) {
// Activate immediately — don't wait for the old SW to release clients
self.skipWaiting()

// Pre-cache the offline fallback page
event.waitUntil(
caches.open(PAGES_CACHE).then(function (cache) {
return cache.add('/offline')
})
)
})

// ── Activate ────────────────────────────────────────────────────────────────
self.addEventListener('activate', function (event) {
event.waitUntil(
Promise.all([
// Take control of all open tabs/windows immediately
self.clients.claim(),

// Delete ALL old caches — including stale next-pwa caches
// (workbox-precache-*, next-static, next-image, etc.)
caches.keys().then(function (keys) {
return Promise.all(
keys
.filter(function (key) { return EXPECTED_CACHES.indexOf(key) === -1 })
.map(function (key) { return caches.delete(key) })
)
}),
])
)
})

// ── Fetch ───────────────────────────────────────────────────────────────────
self.addEventListener('fetch', function (event) {
var request = event.request
var url = new URL(request.url)

// Only handle GET requests
if (request.method !== 'GET') return

// Ignore requests that browsers may send but SW cannot safely handle
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') return

// ── NetworkOnly: Supabase API (auth, REST, RPC, realtime) ──
if (url.hostname.indexOf('supabase.co') !== -1) return

// ── NetworkOnly: internal Next.js API routes ──
if (url.pathname.indexOf('/api/') === 0) return

// ── Navigation requests (page loads) → Network with offline fallback ──
// We do NOT cache navigation responses to avoid issues with redirects
// (SW navigation requests use redirect:"manual" which produces opaque
// redirect responses that cannot be cached or reused).
// Only provide offline fallback when the network is completely down.
if (request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match('/offline').then(function (offlinePage) {
return offlinePage || new Response(
'Offline
Verifica la connessione.
',
{ status: 200, headers: { 'Content-Type': 'text/html' } }
)
})
})
)
return
}

// ── Static assets (_next/static/) → CacheFirst (immutable hashed URLs) ──
if (url.pathname.indexOf('/_next/static/') === 0) {
event.respondWith(
caches.match(request).then(function (cached) {
if (cached) return cached
return fetch(request).then(function (response) {
if (response.ok) {
var clone = response.clone()
caches.open(STATIC_CACHE).then(function (cache) { cache.put(request, clone) })
}
return response
}).catch(function () {
return new Response('', { status: 408 })
})
})
)
return
}

// ── Images & icons → StaleWhileRevalidate ──
if (url.pathname.indexOf('/_next/image') === 0 || url.pathname.indexOf('/icons/') === 0) {
event.respondWith(
caches.match(request).then(function (cached) {
var networkFetch = fetch(request)
.then(function (response) {
if (response.ok) {
var clone = response.clone()
caches.open(ASSETS_CACHE).then(function (cache) { cache.put(request, clone) })
}
return response
})
.catch(function () { return cached })

return cached || networkFetch
})
)
return
}

// ── Everything else → NetworkOnly (no cache) ──
event.respondWith(
fetch(request).catch(function () {
return new Response('Offline', { status: 503 })
})
)
})

// ── Push received ───────────────────────────────────────────────────────────
self.addEventListener('push', function (event) {
if (!event.data) return

var payload
try {
payload = event.data.json()
} catch (e) {
payload = { title: 'Pharmagoli', body: event.data.text(), url: '/customer/dashboard' }
}

var title = payload.title || 'Pharmagoli'
var body = payload.body || ''
var url = payload.url || '/customer/dashboard'
var data = payload.data || {}

event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: '/icons/icon-192.png',
badge: '/icons/icon-192.png',
data: { url: url, ...data },
vibrate: [200, 100, 200],
requireInteraction: false,
})
)
})

// ── Notification click → open / focus app ───────────────────────────────────
self.addEventListener('notificationclick', function (event) {
event.notification.close()

var targetUrl = (event.notification.data && event.notification.data.url) || '/customer/dashboard'

event.waitUntil(
clients
.matchAll({ type: 'window', includeUncontrolled: true })
.then(function (clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList
if ('focus' in client) {
client.focus()
if ('navigate' in client) client.navigate(targetUrl)
return
}
}
return clients.openWindow(targetUrl)
})
)
})


Подробнее здесь: https://stackoverflow.com/questions/798 ... pwa-manual
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»