PWAها (Progressive Web Application) دسترسی به وب اپلیکیشنها را همراه با ویژگیها و تجربه کاربری برنامههای موبایل native ارائه میدهند. با استفاده از Next.js، میتوانیم انواع مختلف وب اپلیکیشن PWA بسازیم که تجربهای یکپارچه و شبیه به برنامه را در همه پلتفرمها، بدون نیاز به چندین پایگاه کد و یا تاییدیه app store ارائه میدهد.
Next.js پشتیبانی داخلی برای ایجاد یک مانیفست وب اپلیکیشن با استفاده از App Router ارائه میدهد. میتوانیم یک فایل مانیفست استاتیک یا داینامیک ایجاد کنیم:
به عنوان مثال، یک فایل app/manifest.ts
یا app/manifest.json
میسازیم:
// app/manifest.ts import type { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { name: 'Next.js PWA', short_name: 'NextPWA', description: 'A Progressive Web App built with Next.js', start_url: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#000000', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', }, ], } }
این فایل باید حاوی اطلاعاتی در مورد نام، آیکونها و نحوه نمایش آن به عنوان آیکون در دستگاه کاربر باشد. این فایل شرایطی را برا کاربران فراهم میکند تا بتوانند PWA ما را روی صفحه اصلی خود نصب کنند و تجربهای شبیه به کار کردن با اپلیکیشن native را داشته باشند.
میتوانیم از ابزارهایی مانند favicon generatorها برای ایجاد مجموعه آیکونهای مختلف و قرار دادن فایلهای تولید شده در فولدر public/
استفاده کنیم.
تمام مرورگرهای مدرن از Web Push Notificationها پشتیبانی میکنند، از جمله:
این باعث میشود PWAها جایگزین مناسبی برای اپلیکیشنهای native باشند. قابل ذکر است ما میتوانیم بدون نیاز به پشتیبانی آفلاین، دستورهای نصب را راهاندازی کنیم.
Web Push Notificationها به ما این امکان را میدهند تا حتی زمانی که کاربران به طور فعال از برنامه ما استفاده نمیکنند، با آن درگیر شوند. در ادامه نحوه پیادهسازی آنها در برنامه Next.js را داریم:
ابتدا، کامپوننت main page را در app/page.tsx
میسازیم. برای درک بهتر، آن را به بخشهای کوچکتر تقسیم میکنیم. ابتدا، برخی از importها و ابزارهای مورد نیاز خود را اضافه میکنیم. اگر Server Actionهای رفرنس شده در برنامه نداریم، فعلا ایرادی ندارد:
'use client' import { useState, useEffect } from 'react' import { subscribeUser, unsubscribeUser, sendNotification } from './actions' function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4) const base64 = (base64String + padding) .replace(/\\-/g, '+') .replace(/_/g, '/') const rawData = window.atob(base64) const outputArray = new Uint8Array(rawData.length) for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i) } return outputArray }
اکنون یک کامپوننت برای مدیریت subscribing، unsubscribing و ارسال push notificationها اضافه میکنیم.
function PushNotificationManager() { const [isSupported, setIsSupported] = useState(false) const [subscription, setSubscription] = useState<PushSubscription | null>( null ) const [message, setMessage] = useState('') useEffect(() => { if ('serviceWorker' in navigator && 'PushManager' in window) { setIsSupported(true) registerServiceWorker() } }, []) async function registerServiceWorker() { const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/', updateViaCache: 'none', }) const sub = await registration.pushManager.getSubscription() setSubscription(sub) } async function subscribeToPush() { const registration = await navigator.serviceWorker.ready const sub = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array( process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY! ), }) setSubscription(sub) await subscribeUser(sub) } async function unsubscribeFromPush() { await subscription?.unsubscribe() setSubscription(null) await unsubscribeUser() } async function sendTestNotification() { if (subscription) { await sendNotification(message) setMessage('') } } if (!isSupported) { return <p>Push notifications are not supported in this browser.</p> } return ( <div> <h3>Push Notifications</h3> {subscription ? ( <> <p>You are subscribed to push notifications.</p> <button onClick={unsubscribeFromPush}>Unsubscribe</button> <input type="text" placeholder="Enter notification message" value={message} onChange={(e) => setMessage(e.target.value)} /> <button onClick={sendTestNotification}>Send Test</button> </> ) : ( <> <p>You are not subscribed to push notifications.</p> <button onClick={subscribeToPush}>Subscribe</button> </> )} </div> ) }
در نهایت، یک کامپوننت دیگر میسازیم تا پیامی را برای دستگاههای iOS نشان دهد تا به آنها بگوید که این اپلیکیشن PWA را در home screen خود نصب کنند، و این پیام را فقط در صورتی نشان دهیم که برنامه قبلاً نصب نشده باشد.
function InstallPrompt() { const [isIOS, setIsIOS] = useState(false) const [isStandalone, setIsStandalone] = useState(false) useEffect(() => { setIsIOS( /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream ) setIsStandalone(window.matchMedia('(display-mode: standalone)').matches) }, []) if (isStandalone) { return null // Don't show install button if already installed } return ( <div> <h3>Install App</h3> <button>Add to Home Screen</button> {isIOS && ( <p> To install this app on your iOS device, tap the share button <span role="img" aria-label="share icon"> {' '} ⎋{' '} </span> and then "Add to Home Screen" <span role="img" aria-label="plus icon"> {' '} ➕{' '} </span>. </p> )} </div> ) } export default function Page() { return ( <div> <PushNotificationManager /> <InstallPrompt /> </div> ) }
اکنون Server Actionهایی را که در این فایل فراخوانی میکنیم را میسازیم.
یک فایل جدید میسازیم تا حاوی اکشنهای ما در app/actions.ts
باشد. این فایل ایجاد subscriptions، حذف subscriptionها و ارسال نوتیفیکیشنها را انجام میدهد.
// app/actions.ts 'use server' import webpush from 'web-push' webpush.setVapidDetails( '<mailto:your-email@example.com>', process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! ) let subscription: PushSubscription | null = null export async function subscribeUser(sub: PushSubscription) { subscription = sub // In a production environment, you would want to store the subscription in a database // For example: await db.subscriptions.create({ data: sub }) return { success: true } } export async function unsubscribeUser() { subscription = null // In a production environment, you would want to remove the subscription from the database // For example: await db.subscriptions.delete({ where: { ... } }) return { success: true } } export async function sendNotification(message: string) { if (!subscription) { throw new Error('No subscription available') } try { await webpush.sendNotification( subscription, JSON.stringify({ title: 'Test Notification', body: message, icon: '/icon.png', }) ) return { success: true } } catch (error) { console.error('Error sending push notification:', error) return { success: false, error: 'Failed to send notification' } } }
ارسال یک نوتیفیکیشن توسط service worker ما به کمک فرآیندی که در مرحله ۵ ایجاد خواهیم کرد، انجام میشود.
در یک محیط production، ما میخواهیم subscription را در یک پایگاه داده برای پایداری در راهاندازی مجدد سرور و مدیریت subscriptionهای چند کاربر ذخیره کنیم.
برای استفاده از Web Push API، باید کلیدهای VAPID تولید کنیم.
برای این کار، یک فایل اسکریپت میسازیم. به عنوان مثال، generate-vapid-keys.js
:
// ./generate-vapid-keys.js const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys() console.log('Paste the following keys in your .env file:') console.log('-------------------') console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=', vapidKeys.publicKey) console.log('VAPID_PRIVATE_KEY=', vapidKeys.privateKey)
این اسکریپت را با Node.js اجرا میکنیم تا کلیدهای VAPID خود را تولید کنیم:
// Terminal node generate-vapid-keys.js
خروجی را کپی کرده و در فایل .env
خود قرار میدهیم.
یک فایل public/sw.js
برای service worker خود میسازیم:
// public/sw.js self.addEventListener('push', function (event) { if (event.data) { const data = event.data.json() const options = { body: data.body, icon: data.icon || '/icon.png', badge: '/badge.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: '2', }, } event.waitUntil(self.registration.showNotification(data.title, options)) } }) self.addEventListener('notificationclick', function (event) { console.log('Notification click received.') event.notification.close() event.waitUntil(clients.openWindow('<https://your-website.com>')) })
این service worker از تصاویر و نوتیفیکیشنهای سفارشی پشتیبانی میکند. همچنین push eventهای ورودی و کلیکهای نوتیفیکیشن را کنترل مینماید.
icon
و badge
، آیکونهای سفارشی را برای نوتیفیکیشنهای خود تنظیم کنیم.vibrate
را نیز میتوانیم برای ایجاد نوتیفیکیشنهای ویبرهای سفارشی در دستگاههایی که از آن پشتیبانی میکنند، تنظیم کنیم.data
به نوتیفیکیشن پیوست نماییم.باید این موضوع را به یاد داشته باشیم که service worker خود را به طور کامل تست کنیم تا مطمئن شویم که در دستگاهها و مرورگرهای مختلف، مطابق انتظاری که داریم رفتار میکند. همینطور باید مطمئن شویم که لینک 'https://your-website.com'
در event listener notificationclick
را به URL مناسب برای برنامه خود به روز رسانی نماییم.
کامپوننت InstallPrompt
که در مرحله ۲ تعریف کردیم، پیامی را برای دستگاههای iOS نشان میدهد تا به کاربران آنها بگوید که برنامه را در home screen خود نصب کنند.
برای اطمینان از اینکه برنامه ما میتواند روی home screen موبایل نصب شود، باید موارد زیر را داشته باشد:
زمانی که این معیارها برآورده شوند مرورگرهای مدرن به طور خودکار یک نوتیفیکیشن نصب را به کاربران نشان میدهند.
برای اطمینان از اینکه میتوانیم نوتیفیکیشنها را به صورت local مشاهده نماییم، باید اطمینان حاصل کنیم که:
– ما در حال اجرا به صورت local با HTTPS هستیم:
next dev --experimental-https
استفاده میکنیم– مرورگر ما (Chrome، Safari، Firefox) نوتیفیکیشنها را فعال کرده است:
امنیت یک جنبه حیاتی از هر وب اپلیکیشن، به ویژه برای PWAها است. Next.js به ما اجازه میدهد تا headerهای امنیتی را با استفاده از فایل next.config.js
پیکربندی کنیم. به عنوان مثال:
// next.config.js module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', }, ], }, { source: '/sw.js', headers: [ { key: 'Content-Type', value: 'application/javascript; charset=utf-8', }, { key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate', }, { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self'", }, ], }, ] }, }
کاری که هر گزینه انجام میدهد عبارت است از:
X-Content-Type-Options: nosniff
: از MIME type sniffing جلوگیری میکند و خطر آپلود فایلهای مخرب را کاهش میدهد.X-Frame-Options: DENY
: با جلوگیری از قرار گرفتن سایت ما در iframe، در برابر حملات clickjacking محافظت میکند.Referrer-Policy: strict-origin-when-cross-origin
: این گزینه نیز کنترل میکند که چه مقدار اطلاعات ارجاع دهنده در درخواستها گنجانده شود، و در نتیجه امنیت و عملکرد را متعادل میکند.Content-Type: application/javascript; charset=utf-8
: اطمینان حاصل میکند که service worker به درستی به عنوان جاوااسکریپت تفسیر شده است.Cache-Control: no-cache, no-store, must-revalidate
: از کش شدن service worker جلوگیری میکند و اطمینان میدهد که کاربران همیشه آخرین نسخه را دریافت میکنند.Content-Security-Policy: default-src 'self'; script-src 'self'
: یک خطمشی امنیتی سختگیرانه برای service worker اجرا میکند و فقط به اسکریپتهایی از منبع یکسان اجازه اجرا میدهد.در این مقاله با مفهوم وب اپلیکیشنهای PWA آشنا شدیم و سعی کردیم تا روش ساخت آنها با استفاده از Next.js را بررسی کنیم.
به طور کلی، وب اپلیکیشنهای PWA به ما اجازه میدهند تا: