Laravel 底层分析:生命周期——响应(第四部分)

Laravel

这个指南介绍了Laravel框架5.6版的源代码.

小结

在 第一部分, 我们学习了在 index.php 加载完成之后立马发生了什么, 并且这些事件是在Laravel 框架的核心服务 (core services) 加载之前发生的.
在 第二部分, 我们学习了框架是如何加载它的结构与配置的 (configuration), 如何设置好错误处理, 如何注册所有的 service providers, 如何解析所有的 facades.
在 第三部分, 我们学习了 Laravel 如何处理你你发出的 url 指令到具体的 route 上, 如何加载 controller 来处理具体的某一个 route.
现在, 让我们来学习 controller 是如何自动生成适用的回应 (response) 然后显示在浏览器上.

响应前的准备

在第三部分, 我们看到 toResponse 方法转变了所有你传过来的数据为一个响应对象 (Response object). 我们还发现变量 $response 是这个控制器的直接输出

public static function toResponse($request, $response)
{
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);
    }

    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        $response = new JsonResponse($response, 201);
    } elseif (! $response instanceof SymfonyResponse && ($response instanceof Arrayable || $response instanceof Jsonable || $response instanceof ArrayObject || $response instanceof JsonSerializable || is_array($response))) {
        $response = new JsonResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);
    }

    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

    return $response->prepare($request);
}

我们发现这里就是Laravel 神奇的转变 数组, 字符串和Eloquent 模型到 JSON的地方 (因为它们在这实施了可序列化的接口, 里面包含了toJson方法).
如果我们观察一下 Illuminate\Http 命名空间里的 Response class,
我们可以发现这个 class 继承了Symfony 的HttpFoundation 组件, 并且名字相同.
并且我们发现这个class 并没有很多代码. 因为大多数的配置是Symfony在后台完成的. 在我们查看响应方法 (preparation method) 之前, 我们要先看看他的构造方法里都做了什么在此查看 .

新的实例

我们立即就看到了从空的 $headers 数组创建了一个 header 包,因为我们在实例化( new Response($response) )的过程中没有发送初始头。

public function __construct($content = '', int $status = 200, array $headers = array())
{
    $this->headers = new ResponseHeaderBag($headers);
    $this->setContent($content);
    $this->setStatusCode($status);
    $this->setProtocolVersion('1.0');
}

如果你在储存了头的内容之后检查 Response 对象的内容,你会发现它是一个空的对象 -- 仅仅设置了日期和缓存控制头。接下来,我们来设置一些初始属性,比如 HTTP 版本、响应内容、状态码和最后前往的准备。如果在设置初始属性之后我们使用 dd 来查看这个类,我们将看到下面的内容:(请记住,这里加载的 welcome 视图是刚刚安装好的 Laravel ):

Screen-Shot-2018-05-26-at-11.17.09

我们当前应该注意几件事儿:original 属性是一个视图对象, statusText 属性是根据HTTP状态码来设置相应的文本内容(404 - Not Found,200 - OK等), content 包含了将要呈现给客户端的文字输出(HTML就是一个例子)。我有点好奇这个视图对象...有趣的是,setContent 方法实际上是从 Laravel 的 Response类中调用的。我们把注释删除之后,会发现这个方法非常简单干净。

public function setContent($content)
{
    $this->original = $content;

    if ($this->shouldBeJson($content)) {
        $this->header('Content-Type', 'application/json');

        $content = $this->morphToJson($content);
    } elseif ($content instanceof Renderable) {
        $content = $content->render();
    }

    parent::setContent($content);

    return $this;
}

首选我们要做的是将初始化内容(对象、视图等等)保存到 $content 属性中,以便我们之后使用。接下来,我们需要修改 $content 变量。应该有一个作为JSON(字符串、模型、集合、JSON 相应等)返回的对象?只需要将 header 头中的 Content-Type 设置为 application/json 并且将输出 编码 为JSON。返回视图?或者任意实现 Renderable 接口的实现?可以在那些对象上调用 render 方法,并且调用 setContent 方法来修改它们的父级内容。在我们的例子中, render 方法使用 Blade 编译视图并创建原始HTML。此刻,我们可以打印 $content ,它将展示最终的HTML内容,但我们并不是在这里完成的。

头的准备

没错,现在我们在 Router 中创建了一个 Response 类的新实例,我们已经准备好调用 prepare 方法了。 我们可以看到 这个方法中包含了很多代码,所以我不会粘贴在这里,以免破坏帖子的排版结构。首先,我们先要搞清楚响应是有信息的还是空的。这里我们要知道,如果响应状态码是 100200 之间,则说明响应是有信息的 ,如果是空的 ,那么响应状态吗应该是 204304。如果他是有信息的或空的,请删除所有的内容和与内容有关的头。否则需要根据请求来设置内容类型和内容长度,同时也要设置正确的字符集。注意,默认的HTTP版本是 v1.1。 最后,检查 是否应该为了IE的SSL加密下载删除 Cache-Control。

JSON 和重定向响应

JSON 和重定向响应只是一些稍微改动的 Response 对象。JSON 响应由 Laravel 中的 JsonResponse 来进行处理。而这个类继承了 Symfony 类并进行了一些 JSON 验证,编码,设置 JSON content-type 响应头部等内容。重定向响应做了它应当做的事情,同样也是继承了 Symfony 类。这个类做的唯一一件事情就是设置 Location 头部为目标 URL。

重定向

重定向 是一个比较特殊的类,它包含了一些重定向响应背后的常用函数:重定向后退,重定向主页,重定向到指定路由等等。所以当我们调用重定向的 facade/helper 时,实际上是启动了 Redirector 对象。他还使用 URLGenerator 来生成指定路由的URL和来自header/session 先前的URL等等。所以就如 redirect()->back() 实际上调用了这段代码:

public function back($status = 302, $headers = [], $fallback = false)
{
    return $this->createRedirect($this->generator->previous($fallback), $status, $headers);
}

如果你看过 createRedirect 方法,你就会知道这个方法事实上只是创建了一个 RedirectResponse 类的新实例,并且在 Response 中设置了 session 和请求。

响应助手

同样值得注意的是,如果您调用 Laravel 的惊人的 response() 助手,那么 工厂类 ResponseFactory 实际上就是从容器中解析出来的。这是一个类,它只是响应对象的包装器,并提供用于创建公共响应类型(JSON、下载、重定向等)的委托:

// Illuminate/Foundation/helpers.php
function response($content = '', $status = 200, array $headers = [])
{
    $factory = app(ResponseFactory::class);

    if (func_num_args() === 0) {
        return $factory;
    }

    return $factory->make($content, $status, $headers);
}

完成生命周期

我们现在已经完成了 Laravel 应用生命周期的每一步,并且通过栈回到了那个大名鼎鼎的 index.php 文件。这里唯一要做的就是向客户端展现(发送)响应并调用任意 terminable 中间件。

$response->send();

$kernel->terminate($request, $response);

发送响应

我们准备好了头部信息, 编译好了 view 至 HTML 格式, 完成了所有控制器和模型里的商业逻辑, 现在唯一剩下的就是发送头部信息然后展示内容了.
这就是 Symfony 的 Response class 里 send 方法所做的.

public function send()
{
    $this->sendHeaders();
    $this->sendContent();

    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
        static::closeOutputBuffers(0, true);
    }

    return $this;
}

这个方法还为使用  PHP-FPM 服务器的用户多做了一些数据的检查和规整, 其实这完全没有必要.
如果你是使用 Laravel Valet 和 brew 安装的 PHP, 那么这个方法基本上一定会被使用到. 如果你使用 output control 的话其实还有一个方法会关闭输出缓存 并且输出内容, 但是我们不会再这个指南中提到. 如果你有兴趣的话可以 在此 查看.

发送头部数据

让我们稍微关注一下 sendHeaders 方法,可以看到在这里使用了两个 PHP 的自带函数: headers_sent 和 header

public function sendHeaders()
{
    if (headers_sent()) {
        return $this;
    }

    foreach ($this->headers->allPreserveCase() as $name => $values) {
        foreach ($values as $value) {
            header($name.': '.$value, false, $this->statusCode);
        }
    }

    header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);

    return $this;
}

如果你把你的 CodeIgniter 应用弄的乱七八糟,那你一定不要忘记之前发生过的错误 -- 已经发送了头部数据
好在, headers_sent条件限制了这一点。 如果我们没有发送过头部数据,那我们将会在现在发送它!在遍历 headers 属性时每次循环的过程中调用 PHP 的 header 方法 。同时,我们也需要一个额外的头部调用来设置响应状态码和 HTTP 协议的版本。这一步是在循环外完成的,因为所有其他头都是以 「 Name : Header 」 的形式填入的(例如 header('Content-Type: application/json'); ),虽然头的状态看起来像是 「 HTTP/version code text 」,例如 header("HTTP/1.0 404 Not Found");

发送内容

在发送完响应头部(headers)之后,我们需要将内容发送给用户。还记得我们说过我们可以直接将内容返回获得 HTML 响应么?这就是sendContent方法所做的。它仅仅是调用了echo方法:

public function sendContent()
{
    echo $this->content;

    return $this;
}

此时,响应已经发送并且浏览器已经开始显示内容。或者如果你使用的是API,那么返回的是 JSON 格式的内容。

终止应用程序

在发送响应之后,生命周期中唯一需要完成的步骤就是调用任何额外的可终止中间件。我们看到 index.php 中内核上的 terminate 方法被称为--$kernel->terminate($request, $response);,该方法在应用程序实例上委托 terminate 方法,但在调用任何其他中间件之前不会。

public function terminate($request, $response)
{
    $this->terminateMiddleware($request, $response);

    $this->app->terminate();
}

现在我们已经了解了中间件的工作原理,我们只需快速查看 terminateMiddleware 方法,并了解它在做什么:

protected function terminateMiddleware($request, $response)
{
    $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
        $this->gatherRouteMiddleware($request),
        $this->middleware
    );

    foreach ($middlewares as $middleware) {
        if (! is_string($middleware)) {
            continue;
        }

        list($name) = $this->parseMiddleware($middleware);

        $instance = $this->app->make($name);

        if (method_exists($instance, 'terminate')) {
            $instance->terminate($request, $response);
        }
    }
}

在获得中间件列表之后,循环遍历它们并在内核上调用 terminate 方法——但仅当存在一个方法时才调用。然后,委托给应用程序对象上的 terminate 方法,该方法将激发终止事件并调用可能为该事件创建的任何其他侦听器。最后,完成整个应用程序的生命周期!

总结

总结:我们查看了在您提出请求时加载的第一个文件,它如何注册核心Laravel 组件,它如何创建一个HTTP内核来注册和启动所有服务提供者,从环境变量创建全局请求对象,运行路由器,以及路由器如何执行您在您的公司中提供的代码。最后,将响应内容回复给发出请求的客户机。

再见

这将是 laravel幕后 中的生命周期系列。我希望你喜欢它,并且学到了一些新的东西!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://crnkovic.me/laravel-behind-the-s...

译文地址:https://learnku.com/laravel/t/30061

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!