Como enviar mensagens com bibliotecas push da Web

Um dos problemas ao trabalhar com o push da Web é que acionar uma mensagem push é extremamente difícil. Para acionar uma mensagem de push, um aplicativo precisa fazer uma solicitação POST para um serviço de push seguindo o protocolo de push da Web. Para usar o push em todos os navegadores, é necessário usar VAPID (também conhecido como chaves do servidor do aplicativo), que basicamente exige a definição de um cabeçalho com um valor que comprove que seu aplicativo pode enviar mensagens a um usuário. Para enviar dados com uma mensagem push, eles precisam ser criptografados e cabeçalhos específicos precisam ser adicionados para que o navegador possa descriptografar a mensagem corretamente.

O principal problema com o acionamento de push é que, se você encontrar um problema, será difícil diagnosticá-lo. Isso está melhorando com o tempo e o suporte mais amplo do navegador, mas está longe de ser fácil. Por esse motivo, é altamente recomendável usar uma biblioteca para lidar com a criptografia, a formatação e o acionamento da sua mensagem push.

Se você realmente quer saber o que as bibliotecas estão fazendo, vamos abordar isso na próxima seção. Por enquanto, vamos analisar o gerenciamento de assinaturas e o uso de uma biblioteca de push da Web para fazer as solicitações push.

Nesta seção, vamos usar a biblioteca Node web-push. Outros idiomas terão diferenças, mas não serão muito diferentes. Estamos analisando o Node, já que ele é JavaScript e deve ser o mais acessível para os leitores.

Vamos seguir estas etapas:

  1. Envie uma assinatura para nosso back-end e salve.
  2. Recupere assinaturas salvas e acione uma mensagem push.

Como salvar assinaturas

O salvamento e a consulta de PushSubscriptions de um banco de dados variam de acordo com a linguagem do servidor e a escolha do banco de dados, mas pode ser útil conferir um exemplo de como isso pode ser feito.

Na página da Web de demonstração, o PushSubscription é enviado ao nosso back-end fazendo uma solicitação POST simples:

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

O servidor Express na nossa demonstração tem um listener de solicitação correspondente para o endpoint /api/save-subscription/:

app.post('/api/save-subscription/', function (req, res) {

Nesta rota, validamos a assinatura apenas para garantir que a solicitação esteja correta e não esteja cheia de lixo:

const isValidSaveRequest = (req, res) => {
  // Check the request body has at least an endpoint.
  if (!req.body || !req.body.endpoint) {
    // Not a valid subscription.
    res.status(400);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'no-endpoint',
          message: 'Subscription must have an endpoint.',
        },
      }),
    );
    return false;
  }
  return true;
};

Se a assinatura for válida, precisamos salvá-la e retornar uma resposta JSON adequada:

return saveSubscriptionToDatabase(req.body)
  .then(function (subscriptionId) {
    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({data: {success: true}}));
  })
  .catch(function (err) {
    res.status(500);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'unable-to-save-subscription',
          message:
            'The subscription was received but we were unable to save it to our database.',
        },
      }),
    );
  });

Esta demonstração usa o nedb para armazenar as assinaturas. É um banco de dados simples baseado em arquivos, mas você pode usar qualquer banco de dados. Estamos usando isso porque não é necessário fazer nenhuma configuração. Para a produção, use algo mais confiável. Eu costumo usar o bom e velho MySQL.

function saveSubscriptionToDatabase(subscription) {
  return new Promise(function (resolve, reject) {
    db.insert(subscription, function (err, newDoc) {
      if (err) {
        reject(err);
        return;
      }

      resolve(newDoc._id);
    });
  });
}

Como enviar mensagens push

Quando se trata de enviar uma mensagem de push, precisamos de algum evento para acionar o processo de envio de uma mensagem aos usuários. Uma abordagem comum é criar uma página de administrador que permita configurar e acionar a mensagem push. No entanto, você pode criar um programa para ser executado localmente ou qualquer outra abordagem que permita acessar a lista de PushSubscriptions e executar o código para acionar a mensagem de push.

Nossa demonstração tem uma página "semelhante a administrador" que permite acionar um push. Como é apenas uma demonstração, é uma página pública.

Vou mostrar cada etapa envolvida para fazer a demonstração funcionar. Essas serão etapas gradual para que todos possam acompanhar, incluindo iniciantes no Node.

Quando discutimos a inscrição de um usuário, abordamos a adição de um applicationServerKey às opções do subscribe(). É no back-end que vamos precisar dessa chave privada.

Na demonstração, esses valores são adicionados ao nosso app Node dessa forma (código chato, eu sei, mas quero que você saiba que não há mágica):

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

Em seguida, precisamos instalar o módulo web-push para o servidor do nó:

npm install web-push --save

Em seguida, no script do Node, exigimos o módulo web-push da seguinte maneira:

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

Agora podemos começar a usar o módulo web-push. Primeiro, precisamos informar ao módulo web-push sobre nossas chaves do servidor do aplicativo. Elas também são conhecidas como chaves VAPID, porque esse é o nome da especificação.

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

webpush.setVapidDetails(
  'mailto:web-push-book@gauntface.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey,
);

Também incluímos uma string "mailto:". Essa string precisa ser um URL ou um endereço de e-mail do mailto:. Essa informação será enviada ao serviço de push da Web como parte da solicitação para acionar um push. Isso é feito para que, se um serviço de push da Web precisar entrar em contato com o remetente, ele tenha algumas informações que permitam isso.

Com isso, o módulo web-push está pronto para uso. A próxima etapa é acionar uma mensagem push.

A demonstração usa o painel de administrador falso para acionar mensagens push.

Captura de tela da página Administrador.

Clicar no botão "Trigger Push Message" vai fazer uma solicitação POST para /api/trigger-push-msg/, que é o indicador para que o back-end envie mensagens push. Portanto, criamos a rota no Express para esse endpoint:

app.post('/api/trigger-push-msg/', function (req, res) {

Quando essa solicitação é recebida, extraímos as assinaturas do banco de dados e acionamos uma mensagem push para cada uma delas.

return getSubscriptionsFromDatabase().then(function (subscriptions) {
  let promiseChain = Promise.resolve();

  for (let i = 0; i < subscriptions.length; i++) {
    const subscription = subscriptions[i];
    promiseChain = promiseChain.then(() => {
      return triggerPushMsg(subscription, dataToSend);
    });
  }

  return promiseChain;
});

A função triggerPushMsg() pode usar a biblioteca web-push para enviar uma mensagem à assinatura fornecida.

const triggerPushMsg = function (subscription, dataToSend) {
  return webpush.sendNotification(subscription, dataToSend).catch((err) => {
    if (err.statusCode === 404 || err.statusCode === 410) {
      console.log('Subscription has expired or is no longer valid: ', err);
      return deleteSubscriptionFromDatabase(subscription._id);
    } else {
      throw err;
    }
  });
};

A chamada para webpush.sendNotification() vai retornar uma promessa. Se a mensagem tiver sido enviada, a promessa vai ser resolvida e não há nada que precisamos fazer. Se a promessa for rejeitada, você precisará examinar o erro, porque ele informará se o PushSubscription ainda é válido ou não.

Para determinar o tipo de erro de um serviço push, é melhor verificar o código de status. As mensagens de erro variam entre os serviços push, e algumas são mais úteis do que outras.

Neste exemplo, ele verifica os códigos de status 404 e 410, que são os códigos de status HTTP para "Não encontrado" e "Desaparecido". Se recebermos um desses, significa que a assinatura expirou ou não está mais válida. Nesses cenários, precisamos remover as assinaturas do nosso banco de dados.

Em caso de algum outro erro, throw err, o que vai fazer com que a promessa retornada por triggerPushMsg() seja rejeitada.

Vamos abordar alguns dos outros códigos de status na próxima seção, quando analisarmos o protocolo de push da Web com mais detalhes.

Depois de percorrer as assinaturas, precisamos retornar uma resposta JSON.

.then(() => {
res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({ data: { success: true } }));
})
.catch(function(err) {
res.status(500);
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({
    error: {
    id: 'unable-to-send-messages',
    message: `We were unable to send messages to all subscriptions : ` +
        `'${err.message}'`
    }
}));
});

Analisamos as principais etapas de implementação:

  1. Crie uma API para enviar inscrições da nossa página da Web para o back-end para que elas possam ser salvas em um banco de dados.
  2. Crie uma API para acionar o envio de mensagens de push. Nesse caso, uma API chamada a partir do painel de administração simulado.
  3. Extraia todas as assinaturas do nosso back-end e envie uma mensagem para cada assinatura com uma das bibliotecas push da Web.

Seja qual for seu back-end (Node, PHP, Python...), as etapas para implementar push serão as mesmas.

Agora, o que exatamente essas bibliotecas de push da Web estão fazendo por nós?

A seguir

Laboratórios de códigos