Conceitos básicos sobre serviços da web

HTML5 Rocks

O problema: simultaneidade do JavaScript

Existem vários gargalos que impedem a portabilidade de aplicativos interessantes (por exemplo, de implementações pesadas de servidor) para o JavaScript no lado do cliente. Entre eles, estão a compatibilidade de navegador, os tipos estáticos, a acessibilidade e o desempenho. Felizmente, este último problema vem sendo superado rapidamente, à medida que os fornecedores de navegadores aprimoram a velocidade dos seus mecanismos de JavaScript.

Na verdade, uma questão que continua sendo um obstáculo para o JavaScript é a própria linguagem. O JavaScript é um ambiente de sequência única, isso significa que não é possível executar vários scripts ao mesmo tempo. Por exemplo, imagine um site que precisa manipular eventos de interface do usuário, consultar e processar grandes quantidades de dados de API e manipular o DOM. Bem comum, não é mesmo? Infelizmente, nem tudo isso pode ser feito ao mesmo tempo, devido às limitações no tempo de execução do JavaScript dos navegadores. A execução do script ocorre dentro de uma única sequência.

Os desenvolvedores emulam a "simultaneidade" usando técnicas como setTimeout(), setInterval(), XMLHttpRequest e até mesmo manipuladores de eventos. Sim, todos esses recursos são executados de forma assíncrona, o modelo sem bloqueio não significa necessariamente simultaneidade. Eventos assíncronos são processados depois que o script de execução atual gera seus resultados. A boa notícia é que o HTML5 oferece muito mais do que isso.

Introdução ao Web Workers: como trazer o sequenciamento ao JavaScript.

A especificação Web Workers (link em inglês) define uma API para geração de scripts de segundo plano no seu aplicativo da web. O Web Workers permite executar tarefas como disparar scripts de longa duração para executar tarefas muito dispendiosas, mas sem bloquear a interface de usuário ou outros scripts para manipular interações com o usuário. Isso poderá ser o fim daquele diálogo de "script não responde" que todos nós adoramos:

Diálogo
Diálogo "script não responde" comum.

O Workers utiliza a transmissão de mensagem do tipo sequência para obter paralelismo. É perfeito para manter a interface atualizada, com desempenho e responsiva para os usuários.

Tipos de Web Workers

É interessante observar que a especificação (link em inglês) discute dois tipos de Web Workers, Workers dedicados (link em inglês) e Workers compartilhados (link em inglês). Este artigo trata somente dos workers dedicados e refere-se a eles como "web workers" ou "workers".

Primeiros passos

Os Web Workers são executados em uma sequência isolada. Como resultado, o código que eles executam precisa estar contido em um arquivo separado. Mas, antes de fazer isso, a primeira coisa a fazer é criar um novo objeto Worker na página principal. O construtor leva o nome do script do worker:

var worker = new Worker('task.js');

Se o arquivo especificado existir, o navegador gerará uma nova sequência do worker, que será transferida por download assíncrono. O worker só é iniciado quando o arquivo tiver sido transferido completamente e executado. Se o caminho para o worker retornar um 404, o worker tem falha silenciosa.

Depois de criar o worker, inicie-o chamando o método postMessage():

worker.postMessage(); // Start the worker.

Comunicação com Workers por transmissão de mensagem

A comunicação entre um worker e sua página de origem é feita por um modelo de evento e pelo método postMessage(). Dependendo do navegador/versão, o postMessage() pode aceitar uma string de texto ou um objeto JSON como seu único argumento. As versões mais recentes dos navegadores modernos suportam a transmissão de um objeto JSON.

Abaixo, temos um exemplo do uso de uma string para transmitir "Hello World" ("Olá, Mundo") para um worker em doWork.js. O worker simplesmente retorna a mensagem que foi transmitida para ele.

Script principal:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
  console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js (o worker):

self.addEventListener('message', function(e) {
  self.postMessage(e.data);
}, false);

Quando postMessage() é chamado da página principal, nosso worker manipula a mensagem definindo um manipulador onmessage para o evento message. A carga útil da mensagem (no caso "Hello World") fica acessível em Event.data. Ainda que este exemplo em especial não seja muito emocionante, ele demonstra que postMessage() também é um meio de transmitir dados de volta para a sequência principal. Prático!

As mensagens transmitidas entre a página principal e os workers são copiadas, não compartilhadas. Por exemplo, no próximo exemplo a propriedade "msg" da mensagem JSON está acessível em ambos os locais. Parece que o objeto está sendo transmitido diretamente para o worker ainda que esteja em execução em um espaço separado, específico. Na verdade, o que acontece é que o objeto está sendo serializado enquanto é entregue para o worker e, subsequentemente, desserializado na outra terminação. A página e o worker não compartilham a mesma instância, portanto o resultado final é uma duplicata criada em cada transmissão. A maioria dos navegadores implementa este recurso pela codificação / decodificação JSON automática do valor em uma das terminações.

O exemplo a seguir é mais complexo, as mensagens são transmitidas usando os objetos JSON.

Script principal:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
  function sayHI() {
    worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
  }

  function stop() {
    // Calling worker.terminate() from this script would also stop the worker.
    worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
  }

  function unknownCmd() {
    worker.postMessage({'cmd': 'foobard', 'msg': '???'});
  }

  var worker = new Worker('doWork2.js');

  worker.addEventListener('message', function(e) {
    document.getElementById('result').textContent = e.data;
  }, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg + '. (buttons will no longer work)');
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

Observação: existem duas maneiras de parar um worker: chamar worker.terminate() na página principal ou chamar self.close() dentro do próprio worker.

Exemplo: execute este worker!

O ambiente do worker

Escopo do worker

No contexto de um worker, tanto self como this fazem referência ao escopo global d o worker. Portanto, o exemplo anterior poderia ser escrito também assim:

addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
  ...
}, false);

Uma alternativa seria configurar o manipulador de evento onmessage diretamente (emboraaddEventListener seja sempre a recomendação dos mestres do JavaScript).

onmessage = function(e) {
  var data = e.data;
  ...
};

Recursos disponíveis para workers

Devido ao comportamento multi-sequenciado, os web workers só têm acesso a um subconjunto dos recursos do JavaScript:

  • O objeto navigator
  • O objeto location (somente leitura)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() e setInterval()/clearInterval()
  • O Cache do aplicativo (link em inglês)
  • Importação de scripts externos usando o método importScripts()
  • Gerar outros web workers

Os workers NÃO têm acesso a:

  • O DOM (não é thread-safe)
  • O objeto window
  • O objeto document
  • O objeto parent

Como carregar scripts externos

É possível carregar arquivos de script ou bibliotecas em um worker com a função importScripts(). O método toma zero ou mais strings que representam os nomes de arquivo para os recursos que serão importados.

Este exemplo carrega script1.js e script2.js no worker:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

O que também pode ser escrito como uma única instrução de importação:

importScripts('script1.js', 'script2.js');

Subworkers

Os workers têm a habilidade de gerar workers filhos. Isso é ótimo para subdividir tarefas muito grandes no tempo de execução. Contudo, os subworkers vêm com algumas restrições:

  • Os subworkers devem ser hospedados na mesma origem que a página pai.
  • URIs dentro dos subworkers são resolvidos com relação ao local do worker pai (em oposição à página principal).

Lembre-se de que a maioria dos navegadores gera processos separados para cada worker. Antes de gerar uma farm de worker, tenha cuidado para não consumir os recursos do sistema do usuário. Um dos motivos para isso é que as mensagens transmitidas entre as páginas principais e os workers são copiadas e não compartilhadas. Consulte Como se comunicar com um worker por transmissão de mensagem.

Para uma amostra de como criar um subworker, veja o exemplo (link em inglês) na especificação.

Workers embutidos

E se você desejasse criar o script do worker durante a execução ou criar uma página independente sem ter de criar arquivos separados de worker? Com a nova interface BlobBuilder (link em inglês), é possível "embutir" o worker no mesmo arquivo HTML que a lógica principal pela criação de um BlobBuilder e anexação do código do worker como uma string:

// Prefixed in Webkit, Chrome 12, and FF6: window.WebKitBlobBuilder, window.MozBlobBuilder
var bb = new BlobBuilder();
bb.append("onmessage = function(e) { postMessage('msg from worker'); }");

// Obtain a blob URL reference to our worker 'file'.
// Note: window.webkitURL.createObjectURL() in Chrome 10+.
var blobURL = window.URL.createObjectURL(bb.getBlob());

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
  // e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

URLs do Blob

A mágica acontece com a chamada para window.URL.createObjectURL() (link em inglês). Esse método cria uma string simples de URL, que pode ser usada para fazer referência aos dados armazenados em um File DOM ou objeto Blob. Por exemplo:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

Os URLs do Blob são únicos e duram toda a vida do aplicativo (por exemplo, até que o document seja descarregado). Se você for criar muitos URLs do Blob, é recomendável dispensar as referências que não são mais necessárias. É possível dispensar um URL do Blob explicitamente transmitindo-o para window.URL.revokeObjectURL() (link em inglês):

window.URL.revokeObjectURL(blobURL); // window.webkitURL.createObjectURL() in Chrome 10+.

No Google Chrome, há uma página boa para visualizar todos os URLs de blob criados: chrome://blob-internals/ (link em inglês).

Exemplo completo

Ao dar esse passo, podemos adquirir habilidade na forma como o código JS do worker é embutido na nossa página. Esta técnica usa uma tag <script> para definir o worker:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>

  <div id="log"></div>

  <script id="worker1" type="javascript/worker">
    // This script won't be parsed by JS engines because its type is javascript/worker.
    self.onmessage = function(e) {
      self.postMessage('msg from worker');
    };
    // Rest of your worker code goes here.
  </script>

  <script>
    function log(msg) {
      // Use a fragment: browser will only render/reflow once.
      var fragment = document.createDocumentFragment();
      fragment.appendChild(document.createTextNode(msg));
      fragment.appendChild(document.createElement('br'));

      document.querySelector("#log").appendChild(fragment);
    }

    var bb = new BlobBuilder();
    bb.append(document.querySelector('#worker1').textContent);

    // Note: window.webkitURL.createObjectURL() in Chrome 10+.
    var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
    worker.onmessage = function(e) {
      log("Received: " + e.data);
    }
    worker.postMessage(); // Start the worker.
  </script>
</body>
</html>

Em minha opinião, essa nova abordagem é um pouco mais limpa e mais legível. Ela define uma tag de script com id="worker1" e type='javascript/worker' (para que o navegador não analise o JS). Esse código é extraído como uma string usando document.querySelector('#worker1').textContent e transmitido para BlobBuilder.append().

Como carregar scripts externos

Ao usar essas técnicas para embutir seu código de worker, o importScripts() funciona apenas se você fornecer um URI absoluto. Se tentar passar um URI relativo, o navegador emite um erro de segurança. A razão disso: o worker (agora criado em um URL de blob) será resolvido com o prefixo blob:, enquanto o aplicativo será executado em um esquema diferente (provavelmente http://). Dessa forma, a falha será devida às restrições de origens diferentes.

Uma forma de utilizar o importScripts() em um worker embutido é "injetar" o url atual em que script principal é executado pela transmissão dele para o worker embutido e a construção do URL absoluto manualmente. Isso garante que o script externo seja importado da mesma origem. Vamos supor que o aplicativo principal seja executado em http://example.com/index.html:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
  var data = e.data;

  if (data.url) {
    var url = data.url.href;
    var index = url.indexOf('index.html');
    if (index != -1) {
      url = url.substring(0, index);
    }
    importScripts(url + 'engine.js');
  }
  ...
};
</script>
<script>
  var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
  worker.postMessage({url: document.location});
</script>

Como resolver erros

Como toda lógica do JavaScript, é recomendável resolver quaisquer erros que sejam lançados nos web workers. Se ocorrer algum enquanto um worker estiver em execução, um ErrorEvent é acionado. A interface contém três propriedades úteis para descobrir o que deu errado: filename - o nome do script do worker que causou o erro, lineno - o número da linha em que o erro ocorreu e message - uma descrição significativa do erro. Aqui temos um exemplo de configuração de um manipulador de evento onerror para imprimir as propriedades do erro:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
  function onError(e) {
    document.getElementById('error').textContent = [
      'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('');
  }

  function onMsg(e) {
    document.getElementById('result').textContent = e.data;
  }

  var worker = new Worker('workerWithError.js');
  worker.addEventListener('message', onMsg, false);
  worker.addEventListener('error', onError, false);
  worker.postMessage(); // Start worker without a message.
</script>

Exemplo: workerWithError.js tenta executar 1/x, em que x está indefinido.

workerWithError.js:

self.addEventListener('message', function(e) {
  postMessage(1/x); // Intentional error.
};

Nota sobre segurança

Restrições ao acesso local

Devido às restrições de segurança do Google Chrome, os workers não são executados localmente (por exemplo, em file://) nas versões mais recentes do navegador. Em vez disso, eles geram falha silenciosa. Para executar o aplicativo no esquema file://, execute o Google Chrome com o sinal --allow-file-access-from-files definido. OBSERVAÇÃO: Não é recomendável executar o navegador principal com esse sinal definido. Ele deve ser usado apenas para fins de teste e não durante a navegação normal.

Outros navegadores não impõem a mesma restrição.

Algumas considerações sobre a origem

Os scripts de workers devem ser arquivos externos com o mesmo esquema de sua página de chamada. Portanto, não é possível carregar um script de um URL data: ou javascript:, e uma página https: não pode iniciar os scripts de worker que começam com URLs http:.

Casos de uso

Então, qual tipo de aplicativo utilizar com os web workers? Infelizmente, os web workers ainda são relativamente novos e a maioria das amostras/tutoriais que existem envolvem computação de números primos. Ainda que isso não seja muito interessante, é útil para a compreensão dos conceitos de web workers. Seguem mais algumas ideias para você quebrar a cabeça:

  • Pré-busca e/ou armazenamento em cache dos dados para usar mais tarde
  • Destaque da sintaxe do código ou outra formatação de texto em tempo real
  • Corretor ortográfico
  • Análise de dados de áudio e vídeo
  • E/S em segundo plano ou sondagem de serviços da web
  • Processamento de grandes matrizes ou respostas JSON gigantescas
  • Filtro de imagens em <canvas>
  • Atualização de muitas linhas de um banco de dados Web local

Demonstrações

Referências

Comments

0