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

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

در قسمت هشتم از سری مقالات «آموزش ارسال notification در یک سایت»، دو پایگاه کد(code base) برای پیاده سازی آزمایشی فرایند ارسال و دریافت پوش نوتیفیکیشن ایجاد کردیم. یکی برای اپلیکیشین سمت سرور و دیگری برای اپلیکیشن سمت کاربر. در قسمت قبلی نیز یک رابط کاربری مناسب در اپلیکیشن سمت کاربر ایجاد کردیم و نحوه ی اضافه کردن و register کردن کد service worker به آن را با یکدیگر دیدیم. سپس پکیج web-push را به پروژه اضافه کردیم و از طریق آن دو کلید، یکی کلید عمومی و دیگری کلید خصوصی تولید کردیم و گفتیم که خوب است در یک فایل .env مقادیر مربوط به کلید ها را ذخیره کنیم.

می دانیم که برای مشترک شدن مرورگر کاربر روی push service به این کلید عمومی نیاز داریم. اما چطور کاربر را مشترک پوش نوتیفیکیشن های push service کنیم؟

در ادامه پاسخ این سوال را پیدا می کنیم و با هم کد قسمت های قبل را به گونه ای تکمیل می کنیم که مرورگر ما مشترک push service شود.

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

برای مشترک کردن مرورگر یک کاربر روی push service، نیاز است که مرورگر یک درخواست اشتراک به push service ارسال کند و در آن کلید عمومی را نیز بفرستد. push service نیز در پاسخ یک آبجکت pushSubcription برمی گرداند که مدارک ثبت نام مرورگر کاربر روی push service است. با مشترک شدن مرورگر کاربر روی push service، هر بار که می خواهیم push service برای مرورگر کاربر مورد نظر ما نوتیفیکیشن ارسال کند، باید اطلاعات آبجکت pushSubscription را برای push service بفرستیم. در واقع push service از روی pushSubscription متوجه می شود که نوتیفیکیشن را برای چه مرورگری ارسال کند. در نتیجه ما به عنوان برنامه نویس سمت سرور، نیاز داریم که داده ی مربوط به pushSubscription هر یک از کاربران را در پایگاه داده ی خود ذخیره کنیم، تا هر بار خواستیم برای کاربری خاص نوتیفیکیشن ارسال کنیم، با استفاده از pushSubscription آن کاربر به pushService درخواست ارسال دهیم. پس هر بار که مرورگر کاربر مشترک پوش نوتیفیکیشن شد، pushSubscription دریافتی آن را برای سرور برنامه نیز ارسال می کنیم تا برای استفاده های بعدی ذخیره شود.

با این مقدمه، به سراغ پروژه ی سمت سرور می رویم و پکیج sqlite3 که پکیج راه انداز sqlite3 برای node.js است را نصب می کنیم:

npm install sqlite3

سپس پکیج knex که یک کوئری ساز (query builder) برای پایگاه داده های sql روی node.js محسوب می شود را نصب می کنیم:

npm install knex

پکیج knex به ما کمک می کند کوئری های راحت تری بنویسیم و درگیر پیچیدگی های نوشتن کوئری خام نشویم.

تا کنون ساختار فایل پروژه ی سمت سرور چنین شکلی دارد:

حال می خواهیم پایگاه داده مان را به پروژه اضافه کنیم. اینکه از چه پایگاه داده ای برای کدتان استفاده می کنید، کاملاً به سلیقه و نیازمندی شما بستگی دارد. حتی می توانید اطلاعات مورد نیاز را در یک فایل json ذخیره کنید. اما همان طور که قبلاً گفتیم، ما در این پروژه از sqlite استفاده کرده ایم.

برای اضافه کردن sqlite و کوئری ساز آن به پروژه، ابتدا یک پوشه به نام db در مسیر اصلی پروژه ایجاد می کنیم. در این فایل نیز یک فایل db.js می سازیم:

در فایل db.js پس از آماده سازی پایگاه داده، یک جدول به نام usersSubscriptions می سازیم:

const path = require('path');
/* add knex with sqlite3 dialect */
const knex = require('knex')({
    client: 'sqlite3',
    connection: {
        filename: path.join(__dirname, 'push-notification-project.db'),
    },
});

const initializeDatabaseTables = () => {
    knex.schema.hasTable('usersSubscriptions').then((exists) => {
        if (!exists) {
            knex.schema.createTable('usersSubscriptions', (table) => {
                table.integer('userId');
                table.string('subscription');
            }).catch(err => {
                console.log('err on creating table: ', err);
            })
        }
    })
}


module.exports = {
    knex,
    initializeDatabaseTables,
}

همان طور که می بینید، از این فایل دو متد با نام های knex و initializeDatabase خروجی داده شده است (export شده است). از آبجکت knex در فایل notificationsRouter استفاده می کنیم تا به کمک آن تعاملاتمان با پایگاه داده را انجام دهیم. هم چنین قرار است در فایل server.js که فایل اصلی اپلیکیشن ماست، متد initializeDatabase را صدا بزنیم تا ابتدای شروع کار برنامه، پایگاه داده و جدول آن ایجاد شوند. 

پس فایل server.js به این شکل در می آید:

//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!');
});

دقت شود در این بخش:

/* 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();
})

همان طور که در کامنت مربوطه نیز آورده ایم، با استفاده از یک middleware اجازه داده ایم درخواست هایی که از localhost:8080 یعنی اپلیکیشن سمت کلاینت به localhost:8000 یعنی سرور برنامه ارسال می شود، به خطای cors برخورد نکند. در مورد این خطا می توانید در سایت MDN بیشتر بخوانید.

در بخش:

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

نیز پکیج body-parser را به عنوان یک middleware دیگر به پروژه اضافه کرده ایم تا داده های ارسالی از سمت کاربر را به req.body روتر ها اضافه کند.

حال فایل notificationsRouter را این گونه تغییر می دهیم:

let {Router} = require('express');
let router = Router();
const {knex} = require('../db/db');

router.post('/add-user-subscription', async (req, res, next) => {
    try {
        await addSubscriptionData(req.body);
        return res.send({message: 'subscription data saved successfully'});
    } catch (e) {
        next(e);
    }
})

const addSubscriptionData = (requestData) => {
    return knex('usersSubscriptions').insert({
        userId: requestData.userId,
        subscription: JSON.stringify(requestData.subscription),
    });
}

module.exports = router;

همان گونه که ملاحظه می کنید، با وارد کردن (require کردن) متد knex، می توانیم تعاملاتمان با پایگاه داده را از طریق این متد انجام دهیم.
در این فایل یک روتر برای مسیر notifications/add-user-subscription/ در نظر گرفته ایم. قرار است کاربران (مرورگر کلاینت ها) به این مسیر درخواست با متد post بدهند و در آن ID کاربر را به همراه اطلاعاتی که به ازای مشترک شدن دریافت کرده اند (همان آبجکت pushSubscription که شامل اطلاعاتی چون endpoint، expirationTime و برخی مقادیر دیگر است) برای سرور ارسال کنند. ما نیز سمت سرور این مقدار را می گیریم و در پایگاه داده ذخیره می کنیم. به این ترتیب ID هر کاربر با اطلاعات اشتراک آن روی push service جفت می شود. یعنی هر کاربر می تواند یک آبجکت subscription داشته باشد که در صورت نیاز، با در دست داشتن ID آن کاربر، می توانیم به آبجکت subscription او برسیم و به کمک آن به push service درخواست ارسال پوش نوتیفیکیشن بدهیم. برای ذخیره ی مقدار آبجکت subscription برای کاربر، از یک متد کمکی به نام addSubscriptionData استفاده کرده ایم که از طریق knex، در جدول usersSubscriptions مقدار مورد نظر ما را ذخیره می کند.
اما چطور باید از سمت کلاینت به سرور درخواست ارسال کنیم؟
برای این کار به پایگاه کد سمت کاربر رفته و در فایل App.vue، مقدار متد subscribeToPush را این گونه تغییر می دهیم:

async subscribeToPush() {
  try {
    const registration = await navigator.serviceWorker.getRegistration();
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: this.urlB64ToUint8Array(process.env.VUE_APP_VAPID_PUBLIC_KEY)
    });
    // subscription data must be sent to the backend to be stored in the database for later use
    let addedEndpointResult = await fetch('http://localhost:8000/notifications/add-user-subscription',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            userId: 1,
            subscription: subscription,
          }),
        }
    )
    let addedEndpoint = await addedEndpointResult.json();
    alert(addedEndpoint.message);
  } catch (err) {
    console.log('There were some error on subscribing the service worker: ', err);
  }
},

در این قطعه کد، در واقع ما سه کار انجام دادیم:

  1. ابتدا اطلاعات مربوط به ثبت نام service worker را دریافت می کنیم:
const registration = await navigator.serviceWorker.getRegistration();

   2. سپس با استفاده از متد subscribe که زیر مجموعه ی آبجکت pushManager است، مرورگر کاربر را مشترک پوش نوتیفیکیشن های push service می کنیم. این متد درخواست مربوطه را به push service ارسال خواهد کرد:

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: process.env.VUE_APP_VAPID_PUBLIC_KEY
});

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

   3. در این مرحله با ارسال یک درخواست به سرور برنامه، مقدار آبجکت subscription را ذخیره می کنیم:

// subscription data must be sent to the backend to be stored in the database for later use
let addedEndpointResult = await fetch('http://localhost:8000/notifications/add-user-subscription',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        userId: 1,
        subscription: subscription,
      }),
    }
)
let addedEndpoint = await addedEndpointResult.json();

دقت شود از آن جا که ما در این پروژه ی تستی، سامانه ی مدیریت کاربران و ... نداریم، فرض را بر این گذاشتیم که ID کاربر برابر با 1 است. اما در عمل نیاز است که ID واقعی کاربر را بفرستیم. 

پس از طی مراحل بالا، پروژه های سمت کاربر و سمت سرور را راه اندازی می کنیم. دقت داریم که با اولین بار راه افتادن پروژه ی سمت سرور، فایل db.js یک پایگاه داده sqlite به نام push-notification-project.db در مسیر db/ ایجاد می کند:

حال اگر به مرورگر رفته و اپلیکیشن را باز کنیم، با کلیک کردن روی دکمه ی Subscribe to push، مقدار آبجکت subscription به سرور ارسال و در پایگاه داده ذخیره می شود:

اگر با نرم افزار های مدیریت پایگاه داده (مانند DB Browser) پایگاه داده ی پروژه را باز کنید، این مقدار را در جدول usersSubscriptions خواهید دید:

به این ترتیب مقدار آبجکت subscription را در قالب json و برای استفاده های بعدی در پایگاه داده ذخیره کردیم.

حال اگر بخواهیم اشتراک کاربر از پوش نوتیفیکیشن را حذف کنیم چه باید بکنیم؟

مشابه مراحل بالا، ابتدا در مرورگر کاربر درخواست لغو اشتراک را به push service می زنیم، و سپس با ارسال یک درخواست به سرور، به آن می گوییم که اشتراک کاربری که با ID مشخص شده است لغو شده. سرور نیز به ازای دریافت درخواست لغو اشتراک، مقدار آبجکت subscription کاربر مورد نظر را از پایگاه داده ی خود پاک می کند.

به این منظور در فایل notificationsRoute، یک روتر برای مسیر notifications/remove-user-subscription/ می سازیم:

router.delete('/remove-user-subscription', async (req, res, next) => {
  try {
    await removeSubscriptionData(req.body);
    return res.send({message: 'subscription data removed'});
  } catch (e) {
    next(e);
  }
})

این روتر به کمک متد removeSubscriptionData اطلاعات مربوط به آبجکت subscription کاربر مورد نظر را از پایگاه داده حذف خواهد کرد:

const removeSubscriptionData = (requestData) => {
    return knex('usersSubscriptions').where('userId', requestData.userId).delete();
}

پس در مجموع، فایل notificationsRouter ما به این شکل در می آید:

let {Router} = require('express');
let router = Router();
const {knex} = require('../db/db');

router.post('/add-user-subscription', async (req, res, next) => {
    try {
        await addSubscriptionData(req.body);
        return res.send({message: 'subscription data saved successfully'});
    } catch (e) {
        next(e);
    }
})

router.delete('/remove-user-subscription', async (req, res, next) => {
    try {
        await removeSubscriptionData(req.body);
        return res.send({message: 'subscription data removed'});
    } catch (e) {
        next(e);
    }
})

const addSubscriptionData = (requestData) => {
    return knex('usersSubscriptions').insert({
        userId: requestData.userId,
        subscription: JSON.stringify(requestData.subscription),
    });
}

const removeSubscriptionData = (requestData) => {
    return knex('usersSubscriptions').where('userId', requestData.userId).delete();
}

module.exports = router;

مانند آنچه که در قبل دیدیم، نیاز داریم در فایل App.vue نیز منطق مربوط به لغو اشتراک را وارد کنیم، به این منظور متد unSubscribeFromPush را این گونه می نویسیم:

async unSubscribeFromPush() {
  try {
    const registration = await navigator.serviceWorker.getRegistration();
    const subscription = await registration.pushManager.getSubscription();
    await subscription.unsubscribe();
    // subscription data must be sent to the backend to be stored in the database for later use
    let addedSubscriptionObjectResult = await fetch('http://localhost:8000/notifications/remove-user-subscription',
        {
          method: 'DELETE',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({userId: 1}),
        }
    )
    let deletedSubscriptionObject = await addedSubscriptionObjectResult.json();
    alert(deletedSubscriptionObject.message);
  } catch (err) {
    console.log('There were some error when unsubscribing the service worker: ', err);
  }
},

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

در مقاله‌ی بعد، با هم می بینیم که سرور با چه ساز و کاری، از push service درخواست می کند که برای کلاینت های مختلف نوتیفیکیشن ارسال کند.

 

شاد و سربلند باشید.

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


online-support-icon