今天在 workerman 的官方群看到有人问 webman 中的一个问题。大意就是 webman 在 windows 中修改 controller 代码需要重启才生效,而修改 view 代码则不需要重启也能生效(注:webman 使用了作者自己写的 FileMonitor 组件。在 Linux 系统中,可以通过监控指定目录,定时更新文件,然后通过 kill 给子进程发送信号达到重新加载的目的;而 windows 系统并没有相应的扩展支持)。这是为什么呢?
感觉这个问题有点意思,但自己又从来没有玩过 webman,于是运行之后稍微看了下 controller 调用 view 的逻辑,很快顺藤摸瓜找到了其中的调用路径app/controller/Index:view
,suppport/helpers:view
,support/view/Raw|ThinkPHP...:render
,最后将问题关键锁定在include $view_path
上,如下:
<?php
namespace support\view;
use Webman\View;
/**
* Class Raw
* @package support\view
*/
class Raw implements View
{
public static function render($template, $vars, $app = null)
{
static $view_suffix;
$view_suffix = $view_suffix ? : config('view.view_suffix', 'html');
$app_name = $app == null ? request()->app : $app;
if ($app_name === '') {
$view_path = app_path() . "/view/$template.$view_suffix";
} else {
$view_path = app_path() . "/$app_name/view/$template.$view_suffix";
}
\extract($vars);
\ob_start();
// Try to include php file.
try {
include $view_path;
} catch (\Throwable $e) {
echo $e;
}
return \ob_get_clean();
}
}
其实 include、require、include_once、require_once 这几个是面试中的常客了,而且日常使用非常频繁,但我猜很多人对其认识应该也仅仅限于引入发生错误的等级不一样
、有 _once 的在多次引入时只引入一次
之类的。
再回到 webman 中,上面提到问题的关键在于include $view_path
。我们知道 PHP 想要调用其他文件的类、函数时,都是需要引入文件的,因此,webman 的控制器肯定也会用到的,那么控制器的引入和视图的引入区别在哪呢?webman 的控制器通过 require_once 引入。如下(由于并未使用过 webman,不确定是不是在这里,看起来像):
<?php
namespace Webman;
/**
* Class App
* @package Webman
*/
class App
{
/**
* @return void
*/
public static function loadController($path)
{
if (\strpos($path, 'phar://') === false) {
foreach (\glob($path . '/controller/*.php') as $file) {
require_once $file;
}
foreach (\glob($path . '/*/controller/*.php') as $file) {
require_once $file;
}
} else {
$dir_iterator = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($dir_iterator);
foreach ($iterator as $file) {
if (is_dir($file)) {
continue;
}
$fileinfo = new \SplFileInfo($file);
$ext = $fileinfo->getExtension();
if (\strpos($file, '/controller/') !== false && $ext === 'php') {
require_once $file;
}
}
}
}
}
一般情况下,大部分人都使用 php-fpm 模式,而几种引入文件的方式在该模式下其实区别并没有那么大,随便选一种问题都不大。但是在 cli 模式下常驻内存时,这几种引入文件的方式区别可就大了。可以使用以下代码来体验一下:
<?php
// include_require.php
$ld = stream_socket_server('tcp://0.0.0.0:10003', $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN);
if (!$ld) {
printf("socket create fail\n");
exit(-1);
}
while (true) {
echo time(), "\n";
$conn = stream_socket_accept($ld);
if ($conn) {
extract(['name' => 'william']);
ob_start();
// TODO: 分别注释以下两行代码,或者将 require 改成 include
require "include_require.html";
// require_once "include_require.html";
$body = ob_get_clean();
$header = <<<HEADER
HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n
HEADER;
$sendRes = stream_socket_sendto($conn, $header);
$sendRes = stream_socket_sendto($conn, $body);
if (!$sendRes) {
printf("send fail\n");
}
fclose($conn);
} else {
printf("accept fail\n");
}
echo time(), "\n";
}
fclose($ld);
<!-- include_require.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
echo $name;
?>
</body>
</html>
关注点放在以上代码的TODO
处,分别用四种不同的方式引入文件,并在运行过程中修改 html 文件对比一下。可以用strace php include_require.php
命令对比几种引入方式的实质区别。以下是命令在 include 或 require 时输出的片段:
lstat("/usr/share/php/notes/src/github.com/lwlwilliam/practice/php/common/./include_require.html", {st_mode=S_IFREG|0777, st_size=158, ...}) = 0
lstat("/usr/share/php/notes/src/github.com/lwlwilliam/practice/php/common/include_require.html", {st_mode=S_IFREG|0777, st_size=158, ...}) = 0
openat(AT_FDCWD, "/usr/share/php/notes/src/github.com/lwlwilliam/practice/php/common/include_require.html", O_RDONLY) = 5
fstat(5, {st_mode=S_IFREG|0777, st_size=158, ...}) = 0
read(5, "<!DOCTYPE html>\r\n<html lang=\"en\""..., 158) = 158
close(5) = 0
经过比较,发现 include 和 require 的系统调用完全一致,而 include_once 和 require_once 则在第一次引入时的系统调用亦一致,区别就在于第一次之后的引入则完全被忽略。可以看出,引入文件的本质就是 open,read,close 这几个常见的读操作。
到这里,应该比较清晰地回答了文章开头的问题,由于 webman 框架在调用 view 时使用了 include,相当于每次都重新读取文件,因此,即使在 cli 模式常驻内存时,仍然能即时更新 view 内容。