HTML文件渲染:Skyeng撰写的《 ReactPHP for Beginners》一章


Skyeng移动应用程序后端开发人员Sergey Zhuk继续写好书。 这次,他为俄语精通的读者发行了俄文教科书。 我请谢尔盖(Sergey)分享书中有用的自给自足章节,并为哈布拉(Habra)读者提供折扣代码。 下面是两者。


首先,让我们告诉您前面几章中我们停止过的内容。

我们已经用PHP编写了简单的HTTP服务器。 我们有一个主要的index.php文件-启动服务器的脚本。 这是最高级别的代码:我们创建一个事件循环,配置HTTP服务器的行为并开始循环:


 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(); 

要路由请求,服务器使用路由器:


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

来自routes.php文件的路由被加载到routes.php 。 现在,这里仅宣布了两条路线:


 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' ); }, ]; 

到目前为止,一切都很简单,并且我们的异步应用程序可以放入几个文件中。


我们传递给更多“有用”的东西。 我们从前几章中学到的纯文本中的几个单词的答案看起来并不吸引人。 我们需要返回真实的内容,例如HTML页面。


那么,我们将HTML放在哪里? 当然,您可以在路由文件中对网页的内容进行硬编码:


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

但是不要那样做! 您不能将业务逻辑(路由)与演示文稿(HTML页面)混合使用。 怎么了 假设您需要更改HTML代码中的某些内容,例如按钮的颜色。 哪个文件需要更改? 与路由文件router.php ? 听起来很怪吧? 更改路由以更改按钮的颜色...


因此,我们将不理会这些路由,对于HTML页面,我们将创建一个单独的目录。 在项目的根目录,添加一个名为pages的新目录。 然后在其中创建index.html文件。 这将是我们的主页。 其内容如下:


 <!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> 

该页面非常简单,它仅包含一个元素-表单。 里面的表单有一个文本框和一个提交按钮。 我还添加了Bootstrap样式,以使我们的页面看起来更好。


正在读取文件。 怎么做


最直接的方法是读取请求处理程序中文件的内容,并将该内容作为响应正文返回。 像这样:


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

顺便说一句,它将起作用。 您可以自己尝试:重新启动服务器,然后在浏览器中重新加载页面http://127.0.0.1:8080/



那怎么了 为什么不这样做呢? 简而言之,因为如果文件系统开始变慢,将会出现问题。


阻塞和非阻塞呼叫


让我向您展示“阻塞”调用的含义,以及其中一个请求处理程序包含阻塞代码时可能发生的情况。 在返回响应对象之前,请添加一个对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' ); }, ]; 

这将使请求处理程序冻结10秒钟,然后它才能返回带有HTML页面内容的响应。 请注意,我们没有触摸/upload地址的处理程序。 通过调用sleep(10)函数,我模拟了某种阻塞操作的执行。


那我们有什么呢? 当浏览器请求页面/ ,处理程序将等待10秒钟,然后返回HTML页面。 当我们打开/upload地址时,其处理程序应立即返回带有字符串“ Upload page”的响应。


现在,让我们看看现实中会发生什么。 与往常一样,我们重新启动服务器。 现在,请在浏览器中打开另一个窗口。 在地址栏中,输入http://127.0.0.1:8080/upload ,但不要立即打开此页面。 只需将此地址暂时留在地址栏中。 然后转到第一个浏览器窗口,并在其中打开页面http://127.0.0.1:8080/ 。 在加载此页面时(请记住,此过程将需要10秒钟),请快速转到第二个窗口并按“ Enter”以加载地址栏中剩余的地址( http://127.0.0.1:8080/upload ) 。


我们得到了什么? 是的,地址/符合预期,需要10秒钟加载。 但是,令人惊讶的是,尽管我们没有向其添加任何sleep()调用,但第二页花费了相同的时间来加载。 知道为什么会这样吗?


ReactPHP在单个线程中运行。 似乎在异步应用程序中,任务是并行执行的,但实际上并非如此。 并行的错觉是由一系列事件构成的,这些事件不断地在各种任务之间切换并执行它们。 但是在某个时间点,总是只执行一项任务。 这意味着,如果这些任务之一花费的时间太长,它将阻止事件循环,这将无法注册新事件并为其调用处理程序。 最终导致整个应用程序“冻结”,它只会失去异步性。


可以,但是这与调用file_get_contents('pages/index.h')什么关系? 这里的问题是我们正在直接访问文件系统。 与其他操作(例如,使用内存或计算)相比,使用文件系统可能会非常慢。 例如,如果结果证明文件太大或磁盘本身很慢,则读取文件可能会花费一些时间,因此会阻塞事件循环。


在标准同步模型中,请求-响应不是问题。 如果客户端请求的文件太重,它将等待下载此文件。 如此繁重的请求不会影响其他客户。 但就我们而言,我们正在处理一个异步的面向事件的模型。 我们启动了一个HTTP服务器,该服务器必须不断处理传入的请求。 如果一个请求花费太多时间才能完成,那么这将影响所有其他服务器客户端。


通常,请记住:


  • 您永远不能阻止事件循环。

那么,我们如何异步读取文件呢? 这是第二条规则:


  • 当无法避免阻塞操作时,应将其分叉到子进程中,并在主线程中继续异步执行。

因此,在学习了如何做到这一点之后,让我们讨论正确的非阻塞解决方案。


子进程


与异步应用程序中文件系统的所有通信都必须在子进程中执行。 要在ReactPHP应用程序中管理子进程,我们需要安装另一个“子进程”组件。 该组件使您可以访问操作系统的功能,以在子进程中运行任何系统命令。 要安装此组件,请在项目的根目录中打开一个终端,然后运行以下命令:


composer require react/child-process


Windows兼容性


在Windows操作系统中,STDIN,STDOUT和STDERR线程处于阻塞状态,这意味着子进程组件将无法正常工作。 因此,该组件主要设计为仅在nix系统上工作。 如果您尝试在Windows系统上创建Process类的对象,则将引发异常。 但是该组件可以在Linux的Windows子系统(WSL)下工作 如果打算在Windows下使用此组件,则需要安装WSL。


现在,我们可以在子进程中执行任何shell命令。 打开routes.php文件,然后更改/路由的处理程序。 创建一个React\ChildProcess\Process类的对象,并作为命令,将ls传递给它以获取当前目录的内容:


 // 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') ); }, // ... ]; 

然后,我们需要通过调用start()方法来启动该过程。 问题在于start()方法需要一个事件循环对象。 但是在routes.php文件中,我们没有此对象。 我们如何将事件循环从index.php传递到直接路由到请求处理程序的路由? 解决这个问题的方法是“依赖注入”。


依赖注入


因此,我们的一条路线需要一个事件循环才能起作用。 在我们的应用程序中,只有一个组件知道路由的存在Router类。 事实证明,为路线提供事件循环是他的责任。 换句话说,路由器需要一个事件循环,或者它取决于事件循环。 我们如何在代码中明确表达这种依赖性? 如何使即使不传递事件循环也无法创建路由器? 当然,通过Router类的构造函数。 打开Router.php并将构造函数添加到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; } // ... } 

在构造函数内部,将传递的事件循环保存在private $loop属性中。 当我们为类提供它需要在外部工作的对象时,这就是依赖注入。


现在有了这个新的构造函数,我们需要更新路由器的创建。 打开index.php文件,并更正我们创建Router类对象的行:


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

做完了 返回routes.php 。 您可能已经猜到了,在这里我们可以将相同的想法与依赖项注入结合使用 ,并将事件循环作为第二个参数添加到查询处理程序中。 更改第一个回调并添加第二个参数:实现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' ); }, ]; 

接下来,我们需要将事件循环传递给子进程的start()方法。 处理程序从哪里获取事件循环? 它已经存储在路由器的private $loop属性中。 我们只需要在调用处理程序时传递它即可。


__invoke()打开Router类并更新__invoke()方法,将第二个参数添加到请求处理程序调用中:


 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); } 

仅此而已! 这可能足够依赖注入 。 事件周期发生了一段相当大的旅程,对不对? 从index.php文件到Router类,然后再从Router类到callbacks内部的Router routes.php文件。


因此,要确认子进程将执行其非阻塞魔术,让我们用更重的ping 8.8.8.8替换简单的ls 。 重新启动服务器,然后再次尝试在两个不同的窗口中打开两个页面。 首先, http://127.0.0.1:8080/ /upload ,然后/upload 。 尽管ping命令在后台的第一个处理程序中执行,但两个页面都可以快速打开,没有任何延迟。 顺便说一下,这意味着我们可以派遣任何昂贵的操作(例如,处理大文件),而不会阻塞主应用程序。


使用线程绑定子进程和响应


让我们回到我们的应用程序。 因此,我们创建了一个子进程,将其启动,但是我们的浏览器不会以任何方式显示分叉操作的结果。 让我们修复它。


我们如何与子进程进行沟通? 在我们的例子中,我们有一个正在运行的ls ,该ls显示当前目录的内容。 我们如何获得该结论,然后将其发送给答案正文? 简短的答案是:线程。


让我们谈谈流程。 您执行的任何Shell命令都具有三个数据流:STDIN,STDOUT和STDERR。 流式传输到标准输出和输入,以及流式传输错误。 例如,当我们执行ls ,该ls的结果直接发送到STDOUT(在终端屏幕上)。 因此,如果我们需要获取流程的输出,则需要访问输出流。 这就像炮击梨一样容易。 在创建响应对象时,用$childProcess->stdout替换file_get_contents()调用:


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

所有子进程都具有与stdio流相关的三个属性: stdoutstdinstderr 。 在我们的例子中,我们想在网页上显示流程的输出。 我们传递一个流作为第三个参数,而不是Response类的构造函数中的字符串。 Response类足够聪明,可以意识到它已接收到流并进行了相应处理。


因此,像往常一样,我们重新启动服务器,然后看看我们做了什么。 让我们在浏览器中打开页面http://127.0.0.1:8080/ :您应该看到项目根文件夹的文件列表。



最后一步是用更有用的ls替换ls 。 我们通过使用file_get_contents()函数呈现pages/index.html文件来开始本章。 现在,我们可以绝对异步地读取该文件,而不必担心它将阻塞我们的应用程序。 用cat pages/index.html替换ls


如果您不熟悉cat ,那么它将用于连接和输出文件。 通常,此命令用于读取文件并将其内容输出到标准输出。 cat pages/index.html命令读取cat pages/index.html文件并将其内容打印到STDOUT。 并且我们已经将stdout发送为响应主体。 这是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' ); }, ]; 

结果,只需要替换所有对file_get_contents()函数的调用就可以使用所有这些代码。 依赖注入,传递事件循环对象,添加子进程以及使用线程。 所有这些只是替换一个函数调用。 值得吗? 答:是的,值得。 当某些东西可以阻止事件循环,而文件系统可以阻止时,请确保它最终将在最不适当的时刻被阻止。


每次我们需要访问文件系统时创建一个子进程可能看起来会产生额外的开销,这会影响应用程序的速度和性能。 不幸的是,在PHP中,没有其他方法可以异步处理文件系统。 所有异步PHP库都使用子进程(或抽象它们的扩展)。


Habra的读者可以通过此链接以折扣价购买整本书。


我们提醒您,我们一直在寻找优秀的开发人员 ! 来吧,我们玩得开心!

Source: https://habr.com/ru/post/zh-CN416003/


All Articles