Архитектура

В современной программной архитектуре становится все более распространенным разбивать приложения на более мелкие компоненты. Эти компоненты должны взаимодействовать друг с другом (например, через RESTful API), обеспечивая беспрепятственный обмен данными.

Преимущество этого подхода заключается в разделении компонентов, что значительно облегчает масштабирование приложения. Однако одна из проблем такой архитектуры заключается в том, как эффективно уведомлять каждый компонент о соответствующих изменениях.

Для решения этой проблемы можно использовать несколько подходов, таких как использование шины сообщений или реализация двухточечных интерфейсов. Тем не менее, популярной и заслуживающей внимания функцией является использование push-уведомлений, если речь идет о событиях, связанных с конечным пользователем.

Чтобы удовлетворить эту потребность, несколько известных интернет-гигантов предоставляют услуги, специально предназначенные для push-уведомлений. Среди них я выбрал Firebase Cloud Messaging от Google из-за его удобного характера, большого количества примеров и множества вариантов использования.

В моем демонстрационном приложении у нас есть несколько клиентов, участвующих в голосовании по разным вопросам, и всем этим помогает администратор. Кроме того, у нас есть клиент, отвечающий за представление текущего состояния процесса голосования. Поддержка этих взаимодействий — это универсальный бэкенд, который предлагает RESTful API и позволяет эффективно манипулировать данными.

Стоит отметить, что клиент голосования, интерфейс администратора и клиент, отображающий результаты голосования, не зависят от платформы, то есть их можно развернуть на разных платформах.

Однако ради простоты и легкости реализации я решил использовать веб-приложения для каждого из этих компонентов (на основе Laravel и Livewire).

Поскольку мне по своей природе интересно понять внутреннюю работу систем, я возьму на себя задачу продемонстрировать, как такая архитектура может быть реализована, вместо того, чтобы полагаться на уже существующие пакеты Laravel.

При рассмотрении концепции push-уведомлений важно создать механизм, который позволит клиентам подписаться на эту функцию. Этого можно достичь с помощью службы подписки, предлагаемой серверной частью, а также необходимых средств для отправки уведомлений через выбранного поставщика услуг. В конечном итоге весь процесс должен завершиться предоставлением клиентам возможности эффективно получать и обрабатывать эти уведомления.

Подписка на push-уведомления

Процесс подписки включает в себя использование конечной точки API, куда клиент отправляет токен, который впоследствии сохраняется для дальнейшего использования.

Чтобы инициировать подписку на уведомления, клиенты могут использовать следующую конечную точку API:

// Tokens for push notifications
$router->post('/subscribe', ['uses' => 'TokenController@storeToken']);

С нашей стороны, мы используем кэширование Laravel для хранения токенов, сгенерированных клиентами. Если клиент представляет новый токен, мы можем обновить его в кеше. С другой стороны, если клиент использует тот же токен, мы отвечаем соответствующим образом.

public function storeToken(Request $request)
{
    $key = $request->ip; // key the tokens by the requestor IP address

    if (!Cache::has($key)) {
        Cache::put($key, $request->token, Carbon::now()->addMinutes(self::CACHE_EXPIRATION_TIME));
        return response()->json(['status' => 'success', 'message' => 'Token registered successfully'], 201);
    } else {
        if ($request->token !== Cache::get($key)) {
            Cache::forget($key);
            Cache::put($key, $request->token, Carbon::now()->addMinutes(self::CACHE_EXPIRATION_TIME));
            return response()->json(['status' => 'success', 'message' => 'Token refreshed successfully'], 200);
        }
    }

    return response()->json(['status' => 'success', 'message' => 'Token is already registered'], 200);
}

Отправка уведомлений

Уведомления инициируются действиями пользователя, такими как получение нового голоса за вопрос. В примере приложения, когда такое событие происходит, наша цель — уведомить подписанных клиентов об изменении голосования, чтобы они могли надлежащим образом обработать эту информацию.

try {
     $this->initWithPushNotification(
        $question->question_text . ' / '. $new_vote->vote_text, 
        'Votes increased to ' . $new_vote->number_of_votes,
        "http://localhost:8200/questions/$question_id/votes"
     )->sendPushNotification();
} catch (\Exception $e) {
     return response()->json($e->getMessage(), 500);
}

Чтобы поддерживать чистый и организованный контроллер маршрута, я объединил все действия в отдельный трейт.

Мы инициализируем наш «пушер» нужной информацией и оперативно рассылаем уведомление.

Для эффективной обработки я реализовал простой метод на основе HTTP-фасада вместе с обработчиками очередей. Такой подход позволяет нам эффективно управлять нагрузкой даже при большом количестве подписанных клиентов.

Обработчики очередей также предоставляют хороший способ обработки невыполненных заданий и управления журналом событий.

Отображение результатов

FCM предлагает обширную библиотеку JavaScript, облегчающую обработку входящих сообщений.

Документация для этой библиотеки очень обширна и сопровождается многочисленными примерами.

Поэтому я не буду здесь углубляться в специфику клиентской реализации. Тем не менее, я хотел бы предоставить краткую демонстрацию того, как обрабатывать входящие сообщения:

messaging.onMessage((payload) => {
    console.log('Message received. ', payload);
    Livewire.emit('refresh-chart');
});

Я использую Livewire для отображения красивого графика результатов голосования.

Всякий раз, когда получено новое сообщение, я запускаю событие (обновление диаграммы), которое обрабатывается на бэкэнде:

 public function refreshChart()
 {
     $this->fetchData();
 }

 public function fetchData(): void
 {
     try {
         $url = self::URL.'/questions/'.$this->question_id.'/votes';
            
         $response = Http::get($url)->throwUnlessStatus(200)->json();               
     } catch (\Exception $e) {
         $this->error_message = $e->getMessage();
     }

     $this->votes = $response;
     $this->vote_texts = $this->getVoteTexts($response);
     $this->vote_results = $this->getVoteResults($response);
     $this->emit('chart-refreshed');
 }

Мы знаем, что было получено новое голосование, и для того, чтобы обновить наши данные, нам просто нужно сделать вызов к серверной части, используя соответствующую конечную точку API.

Как только мы получаем обновленный набор данных, мы информируем клиентскую сторону, создавая событие «обновление диаграммы».

В результате наша линейчатая диаграмма на основе chart.js автоматически обновляется, демонстрируя плавный эффект перехода.