玩转 ReactPHP
我最近看到了一个Twitter墙的实现,用于node.js在Twitter上运行搜索并将结果发布到网页上。我一直想使用ReactPHP创建一些东西,所以我认为这是一个尝试的好机会。ReactPHP,如果您还没有听说过,它是一个事件驱动的、非阻塞的I/O,本质上是node.js的PHP等价物。主要区别在于ReactPHP是用纯PHP编写的,没有额外的组件,而是node.js不同程序、接口和语言的集合。作为第一次尝试,我想创建一些简单的东西,所以它需要使用简单的JavaScript从ReactPHP服务器加载给定主题标签的最新推文。我必须警告说,这是ReactPHP的一个简单实现,但它显示了如何开始的基础知识。
构建前端
首先要做的是创建一个HTML页面来显示推文。创建一个简单的ReactPHP服务器时,通常会分配一个端口,在这种情况下,我要做的第一件事就是弄清楚如何在JavaScript中向某个端口号(而不是端口80)发出请求。通常这会违反JavaScript强制执行的同源策略,但我们可以使用一种称为JSONP的技术来解决这个问题。这涉及一个跨域请求,该请求返回包装在函数回调中的JSON字符串。这在jQuery中使用该getJSON()函数是可能的。
这个函数的第一个参数是我们ReactPHP服务器的完整URL(包括端口),它必须包含一个类似于'jsoncallback'的参数。这是服务器端用于发送回正确数据格式的内容。未使用第二个参数,但它允许您传递其他参数。第三个参数是对返回的数据做一些事情的回调函数。我们将返回一个带有'content'属性的JSON对象,该对象将包含推文。
function loadtweets() { $.getJSON("http://www.tweetstream.local:4000?jsoncallback=?", {}, function(data) { $('#stream').html(data.content); } ); }
当我们运行此代码时,它将向以下URL结构发送请求。
http://www.tweetstream.local:4000?jsoncallback=jQuery18205617587673477829_1353020048954&_=1353020058958
我们在服务器端所做的是返回一个字符串,该字符串将一些JSON输出包装在一个与jsoncallback参数同名的函数中。函数被扩展,然后它包含的JSON可用于返回函数。我们在这里唯一要做的就是将整个内容字符串传递到ID为“stream”的元素中。
jQuery18205617587673477829_1353020048954('content':'text')
这个函数基本上包含在对的调用中setInterval(),它用于每5秒轮询一次ReactPHP服务器以查看是否有任何推文。我们可以扩展它以尝试将推文保留在屏幕上,并将新推文附加到顶部,但这将适用于本演示的目的。
作曲家
有了它,我们现在可以构建我们的ReactPHP组件。这是使用名为Composer的工具完成的,该工具是PHP内置的依赖项管理器。我不会过多地介绍Composer,但要获取副本,只需在要工作的目录中运行以下命令即可。
curl-shttps://getcomposer.org/installer|php
这将下载一个名为composer.phar的文件,该文件用于下载各种包。我们需要做的就是告诉Composer我们需要下载哪些包。这是使用一个名为composer.jsonwherewelist(以JSON格式)我们想要用于该项目的组件的文件来完成的。显然我们需要包含ReactPHP,但我也包含了ZendFramework1,以便我可以使用Zend_Service_Twitter_Search类。这是composer.json这个项目的文件。我可以使用不同的Twitter类,但我熟悉Zend框架的实现,所以为了方便我选择了那个。
{ "require": { "react/http": "0.2.*", "zendframework/zendframework1": "dev-release-1.12" } }
要让Composer做一些事情,只需像这样调用带有标志安装的作曲家文件。
composerinstall
这将下载ReactPHP和ZendFramework1以及一些其他依赖项。您现在将拥有一个名为vendor的目录,其中包含下载的软件包和一个autoloader.php文件。要使用这些目录中的任何内容,只需包含autoload.php文件并开始使用您需要的类。
autoload.php composer evenement guzzle react symfony zendframework
反应PHP
我们终于准备好开始使用ReactPHP。第一步(除了包含autoload.php文件)是实例化几个对象。这些是事件循环、套接字和http对象。ReactPHP通过创建一个内部事件循环来工作,然后其他系统可以将其插入。然后使用该循环创建一个套接字服务器,然后该服务器又用于创建一个HTTP服务器。HTTP服务器本质上是一个围绕套接字对象的包装器,带有一些额外的东西来处理标头和其他与HTTP相关的事情。
然后,HTTP对象被设置为在它使用该on()方法接收和传入请求时触发一个事件。在这个方法中,我们设置了一个闭包来接收传入的请求并发送相应的输出。这个闭包是我们找到jsoncallback包装器名称并返回一个包含我们加载的所有推文的json字符串的地方。
最后,我们设置socketserver监听4000端口并运行。
on('request', function ($request, $response) use() { $query = $request->getQuery(); if (!isset($query['jsoncallback'])) { //没有jsoncallback参数通过,所以我们退出。 $response->writeHead(200, array('Content-Type' => 'text/plain; charset=utf-8')); $response->end('finish' . PHP_EOL); return; } //设置正确的标题 $response->writeHead(200, array( 'Content-Type' => 'application/x-javascript; charset=utf-8', 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', 'Last-Modified' => gmdate("D, d M Y H:i:s") . ' GMT', 'Cache-Control' => 'no-store, no-cache, must-revalidate', 'Cache-Control' => 'post-check=0, pre-check=0', 'Pragma' => 'no-cache', )); //加载推文 $searchResults = loadtweets(); //生成输出 $output = ''; if ($searchResults !== FALSE) { foreach ($searchResults as $result) { $text = clean_tweet($result['text']); $output .= ''; $output .= ''; } } //JSON编码并将输出包装在jsoncallback参数中 $output = $query['jsoncallback'] . '(' . json_encode(array('content' => $output)) . ')' . PHP_EOL; //结束响应 $response->end($output); }); //侦听套接字4000 $socket->listen(4000); //运行服务器 $loop->run();' . $result['from_user_name'] . ''; $output .= ''; $output .= ''; $output .= ''; $output .= '' . $text . ' on ' . $result['created_at'] . '
'; $output .= '
clean_tweet()上面的函数取自五分钟参数的RemySharp的JavaScriptTwitter解析函数的PHP实现。这些函数将向用户名和主题标签等内容添加HTML链接,这意味着Twitter流本身更加用户友好。
当然,我们错过了实际获取推文的重要组成部分。我们使用Zend_Service_Twitter_Search类来做到这一点,它是Zend框架的一部分。由于我们之前对Composer的操作,我们已经可以访问这个类,因此我们不需要包含任何其他内容。这是完整的功能,它将使用标签#drupal搜索英文推文。
function loadtweets() { //获取推特搜索对象 $twitterSearch = new Zend_Service_Twitter_Search('json'); //为搜索设置正确的语言 $searchQuery = array( 'lang' => 'en' ); //运行搜索 $searchResults = $twitterSearch->search('#drupal', $searchQuery); //返回结果 return $searchResults; }
将此脚本保存到文件后,您可以像运行任何其他PHP脚本一样运行它。
phpTweetStream.php
您可以通过以下命令通过curl运行该脚本来检查该脚本是否正在运行。如果没有jsoncallback参数,脚本将只返回“已完成”并退出,但这是确保正在侦听正确端口的好方法。
curlwww.tweetstream.local:4000
您在脚本中回显的任何内容都将输出到终端而不是等待连接的用户。看到事情按预期进行是一个好主意。要再次停止程序,只需按Ctrl+c。
我们在这里所做的只是创建一个基于Web的脚本,该脚本将拉取最新的推文并将它们返回到浏览器。ReactPHP的强大之处在于我们可以将搜索推文与请求推文列表的用户分开。我们可以通过使用addPeriodicTimer()事件循环对象的方法创建一个每10秒执行一次的计时器来实现这一点。有了这个,我们可以以受控的方式下拉最新的推文,而无需依赖用户交互来运行脚本。
$loop->addPeriodicTimer(10, function() use() { gettweets(); });
该gettweets()函数运行twitter搜索并将每个结果存储在一个文件中。
function gettweets() { //获取推特搜索对象 $twitterSearch = new Zend_Service_Twitter_Search('json'); //为搜索设置正确的语言 $searchQuery = array( 'lang' => 'en' ); //运行搜索 $searchResults = $twitterSearch->search('#drupal', $searchQuery); //存储结果 if ($searchResults !== FALSE && count($searchResults['results']) > 0) { foreach ($searchResults['results'] as $result) { if (!file_exists($cache_directory . $result['id'])) { $fp = fopen($cache_directory . $result['id'], 'w+'); fwrite($fp, serialize($result)); fclose($fp); } } } }
然后我们可以更改loadtweets()函数以从保存的文件中提取数据,并在需要时将其返回给请求事件。当我们使用推文的id作为文件名时,我们可以对它们进行反向排序,以获得一个排序的推文列表,最后一个排在前面。这意味着搜索推文和向用户显示这些推文现在是两个独立的事件。
function loadtweets() { //获取缓存目录中的文件列表 $files = array(); foreach (new DirectoryIterator('cache') as $fileInfo) { if ($fileInfo->isDot()) { continue; } $files[] = $fileInfo->getFilename(); } //对我们找到的文件进行反向排序 rsort($files); //解压每个文件 foreach ($files as $key => $file) { $files[$key] = unserialize(file_get_contents('cache/' . $file)); } //返回结果 return $files; }
未来的计划
下一步是创建一个套接字服务器并使用HTMLWeb套接字接口或类似socket.io.js的接口连接到它。这将不需要时间间隔来提取最新结果。当发现新项目时,它们会被直接推送到浏览器,然后只是推送到列表的顶部,而不是重新加载整个推文流。
在玩这些东西时,我发现有趣的一件事是它所需的范式转变。我在Web服务器环境中使用PHP已经很长时间了,以至于我一直在考虑处于无状态状态的应用程序。一个主要的启示是,我在全局范围内创建的任何变量或函数都在该范围内持续存在,直到程序停止。这意味着在发出下一个请求时,我在一个请求中设置的任何内容仍然存在。
我可以看到ReactPHP的很多潜力,即使只花了几分钟搞乱了简单的套接字和端口。我在这里完成的工作是在几个小时内完成的,尽管我花了一段时间试图弄清楚如何在JavaScript中联系ReactPHP服务器。查看ReactPHP网站以获取更多信息和ReactPHP的更多(更好)实现。我目前正在熟悉这个包,将来肯定会再次访问它。