Laravel 响应类 Response 剖析


和 Laravel 请求对象类 Request 类似,代码底层有一个 Response 类用于表示发送给终端用户的应用响应,其中包含响应头、Cookie、响应内容、以及其它发送给终端用户浏览器用于渲染响应页面的东西。

和 Request 类似,Response 的完整类名是 Illuminate\Http\Response,继承自 Symfony 的 HTTP 响应基类 Symfony\Component\HttpFoundation\Response,该响应类中包含了一系列的属性和方法用于表示和渲染响应,Laravel 的响应类还在其基础上添加了很多额外的有用方法。

Laravel 响应的底层实现

我们首先从 Symfony\Component\HttpFoundation\Response 开始分析,在这个响应基类中,Symfony 实现了对状态码、HTTP协议版本、缓存、Etag、字符编码、响应实体等响应信息的封装,然后在 Illuminate\Http\Response 中,对 JSON 格式响应做了特别的优化,为响应头和响应内容提供了更加便捷的获取方法,而且设置了 cookie 方法用于向响应中添加 Cookie,此外,还支持设置响应宏。

在 Laravel 请求的生命周期中,用户请求经过匹配路由处理后(通过控制器或匿名函数),就会返回相应的响应实例,响应头和响应内容的设置是在具体路由定义中实现,然后再经由路由器类 Illuminate\Routing\RoutertoResponse 方法进行最后的兜底处理(自定义响应类的处理、返回的是数组、字符串、视图等非 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);
}

请求处理完成,响应实例准备停当,就可以将其发送给终端用户了,这段逻辑位于入口文件 public/index.php 中:

$response = $kernel->handle(
    $request = \Illuminate\Http\Request::capture()
);

$response->send();

以上,就是响应类 Response 实例化及返回给用户的底层实现原理,下面我们具体来看一下 Response 类的创建和常见用法。

创建响应实例

通过上面的介绍,我们知道响应类的实例化和初始化是在具体的路由定义中完成的,这些路由定义包括通过匿名函数定义的路由,也包括控制器路由,我们可以在路由定义中处理完请求后,创建相应的响应实例,为其添加响应头、响应内容、Cookie 等属性并返回。

注:如果返回的是字符串、数组、视图的话,会通过上面介绍的路由器中的兜底函数 toResponse 进行转化,最终将其转化为 Response 类,这也是为什么我们在控制器方法或路由定义匿名函数中返回字符串、数组以及视图的时候也能输出响应的原因。需要指出的是,返回的是视图实例的情况下,整个视图实例会作为响应内容返回,在渲染的时候通过相应的视图方法输出视图内容。

我们可以通过 Response 类来创建响应实例:

Route::get('response/test', function() {
    return new \Illuminate\Http\Response('学院君');
});

还可以通过辅助函数 response 创建响应实例:

Route::get('response/test2', function () {
    return response('学院君');
});

当然,前面说了,还可以直接通过返回字符串的方式,底层会帮我们将其转化为 Response 响应:

Route::get('response/test3', function () {
    return '学院君';
});

当然,你还可以根据 Response 类提供的方法设置更多响应信息:

Route::get('response/error', function () {
    return response('出错了!', 500)
        ->header('header_name', 'header_value')
        ->cookie('cookie_name', 'cookie_value');
});

response 辅助函数的第二个字段传入的是响应状态码,默认是200,通过 header 方法可以设置响应头,通过 cookie 方法可以添加 Cookie 到响应:

可以看到对应的响应结果里包含了我们追加的信息。

不同响应的类型

上面演示的都是直接或间接创建 Response 对象实例并返回,在实际项目开发过程中,大部分情况下都不会这么做,对于 Web 浏览器或 H5 站点,我们更多返回的是视图响应,对于 API 请求,我们更多返回的是 JSON 响应,此外,还有一些特殊场景,比如文件下载、重定向等,下面我们就来演示下如何返回这些类型的响应。

视图响应

首先是视图响应,关于视图的具体使用方式学院君在视图入门这篇教程中已经详细介绍过。

通常,我们使用辅助函数 view() 来创建视图实例,我们前面说过,返回 View 实例的时候,底层路由器的 toResponse 方法会将其转化为 Response 实例,并且以 View 实例作为响应内容,在渲染页面的时候调用 View 实例的 render 方法进行最终的渲染。

不过,如果你想要在视图响应中添加 Cookie 等其他响应头信息,则需要通过 response() 辅助函数来实现:

Route::get('response/view', function () {
    return response()
        ->view('welcome')
        ->cookie('site-name', 'Laravel学院');
});

此时,底层也会将 View 实例设置为 Response 实例的响应内容,具体实现代码位于 Illuminate\Routing\ResponseFactory 中,感兴趣的同学可以去看看。

JSON响应

JSON 响应现在越来越常见了,不管是 Ajax 异步请求,还是前后端分离应用,以及微服务接口,现在基本上返回的都是 JSON 格式的响应。

JSON 响应会将返回的数据转化为 JSON 格式数据,以便调用方拿到后进行处理,我们可以调用 PHP 原生方法 json_encode 对数组数据进行处理,再设置响应头 Content-Type 字段值为 application/json,通过这种手动方式来返回 JSON 响应,不过在 Laravel 中,我们可以直接通过 ResponseFactory 实例(response 函数不传参数返回的实例)上的 json 方法来返回 JSON 响应:

Route::get('response/json', function () {
    return response()->json(['name' => '学院君']);
});

对应的输出如下:

还可以通过返回 API 资源类实例或者原生数组的方式返回 JSON 响应,其底层原理就是上面提到的,路由器的兜底函数 toResponse 方法会将返回数据转化为 JsonResponse 实例。

下载文件

有时候,我们需要返回的响应是下载指定文件,比如一些资源下载站,或者运营后台下载报表之类的统计文件,这可以通过调用 ResponseFactory 实例上的 download 方法来实现:

Route::get('response/download', function () {
    return response()->download(storage_path('app/protected/files/sales.csv'));
});

上述代码会下载 storage/app/protected/files/sales.csv 文件到本地。当然,你还可以自定义下载的文件名:

Route::get('response/download', function () {
    return response()->download(storage_path('app/protected/files/sales.csv'), '销售报表.csv');
});

有的时候,我们希望用户可以在浏览器中直接预览文件,而不必下载后再打开,尤其是 PDF 文件或图片,这可以直接通过 file 方法来实现:

Route::get('response/preview', function () {
    return response()->file(storage_path('app/protected/files/sales.pdf'));
});

重定向响应

重定向一般有两种应用场景,一种是之前的路由对应的路径调整了,或者服务已经废弃了,需要将用户重定向到指定的新的路由,一种是用户提交表单验证失败,需要将用户重定向回去。

重定向响应和前面介绍的几种类型的响应不太一样,往往不是通过 response() 辅助函数来创建,而是直接通过另一个辅助函数 redirect,该函数返回的是一个 RedirectResponse 实例:

Route::get('redirect/test', function () {
    return redirect('/');  // 重定向到首页
});

当然,你也可以借助 response 辅助函数实现同样的功能:

return response()->redirectTo('/');

但是通过 redirect 函数更直观、简捷,你还可以通过调用 Redirector 实例上的 route 方法传入路由名称来进行重定向(redirect 函数不带参数调用返回的是 Redirector 实例):

Route::get('redirect/test2', function () {
    return redirect()->route('home');  // 重定向到首页(假设首页路由名称是home)
    //return response()->redirectToRoute('home');  # 与上面等价
});

除此之外,还可以重定向到指定的控制器方法:

Route::get('redirect/test3', function () {
    return redirect()->action('HomeController@index');  // 重定向到首页
    //return response()->redirectToAction('HomeController@index');  # 与上面等价
});

更多使用细节,请参考重定向类 Illuminate\Routing\Redirector 的底层源码。

最后,如果表单请求验证失败,想要返回填写表单页面,可以通过辅助函数 back 来实现,这个时候,我们往往还会追加一个 withInput 方法将填写的表单请求数据一并返回:

Route::get('redirect/back', function () {
    return back()->withInput();
    //return redirect()->back()->withInput();  # 与上面等价
});

好了,有关 Laravel 的 Response 响应类的底层原理和使用我们就介绍到这里,关于控制器方法中返回响应的使用方式路由匿名函数中一模一样,有关一次性 Session 相关的功能介绍我们留到 Session 那里去讲。下一篇教程学院君将给大家剖析 Laravel 中间件底层原理。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: Laravel 请求类 Request 剖析

>> 下一篇: Laravel 中间件底层源码剖析