PWAها (Progressive Web Application) دسترسی به وب اپلیکیشنها را همراه با ویژگیها و تجربه کاربری برنامههای موبایل native ارائه میدهند. با استفاده از Next.js، میتوانیم انواع مختلف وب اپلیکیشن PWA بسازیم که تجربهای یکپارچه و شبیه به برنامه را در همه پلتفرمها، بدون نیاز به چندین پایگاه کد و یا تاییدیه app store ارائه میدهد.
ساخت یک وب اپلیکیشن PWA با استفاده Next.js
۱ – ایجاد مانیفست وب اپلیکیشن
Next.js پشتیبانی داخلی برای ایجاد یک مانیفست وب اپلیکیشن با استفاده از App Router ارائه میدهد. میتوانیم یک فایل مانیفست استاتیک یا داینامیک ایجاد کنیم:
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',
},
],
}
}
// 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',
},
],
}
}
// 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/
public/ استفاده کنیم.
۲ – پیادهسازی Web Push Notificationها
تمام مرورگرهای مدرن از Web Push Notificationها پشتیبانی میکنند، از جمله:
iOS 16.4+ برای برنامههای نصب شده در home screen
سافاری ۱۶ برای macOS 13 یا جدیدتر
مرورگرهای مبتنی بر کروم
فایرفاکس
این باعث میشود PWAها جایگزین مناسبی برای اپلیکیشنهای native باشند. قابل ذکر است ما میتوانیم بدون نیاز به پشتیبانی آفلاین، دستورهای نصب را راهاندازی کنیم.
Web Push Notificationها به ما این امکان را میدهند تا حتی زمانی که کاربران به طور فعال از برنامه ما استفاده نمیکنند، با آن درگیر شوند. در ادامه نحوه پیادهسازی آنها در برنامه Next.js را داریم:
ابتدا، کامپوننت main page را در
app/page.tsx
app/page.tsx میسازیم. برای درک بهتر، آن را به بخشهای کوچکتر تقسیم میکنیم. ابتدا، برخی از importها و ابزارهای مورد نیاز خود را اضافه میکنیم. اگر Server Actionهای رفرنس شده در برنامه نداریم، فعلا ایرادی ندارد:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
'use client'
import{ useState, useEffect } from 'react'
import{ subscribeUser, unsubscribeUser, sendNotification } from './actions'
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>
)
}
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 خود نصب کنند، و این پیام را فقط در صورتی نشان دهیم که برنامه قبلاً نصب نشده باشد.
return{ success: false, error: 'Failed to send notification'}
}
}
// 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' }
}
}
// 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های چند کاربر ذخیره کنیم.
۴ – ایجاد کلیدهای VAPID
برای استفاده از Web Push API، باید کلیدهای VAPID تولید کنیم.
برای این کار، یک فایل اسکریپت میسازیم. به عنوان مثال،
generate-vapid-keys.js
generate-vapid-keys.js:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// ./generate-vapid-keys.js
const webpush = require('web-push')
const vapidKeys = webpush.generateVAPIDKeys()
console.log('Paste the following keys in your .env file:')
این service worker از تصاویر و نوتیفیکیشنهای سفارشی پشتیبانی میکند. همچنین push eventهای ورودی و کلیکهای نوتیفیکیشن را کنترل مینماید.
میتوانیم با استفاده از ویژگیهای
icon
icon و
badge
badge، آیکونهای سفارشی را برای نوتیفیکیشنهای خود تنظیم کنیم.
الگوی
vibrate
vibrate را نیز میتوانیم برای ایجاد نوتیفیکیشنهای ویبرهای سفارشی در دستگاههایی که از آن پشتیبانی میکنند، تنظیم کنیم.
همچنین میتوانیم دادههای اضافی را با استفاده از ویژگی
data
data به نوتیفیکیشن پیوست نماییم.
باید این موضوع را به یاد داشته باشیم که service worker خود را به طور کامل تست کنیم تا مطمئن شویم که در دستگاهها و مرورگرهای مختلف، مطابق انتظاری که داریم رفتار میکند. همینطور باید مطمئن شویم که لینک
'https://your-website.com'
'https://your-website.com' در event listener
notificationclick
notificationclick را به URL مناسب برای برنامه خود به روز رسانی نماییم.
۶ – افزودن به Home Screen
کامپوننت
InstallPrompt
InstallPrompt که در مرحله ۲ تعریف کردیم، پیامی را برای دستگاههای iOS نشان میدهد تا به کاربران آنها بگوید که برنامه را در home screen خود نصب کنند.
برای اطمینان از اینکه برنامه ما میتواند روی home screen موبایل نصب شود، باید موارد زیر را داشته باشد:
مانیفست معتبر وب اپلیکیشن (که در مرحله ۱ ساختیم)
ارائه وب سایت از طریق HTTPS
زمانی که این معیارها برآورده شوند مرورگرهای مدرن به طور خودکار یک نوتیفیکیشن نصب را به کاربران نشان میدهند.
۷ – تست به صورت Local
برای اطمینان از اینکه میتوانیم نوتیفیکیشنها را به صورت local مشاهده نماییم، باید اطمینان حاصل کنیم که:
– ما در حال اجرا به صورت local با HTTPS هستیم:
برای تست این مورد از
next dev --experimental-https
next dev --experimental-https استفاده میکنیم
– مرورگر ما (Chrome، Safari، Firefox) نوتیفیکیشنها را فعال کرده است:
هنگامی که به صورت local از ما درخواست میشود، باید مجوزهای استفاده از نوتیفیکیشنها را قبول کنیم
باید اطمینان حاصل کنیم که نوتیفیکیشنها به صورت سراسری برای کل مرورگر غیرفعال نیستند
در نهایت، اگر هنوز نوتیفیکیشنها را نمیبینیم، باید از مرورگر دیگری برای دیباگ کردن استفاده کنیم
۸ – ایمنسازی اپلیکیشن
امنیت یک جنبه حیاتی از هر وب اپلیکیشن، به ویژه برای PWAها است. Next.js به ما اجازه میدهد تا headerهای امنیتی را با استفاده از فایل
Headerهای سراسری(بر روی همه routeها اعمال میشود):
X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff: از MIME type sniffing جلوگیری میکند و خطر آپلود فایلهای مخرب را کاهش میدهد.
X-Frame-Options: DENY
X-Frame-Options: DENY: با جلوگیری از قرار گرفتن سایت ما در iframe، در برابر حملات clickjacking محافظت میکند.
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: strict-origin-when-cross-origin: این گزینه نیز کنترل میکند که چه مقدار اطلاعات ارجاع دهنده در درخواستها گنجانده شود، و در نتیجه امنیت و عملکرد را متعادل میکند.
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 به ما اجازه میدهند تا:
به روز رسانیها را فوراً و بدون انتظار برای تأیید app store اجرا کنیم
اپلیکیشنهای چند پلتفرمی را با یک پایگاه کد واحد بسازیم
ویژگیهای native مانند نصب home screen و push notificationها را ارائه دهیم