ایجاد پروژه آزمایشی برای ارسال و دریافت push notification (قسمت چهارم)

ایجاد پروژه آزمایشی برای ارسال و دریافت push notification (قسمت چهارم)

در سه قسمت اخیر از سری مقالات «آموزش ارسال notification در یک سایت»، برای آزمایش فرایند ایجاد و ارسال پوش نوتیفیکیشن، دو پایگاه کد(codebase) با هم ساختیم. یک پایگاه کد مربوط به برنامه ی سمت سرور و دیگری مربوط به برنامه ی سمت کاربر.

در قسمت هشتم این سری مقالات، برای هر یک پروژه ای راه اندازی کردیم.

در قسمت نهم، در پروژه ی سمت کاربر یک رابط کاربری مناسب ایجاد کردیم، service worker را به پروژه اضافه کردیم، service worker را register کردیم و درباره ی نحوه ی ایجاد کلید عمومی و خصوصی توضیحاتی دادیم. همچنین گفتیم که مرورگر کاربر چگونه مشترک پوش نوتیفیکیشن های push service خود می شود.

در قسمت دهم نیز توضیح دادیم که به ازای مشترک شدن مرورگر روی push service، داده های مربوط به این اشتراک دریافت می شود. این داده ها برای استفاده مجدد، نیاز است در جایی ذخیره شوند. به این منظور سمت سرور یک پایگاه داده sqlite ایجاد کردیم و با ایجاد api های مناسب، کاری کردیم که پس از مشترک شدن مرورگر روی پوش نوتیفیکیشن push service مربوطه، داده ی مربوط به اشتراک این کاربر (آبجکت subscription) به همراه ID کاربر به سرور ارسال شوند و در پایگاه داده ذخیره شوند.

در این قسمت می بینیم که با در دست داشتن آبجکت subscription مربوط به مرورگر یک کاربر، چگونه می توان از push service درخواست کرد که برای آن کاربر نوتیفیکیشن ارسال کند.

برای این کار ابتدا سری به رابط کاربری پروژه می زنیم. همان طور که در تصویر می بینیم، یک دکمه به نام notify me داریم که می خواهیم به ازای فشردن این دکمه، درخواستی به سرور ارسال کنیم و در آن ID کاربر را برای سرور بفرستیم. سرور نیز به ازای آن آبجکت subscription مربوط به مرورگر کاربر را یافته و از push service درخواست ارسال پوش نوتیفیکیشن می کند:

پس در کد سمت کاربر، باید متد notifyMe در فایل App.vue را این گونه تغییر دهیم:

async notifyMe() {
  try {
    /* In real apps, we have to get the ID of the user from the backend,
    here we just supposed that the user id is 1
     */
    let notifyUserUrl = new URL('http://localhost:8000/notifications/notify-user');
    notifyUserUrl.search = new URLSearchParams({userId: 1});
    await fetch(notifyUserUrl, {
      method: 'GET',
    })
  } catch (err) {
    console.log('There were some error when requesting for notification: ', err);
  }

},

در این بخش ابتدا url مربوط به درخواست را ساختیم و userId که همان ID کاربر است را به عنوان query string به آن اضافه کردیم. همان طور که در قسمت قبل گفتیم، از آن جا که در پروژه ی آزمایشی مان سیستم مدیریت کاربران نداریم، فرض کردیم که ID کاربر برابر 1 بوده، بنابراین این مقدار را هم در کد سمت کاربر و هم در کد سمت سرور hard-code کرده ایم.

حال باید در سمت سرور، متد کنترل کننده ی این درخواست را به گونه ای بنویسیم که ابتدا آبجکت subscription کاربر را پیدا کند و سپس به کمک مقادیر آن به push service درخواست ارسال پوش نوتیفیکیشن بدهد. اما قبل از آن باید یادآوری کنیم که قرار بود برای تعامل با push service از یک پکیج معروف به نام web-push استفاده کنیم. پیش تر نیز این پکیج را در پایگاه کد سمت سرور نصب کردیم. اما حالا لازم داریم که آن را به کد اضافه کنیم. برای این کار در فایل routes/notificationsRouter.js این پکیج را require می کنیم:

let webpush = require('web-push');

در قسمت نهم از این سری مقالات به کمک web-push کلید های عمومی و خصوصی را ساختیم و گفتیم که خوب است مقادیر این کلید ها را به عنوان متغیر محیطی و در فایل env. ذخیره کنیم. در آن جا با هم مقدار کلید عمومی را که مورد نیاز پروژه ی سمت کاربر بود، در فایل env. پروژه ی سمت کاربر ذخیره کردیم. حال به طور مشابه، یک فایل env. در پایه کد سمت سرور ایجاد می کنیم و مقادیر کلید عمومی و کلید خصوصی که هر دو مورد نیاز این پکیج web-push برای ارسال درخواست هستند را در آن ذخیره می کنیم:

public_key="BLAhpQMrurfVGr1LJ28x0KbWPQlC00VC7G5yJjKESOWcTyffj5HVekNJVdq6_-ajLe-8Y6YVplW70610xy0ewgs"
private_key="nJ1wgjI69NQXOpZdU_6j20C8kM8dGX-V4giD8GPjgR0"

همان طور که پیش تر نیز گفته شد، برای استفاده از این متغیر های محیطی در قسمت های مختلف پروژه، نیاز است که آن ها را از طریق پکیج dotenv به process.env اضافه کنیم تا در دسترس فایل های پروژه قرار گیرند. پس به سراغ نصب پکیج dotenv می رویم:

npm install dotenv

پس از نصب، برای راه اندازی این پکیج باید متد config آن را صدا بزنیم، پس فایل server.js این گونه تغییر می کند:

//add and initialize dotenv packagerequire('dotenv').config();

//add neccessary packages
const express = require('express');


const notifRouter = require('./routes/notificationsRouter');

//create an express server
const app = express();

/* set this part to bypass cors error */
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
  res.setHeader('Access-Control-Allow-Credentials', true);
  // Pass to next layer of middleware
  next();
})

/* add body parser to parse json from request and attach it to req.body */
const bodyParser = require('body-parser');
app.use(bodyParser.json());


/* at application start, create database and initialize database tables (if not exist) */
let {initializeDatabaseTables} = require('./db/db');
initializeDatabaseTables();

// add routers
app.use('/notifications', notifRouter);

app.listen('8000', () => {
  console.log('after listening!');
});

در این مرحله نیاز داریم که متد کنترل کننده ی درخواست ارسال نوتیفیکیشن را در فایل notificationsRouter بنویسیم. اما قبل از آن لازم است در این فایل با استفاده از مقادیری که در فایل env. ذخیره کرده ایم، داده هایی را که برای ارسال درخواست پوش نوتیفیکیشن مورد نیاز است به سرور ارسال کنیم. به این منظور در ابتدای فایل notificationsRouter می نویسیم:

let vapidDetails = {
    publicKey: process.env.public_key,
    privateKey: process.env.private_key,
    subject: 'mailto:test@test.test'
}

و بعد به سراغ متد کنترل کننده ی درخواستمان می رویم:

router.get('/notify-user', async (req, res, next) => {
    try {
        await notifyUser(req.query);
        return res.send({message: 'notification sent to you!'});
    } catch (e) {
        next(e);
    }

})

این متد کنترل کننده، خود از یک متد کمکی به نام notifyUser استفاده می کند که چنین ساختاری دارد:

const notifyUser = async (requestData) => {
    try {

        let subscriptionData = await knex('usersSubscriptions').select('subscription').where('userId', requestData.userId);
        let subscription = JSON.parse(subscriptionData[subscriptionData.length - 1].subscription);

        // Create the notification content.
        const notification = JSON.stringify({
            title: "Hello dear sokanacademy user!",
            options: {
                body: `ID: ${Math.floor(Math.random() * 100)}`
            }
        });

        // Customize how the push service should attempt to deliver the push message.
        // And provide authentication information.
        const options = {
            TTL: 10000,
            vapidDetails: vapidDetails
        };
        // Send a push message to each client specified in the subscriptions array.
        const endpoint = subscription.endpoint;
        const id = endpoint.substr((endpoint.length - 8), endpoint.length);
        webpush.sendNotification(subscription, notification, options)
            .then(result => {
                console.log(`Endpoint ID: ${id}`);
                console.log(`Result: ${result.statusCode}`);
            })
            .catch(error => {
                console.log(`Endpoint ID: ${id}`);
                console.log(`Error: ${error} `);
            });
    } catch (err) {
        console.log('err occured while sending: ', err);
        Promise.reject(err);
    }

}

این متد ابتدا بر اساس userId دریافتی از کاربر، آبجکت subscription او را از پایگاه داده می خواند. سپس فرم و محتوای پیام نوتیفیکیشن را در قالب یک آبجکت به نام notification ایجاد می کند و بعد در قالب آبجکت options و با کمک متغیر vapidDetails، مقادیر مربوط به احراز هویت را تنظیم می کند. در نهایت نیز با پاس دادن این مقادیر به متد webpush.sendNotification از push service درخواست ارسال پوش نوتیفیکیشن می کند.

اگر همه ی مراحل به درستی طی شده باشد، با این پیش فرض که مرورگر ما به درستی مشترک push service شده است، با فشردن دکمه ی notify me در صفحه ی برنامه، نوتیفیکیشن برای ما ارسال می شود.

اما برای دریافت این نوتیفیکیشن دریافتی باید چه کنیم؟

باید کد service worker را طوری تکمیل کنیم که پیام را نمایش دهد، پس فایل public/service-worker.js را باز کرده و مقدار آن را این گونه تغییر می دهیم:

self.addEventListener('push', event => {
    let data = event.data.json();
    const image = '...';
    const options = {
        body: data.options.body,
        icon: image
    }
    self.registration.showNotification(
        data.title,
        options
    );
});

(می توانید مقدار متغیر image را برابر آدرس هر عکسی که مایل بودید قرار دهید)

حال دکمه ی notify me را می زنیم و نتیجه را می بینیم:

 

تبریک می گوییم! شما تمامی مراحل ایجاد و ارسال نوتیفیکیشن را پیاده سازی کرده اید!

حتماً دقت کرده اید در صفحه ای که طراحی کرده ایم، یک دکمه ی Notify all داریم که شما می توانید برای آزمایش و یادگیری خود، منطق مربوط به ارسال پوش نوتیفیکیشن برای تمامی کاربران سامانه را پیاده سازی کنید.

پیاده سازی قابلیت ارسال پوش نوتیفیکیشن در وبسایت های واقعی

  • طی چند مقاله ی اخیر، با هم یک پروژه ی ساده پیاده سازی کردیم و طی آن فرایند ایجاد و ارسال پوش نوتیفیکیشن را بهتر و عمیق تر درک کردیم. اما واقعیت آن است که در وبسایت های مختلف، معمولاً از یک سرویس سوم شخص (third party service) برای این کار استفاده می شود. شرکت های داخلی و خارجی مختلفی چنین سرویس هایی ارائه می دهند. این سرویس ها کار توسعه دهندگان برای ارسال نوتیفیکیشن را راحت می کنند و معمولاً چالش های کدنویسی این زمینه و بررسی حالات خطا و ... را انجام می دهند. از این رو توسعه دهندگان ترجیح می دهند قابلیت ارسال پوش نوتیفیکیشن را با استفاده از این سرویس ها پیاده سازی کنند.

به عنوان مثال سرویس های داخلی «نجوا»، «پوشه»، «پوش نامه» و ... از جمله سرویس های معروف در این زمینه هستند.

  • نکته ی دیگر آن که در عمل ممکن است کاربران مختلف با دستگاه های مختلفی از وبسایت ما دیدن کنند، از این رو گاهی لازم می شود که برای یک کاربر چندین آبجکت subscription ذخیره شود تا در صورت نیاز برای همه ی دستگاه های او پوش نوتیفیکیشن ارسال شود.
شنیدن این اپیزود از رادیوفول‌استک را به شما پیشنهاد می‌دهیم

از بهترین نوشته‌های کاربران سکان آکادمی در سکان پلاس