ساخت وب اپلیکیشن PWA با Next.js

PWAها (Progressive Web Application) دسترسی به وب اپلیکیشن‌ها را همراه با ویژگی‌ها و تجربه کاربری برنامه‌های موبایل native ارائه می‌دهند. با استفاده از Next.js، می‌توانیم انواع مختلف وب اپلیکیشن PWA بسازیم که تجربه‌ای یکپارچه و شبیه به برنامه را در همه پلتفرم‌ها، بدون نیاز به چندین پایگاه کد و یا تاییدیه app store ارائه می‌دهد.

ساخت یک وب اپلیکیشن PWA با استفاده Next.js

۱ – ایجاد مانیفست وب اپلیکیشن

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ها

تمام مرورگرهای مدرن از Web Push Notificationها پشتیبانی می‌کنند، از جمله:

  • iOS 16.4+ برای برنامه‌های نصب شده در home screen
  • سافاری ۱۶ برای macOS 13 یا جدیدتر
  • مرورگرهای مبتنی بر کروم
  • فایرفاکس

این باعث می‌شود 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هایی را که در این فایل فراخوانی می‌کنیم را می‌سازیم.

۳ – پیاده‌سازی 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های چند کاربر ذخیره کنیم.

۴ – ایجاد کلیدهای VAPID

برای استفاده از 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 خود قرار می‌دهیم.

۵ – ایجاد یک Service Worker

یک فایل 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 مناسب برای برنامه خود به روز رسانی نماییم.

۶ افزودن به Home Screen

کامپوننت InstallPrompt که در مرحله ۲ تعریف کردیم، پیامی را برای دستگاه‌های iOS نشان می‌دهد تا به کاربران آن‌ها بگوید که برنامه را در home screen خود نصب کنند.

برای اطمینان از اینکه برنامه ما می‌تواند روی home screen موبایل نصب شود، باید موارد زیر را داشته باشد:

  1. مانیفست معتبر وب اپلیکیشن (که در مرحله ۱ ساختیم)
  2. ارائه وب سایت از طریق HTTPS

زمانی که این معیارها برآورده شوند مرورگرهای مدرن به طور خودکار یک نوتیفیکیشن نصب را به کاربران نشان می‌دهند.

۷ تست به صورت Local

برای اطمینان از اینکه می‌توانیم نوتیفیکیشن‌ها را به صورت local مشاهده نماییم، باید اطمینان حاصل کنیم که:

– ما در حال اجرا به صورت local با HTTPS هستیم:

  • برای تست این مورد از next dev --experimental-https استفاده می‌کنیم

– مرورگر ما (Chrome، Safari، Firefox) نوتیفیکیشن‌ها را فعال کرده است:

  • هنگامی که به صورت local از ما درخواست می‌شود، باید مجوزهای استفاده از نوتیفیکیشن‌ها را قبول کنیم
  • باید اطمینان حاصل کنیم که نوتیفیکیشن‌ها به صورت سراسری برای کل مرورگر غیرفعال نیستند
  • در نهایت، اگر هنوز نوتیفیکیشن‌ها را نمی‌بینیم، باید از مرورگر دیگری برای دیباگ کردن استفاده کنیم

۸ ایمن‌سازی اپلیکیشن

امنیت یک جنبه حیاتی از هر وب اپلیکیشن، به ویژه برای 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'",
          },
        ],
      },
    ]
  },
}

کاری که هر گزینه انجام می‌دهد عبارت است از:

  1. Headerهای سراسری(بر روی همه routeها اعمال می‌شود):
  • X-Content-Type-Options: nosniff: از MIME type sniffing جلوگیری می‌کند و خطر آپلود فایل‌های مخرب را کاهش می‌دهد.
  • X-Frame-Options: DENY: با جلوگیری از قرار گرفتن سایت ما در iframe، در برابر حملات clickjacking محافظت می‌کند.
  • Referrer-Policy: strict-origin-when-cross-origin: این گزینه نیز کنترل می‌کند که چه مقدار اطلاعات ارجاع دهنده در درخواست‌ها گنجانده شود، و در نتیجه امنیت و عملکرد را متعادل می‌کند.
  1. Headerهای مخصوص Service Worker:
  • 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 به ما اجازه می‌دهند تا:

  • به روز رسانی‌ها را فوراً و بدون انتظار برای تأیید app store اجرا کنیم
  • اپلیکیشن‌های چند پلتفرمی را با یک پایگاه کد واحد بسازیم
  • ویژگی‌های native مانند نصب home screen و push notificationها را ارائه دهیم

دیدگاه‌ها:

افزودن دیدگاه جدید