Renderização de arquivo HTML: um capítulo do livro ReactPHP for Beginners de Skyeng


O desenvolvedor de back-end de aplicativos móveis Skyeng, Sergey Zhuk, continua escrevendo bons livros. Desta vez, ele lançou um livro em russo para um público que domina o PHP. Pedi a Sergey que compartilhasse um capítulo útil e auto-suficiente de seu livro e que desse aos leitores de Habra um código de desconto. Abaixo estão os dois.


Primeiro, vamos lhe dizer o que paramos nos capítulos anteriores.

Escrevemos nosso servidor HTTP simples em PHP. Temos o arquivo index.php principal - o script que inicia o servidor. Aqui está o código de nível mais alto: criamos um loop de eventos, configuramos o comportamento do servidor HTTP e iniciamos o loop:


 use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; $loop = React\EventLoop\Factory::create(); $router = new Router(); $router->load('routes.php'); $server = new Server( function (ServerRequestInterface $request) use ($router) { return $router($request); } ); $socket = new React\Socket\Server(8080, $loop); $server->listen($socket); $loop->run(); 

Para rotear solicitações, o servidor usa um roteador:


 // src/Router.php use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; class Router { private $routes = []; public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request); } public function load($filename) { $routes = require $filename; foreach ($routes as $path => $handler) { $this->add($path, $handler); } } public function add($path, callable $handler) { $this->routes[$path] = $handler; } private function notFound($path) { return function () use ($path) { return new Response( 404, ['Content-Type' => 'text/html; charset=UTF-8'], "No request handler found for $path" ); }; } } 

As rotas do arquivo routes.php são carregadas no routes.php . Agora, apenas duas rotas foram anunciadas aqui:


 use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Main page' ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Até agora, tudo é simples e nosso aplicativo assíncrono se encaixa em vários arquivos.


Passamos a coisas mais "úteis". As respostas de algumas palavras de um texto simples que aprendemos a derivar nos capítulos anteriores não parecem muito atraentes. Precisamos retornar algo real, como uma página HTML.


Então, onde colocamos esse HTML? Obviamente, você pode codificar o conteúdo da página da web dentro do arquivo de rotas:


 // routes.php return [ '/' => function (ServerRequestInterface $request) { $html = <<<HTML <!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8”> <title>ReactPHP App</title> </head> <body> Hello, world </body> </html> HTML; return new Response( 200, ['Content-Type' => 'text/html'], $html ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Mas não faça isso! Você não pode misturar lógica de negócios (roteamento) com apresentação (página HTML). Porque Imagine que você precise alterar algo no código HTML, por exemplo, a cor do botão. E qual arquivo precisará ser alterado? Arquivo com rotas router.php ? Soa estranho, certo? Faça alterações no roteiro para alterar a cor do botão ...


Portanto, deixaremos as rotas em paz e, para as páginas HTML, criaremos um diretório separado. Na raiz do projeto, adicione um novo diretório chamado páginas. Então, dentro dele, criamos o arquivo index.html . Esta será a nossa página principal. Aqui está o seu conteúdo:


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ReactPHP App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" > </head> <body> <div class="container"> <div class="row"> <form action="/upload" method="POST" class="justify-content-center"> <div class="form-group"> <label for="text">Text</label> <textarea name="text" id="text" class="form-control"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </body> </html> 

A página é bastante simples, contém apenas um elemento - o formulário. O formulário dentro tem uma caixa de texto e um botão para enviar. Também adicionei estilos de Bootstrap para tornar nossa página mais agradável.


Lendo arquivos. Como NÃO fazer


A abordagem mais direta é ler o conteúdo do arquivo dentro do manipulador de solicitações e retornar esse conteúdo como o corpo da resposta. Algo assim:


 // routes.php return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, // ... ]; 

E, a propósito, vai funcionar. Você pode tentar você mesmo: reinicie o servidor e recarregue a página http://127.0.0.1:8080/ no seu navegador.



Então, o que há de errado aqui? E porque não fazer isso? Em resumo, porque haverá problemas se o sistema de arquivos começar a ficar mais lento.


Bloqueando e não bloqueando chamadas


Deixe-me mostrar o que quero dizer com "bloqueio" de chamadas e o que pode acontecer quando um dos manipuladores de solicitação contém código de bloqueio. Antes de retornar o objeto de resposta, adicione uma chamada à função sleep() :


 // routes.php return [ '/' => function (ServerRequestInterface $request) { sleep(10); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Isso fará com que o manipulador de solicitações congele por 10 segundos antes de poder retornar uma resposta com o conteúdo da página HTML. Observe que não tocamos no manipulador do endereço /upload . Ao chamar a função sleep(10) , eu emulo a execução de algum tipo de operação de bloqueio.


Então o que nós temos? Quando o navegador solicita a página / , o manipulador aguarda 10 segundos e retorna a página HTML. Quando abrimos o endereço /upload , seu manipulador deve retornar imediatamente uma resposta com a string 'Upload page'.


Agora vamos ver o que acontece na realidade. Como sempre, reiniciamos o servidor. Agora, abra outra janela no seu navegador. Na barra de endereço, digite http://127.0.0.1:8080/upload , mas não abra esta página imediatamente. Deixe esse endereço na barra de endereços por enquanto. Em seguida, vá para a primeira janela do navegador e abra a página http://127.0.0.1:8080/ . Enquanto esta página estiver carregando (lembre-se de que levará 10 segundos para fazer isso), vá rapidamente para a segunda janela e pressione “Enter” para carregar o endereço que foi deixado na barra de endereços ( http://127.0.0.1:8080/upload ) .


O que conseguimos? Sim, o endereço /, como esperado, leva 10 segundos para carregar. Surpreendentemente, porém, a segunda página demorou o mesmo tempo para carregar, embora não tenhamos adicionado nenhuma chamada sleep() . Alguma idéia de por que isso aconteceu?


O ReactPHP é executado em um único thread. Pode parecer que em um aplicativo assíncrono, as tarefas são executadas em paralelo, mas, na realidade, não é assim. A ilusão de paralelismo é criada por um ciclo de eventos que alterna constantemente entre várias tarefas e as executa. Mas em um determinado momento, apenas uma tarefa é sempre executada. Isso significa que, se uma dessas tarefas demorar muito, ele bloqueará o loop de eventos, que não poderá registrar novos eventos e chamar manipuladores para eles. E, finalmente, isso leva ao "congelamento" de todo o aplicativo, ele simplesmente perde a assincronia.


OK, mas o que isso tem a ver com chamar file_get_contents('pages/index.h') ? O problema aqui é que estamos acessando o sistema de arquivos diretamente. Comparado a outras operações, como trabalhar com memória ou computação, trabalhar com o sistema de arquivos pode ser extremamente lento. Por exemplo, se o arquivo for muito grande ou o próprio disco estiver lento, a leitura do arquivo poderá levar algum tempo e, como resultado, bloquear o loop de eventos.


No modelo síncrono padrão, solicitação-resposta não é um problema. Se o cliente solicitou um arquivo muito pesado, ele aguardará até que o arquivo seja baixado. Uma solicitação tão pesada não afeta outros clientes. Mas, no nosso caso, estamos lidando com um modelo assíncrono orientado a eventos. Lançamos um servidor HTTP que deve processar constantemente as solicitações recebidas. Se uma solicitação demorar muito para ser concluída, isso afetará todos os outros clientes do servidor.


Como regra, lembre-se:


  • Você nunca pode bloquear um loop de eventos.

Então, como lemos o arquivo de forma assíncrona? E aqui chegamos à segunda regra:


  • Quando uma operação de bloqueio não pode ser evitada, ela deve ser bifurcada no processo filho e continuar a execução assíncrona no encadeamento principal.

Então, depois de aprendermos como não fazer isso, vamos discutir a solução não-bloqueio correta.


Processo filho


Toda a comunicação com o sistema de arquivos em um aplicativo assíncrono deve ser executada em processos filho. Para gerenciar processos filhos em um aplicativo ReactPHP, precisamos instalar outro componente "Processo filho" . Este componente permite acessar as funções do sistema operacional para executar qualquer comando do sistema dentro do processo filho. Para instalar este componente, abra um terminal na raiz do projeto e execute o seguinte comando:


composer require react/child-process


Compatibilidade com Windows


No sistema operacional Windows, os encadeamentos STDIN, STDOUT e STDERR estão bloqueando, o que significa que o componente Processo do Filho não poderá funcionar corretamente. Portanto, esse componente foi projetado principalmente para funcionar apenas em sistemas nix. Se você tentar criar um objeto da classe Process em um sistema Windows, uma exceção será lançada. Mas o componente pode funcionar no Windows Subsystem for Linux (WSL) . Se você pretende usar este componente no Windows, precisará instalar o WSL.


Agora podemos executar qualquer comando shell dentro do processo filho. Abra o arquivo routes.php e, em seguida, routes.php o manipulador da rota / . Crie um objeto da classe React\ChildProcess\Process e, como um comando, passe ls para ele para obter o conteúdo do diretório atual:


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request) { $childProcess = new Process('ls'); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, // ... ]; 

Então precisamos iniciar o processo chamando o método start() . O problema é que o método start() precisa de um objeto de loop de eventos. Mas no arquivo routes.php não temos esse objeto. Como passamos o loop de eventos de index.php para rotas diretamente para o manipulador de solicitações? A solução para esse problema é "injeção de dependência".


Injeção de Dependência


Portanto, uma de nossas rotas precisa de um loop de eventos para funcionar. Em nossa aplicação, apenas um componente conhece a existência de rotas - a classe Router . Acontece que é sua responsabilidade fornecer um loop de eventos para as rotas. Em outras palavras, o roteador precisa de um loop de eventos ou depende do loop de eventos. Como expressamos explicitamente essa dependência no código? Como tornar impossível criar um roteador sem passar um loop de eventos para ele? Obviamente, através do construtor da classe Router . Abra Router.php e adicione o construtor à classe Router :


 use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Response; class Router { private $routes = []; /** * @var LoopInterface */ private $loop; public function __construct(LoopInterface $loop) { $this->loop = $loop; } // ... } 

Dentro do construtor, salve o loop de eventos passados ​​na propriedade privada $loop . Isso é injeção de dependência quando fornecemos à classe os objetos de que ela precisa para trabalhar externamente.


Agora que temos esse novo construtor, precisamos atualizar a criação do roteador. Abra o arquivo index.php e corrija a linha em que criamos o objeto da classe Router :


 // index.php $loop = React\EventLoop\Factory::create(); $router = new Router($loop); $router->load('routes.php'); 

Feito. Volte para routes.php . Como você provavelmente já adivinhou, aqui podemos usar a mesma idéia com injeção de dependência e adicionar um loop de eventos como um segundo parâmetro aos nossos manipuladores de consultas. Altere o primeiro retorno de chamada e adicione o segundo argumento: um objeto que implementa LoopInterface :


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request, LoopInterface $loop) { $childProcess = new Process('ls'); $childProcess->start($loop); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Em seguida, precisamos passar o loop de eventos para o método start() do processo filho. E onde o manipulador obtém o loop do evento? E já está armazenado dentro do roteador na propriedade privada $loop . Nós só precisamos passar quando o manipulador é chamado.


__invoke() abrir a classe Router e atualizar o método __invoke() , adicionando o segundo argumento à chamada do manipulador de solicitações:


 public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request, $this->loop); } 

Isso é tudo! Provavelmente, isso é injeção de dependência suficiente. Uma jornada bastante grande do ciclo de eventos aconteceu, certo? Do arquivo index.php à classe Router e, em seguida, da classe Router ao arquivo routes.php diretamente dentro dos retornos de chamada.


Portanto, para confirmar que o processo filho fará sua mágica sem bloqueio, vamos substituir o ls simples pelo ping 8.8.8.8 mais pesado. Reinicie o servidor e tente novamente duas páginas em duas janelas diferentes. Primeiro, http://127.0.0.1:8080/ e depois /upload . Ambas as páginas abrem rapidamente, sem demora, embora o comando ping seja executado no primeiro manipulador em segundo plano. A propósito, isso significa que podemos fazer uma operação cara (por exemplo, processar arquivos grandes), sem bloquear o aplicativo principal.


Vincular processo filho e resposta usando threads


Vamos voltar ao nosso aplicativo. Por isso, criamos um processo filho e o iniciamos, mas nosso navegador não exibe os resultados de uma operação bifurcada de forma alguma. Vamos consertar.


Como podemos nos comunicar com o processo filho? No nosso caso, temos um ls execução que exibe o conteúdo do diretório atual. Como chegamos a essa conclusão e a enviamos ao corpo da resposta? A resposta curta é: tópicos.


Vamos falar um pouco sobre processos. Qualquer comando shell que você executa possui três fluxos de dados: STDIN, STDOUT e STDERR. Transmita para saída e entrada padrão, além de transmitir erros. Por exemplo, quando executamos o ls , o resultado desse comando é enviado diretamente para STDOUT (na tela do terminal). Portanto, se precisarmos obter a saída de um processo, é necessário acesso ao fluxo de saída. E isso é tão fácil quanto descascar peras. Ao criar o objeto de resposta, substitua a chamada file_get_contents() por $childProcess->stdout :


 return new Response( 200, ['Content-Type' => 'text/plain'], $childProcess->stdout ); 

Todos os processos filhos têm três propriedades relacionadas aos fluxos stdio : stdout , stdin e stderr . No nosso caso, queremos exibir a saída do processo em uma página da web. Em vez de uma sequência no construtor da classe Response , passamos um fluxo como o terceiro argumento. A classe Response é inteligente o suficiente para perceber que recebeu o fluxo e processá-lo adequadamente.


Portanto, como sempre, reinicializamos o servidor e vemos o que fizemos. Vamos abrir a página http://127.0.0.1:8080/ no navegador: você deve ver uma lista de arquivos da pasta raiz do projeto.



A etapa final é substituir o ls por algo mais útil. Começamos este capítulo renderizando o arquivo pages/index.html usando a função file_get_contents() . Agora, podemos ler este arquivo de forma absolutamente assíncrona, sem nos preocuparmos com o bloqueio do aplicativo. Substitua o ls por cat pages/index.html .


Se você não estiver familiarizado com o cat , ele será usado para concatenar e gerar arquivos. Na maioria das vezes, esse comando é usado para ler um arquivo e enviar seu conteúdo para a saída padrão. O comando cat pages/index.html lê o arquivo cat pages/index.html e imprime seu conteúdo em STDOUT. E já estamos enviando stdout como o corpo da resposta. Aqui está a versão final do arquivo routes.php :


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request, LoopInterface $loop) { $childProcess = new Process('cat pages/index.html'); $childProcess->start($loop); return new Response( 200, ['Content-Type' => 'text/html'], $childProcess->stdout ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Como resultado, todo esse código foi necessário apenas para substituir uma chamada para a função file_get_contents() . Injeção de dependência, passando um objeto de loop de eventos, adicionando processos filhos e trabalhando com threads. Tudo isso é apenas para substituir uma chamada de função. Valeu a pena? Resposta: sim, valeu a pena. Quando algo puder bloquear o loop de eventos, e o sistema de arquivos puder definitivamente, certifique-se de que, eventualmente, ele será bloqueado e no momento mais inoportuno.


Criar um processo filho toda vez que precisamos acessar o sistema de arquivos pode parecer uma sobrecarga extra que afetará a velocidade e o desempenho de nosso aplicativo. Infelizmente, no PHP não há outra maneira de trabalhar com o sistema de arquivos de forma assíncrona. Todas as bibliotecas PHP assíncronas usam processos filhos (ou extensões que as abstraem).


Os leitores da Habra podem comprar o livro inteiro com desconto neste link .


E lembramos que estamos sempre em busca de desenvolvedores legais ! Venha, nos divertimos!

Source: https://habr.com/ru/post/pt416003/


All Articles