第三选择

刻意练习,日渐精进

0%

从零学习Laravel框架-Json响应

不知道平时小伙伴在用 Laravel 返回响应时,有没有想过:Laravel 到底是怎么把响应返回给到客户端的呢?简单的一句 response()->json() 的背后,经历了什么样曲折坎坷的故事?今天走进 Laravel,让我们来一探究竟。

response() 返回的是什么?

首先从最表层开始,我们在控制器中要输出 Json 响应时,只需要一句 response()->json() 就可以输出 Json 响应了,那我们先来看看这个 response() 输出的是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Return a new response from the application.
* 从容器中返回一个响应类
*
* @param \Illuminate\View\View|string|array|null $content
* @param int $status
* @param array $headers
* @return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
*/
function response($content = '', $status = 200, array $headers = [])
{
$factory = app(ResponseFactory::class);

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

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

那么 app(ResponseFactory::class) 会得到什么呢。我们可以根据
Illuminate\Routing\RoutingServiceProvider 里的代码得到结果。(关于 app() 方法我们在另外一篇文章(待补充链接)中有讲到,这里就不重复了。不了解的童鞋可以去补充一下相关的知识。

下面我们来看看 RoutingServiceProvider 里的相关代码:

1
2
3
4
5
6
7
8
9
10
11
/**
* Register the response factory implementation.
*
* @return void
*/
protected function registerResponseFactory()
{
$this->app->singleton(ResponseFactoryContract::class, function ($app) {
return new ResponseFactory($app[ViewFactoryContract::class], $app['redirect']);
});
}

使用 app(ResponseFactory::class) 最终会调用这个匿名函数,实例化
Illuminate\Routing\ResponseFactory 对象,并且,返回Json响应时,response() 是不带参数的,于是就直接返回来 ResponseFatory 这个对象。

json() 方法做了什么?

首先看看 json() 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Create a new JSON response instance.
*
* @param mixed $data
* @param int $status
* @param array $headers
* @param int $options
* @return \Illuminate\Http\JsonResponse
*/
public function json($data = [], $status = 200, array $headers = [], $options = 0)
{
return new JsonResponse($data, $status, $headers, $options);
}

可以看到他首先是 new 了一个 JsonResponse 实例,让我们来康康这个实例都做些什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use Symfony\Component\HttpFoundation\JsonResponse as BaseJsonResponse;

class JsonResponse extends BaseJsonResponse
{
···
···
···
/**
* Constructor.
*
* @param mixed $data
* @param int $status
* @param array $headers
* @param int $options
* @return void
*/
public function __construct($data = null, $status = 200, $headers = [], $options = 0)
{
// 设置 encode 选项的参数
$this->encodingOptions = $options;

parent::__construct($data, $status, $headers);
}
···
···
···
}

这一步没有什么太特别的,我们继续往下看, JsonResponse 继承了
Symfony\Component\HttpFoundation\JsonResponse,那就贴上代码康康:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class JsonResponse extends Response
{
/**
* @param mixed $data The response data
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $json If the data is already a JSON string
*/
public function __construct($data = null, int $status = 200, array $headers = [], bool $json = false)
{
parent::__construct('', $status, $headers);

// 如果没有返回数据,则 new 一个
if (null === $data) {
$data = new \ArrayObject();
}

$json ? $this->setJson($data) : $this->setData($data);
}
}

他首先实现了父类的构建函数,那我们把父类,也就是
Symfony\Component\HttpFoundation\Response 的构建函数也看下:

1
2
3
4
5
6
7
8
9
10
class Response
{
public function __construct($content = '', int $status = 200, array $headers = [])
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
}

这几行代码也比较好理解,主要是设置 Header 头、Content、Status Code 和 协议版本,默认 HTTP 协议是 1.0 的。
继续往下看,当 $data 为 null 时,需要新建一个数组对象,但我们一般调用时 $data 是不为 null的。
$json 为 false,会调用 setData()的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Sets the data to be sent as JSON.
*
* @param mixed $data
*
* @return $this
*
* @throws \InvalidArgumentException
*/
public function setData($data = [])
{
$this->original = $data;

if ($data instanceof Jsonable) {
$this->data = $data->toJson($this->encodingOptions);
} elseif ($data instanceof JsonSerializable) {
$this->data = json_encode($data->jsonSerialize(), $this->encodingOptions);
} elseif ($data instanceof Arrayable) {
$this->data = json_encode($data->toArray(), $this->encodingOptions);
} else {
$this->data = json_encode($data, $this->encodingOptions);
}

// 验证 json 编码时有无发生错误
if (! $this->hasValidJson(json_last_error())) {
throw new InvalidArgumentException(json_last_error_msg());
}

return $this->update();
}

这个方法里会根据 $data 类型的不同,用不同的方法将 $data 转成 json 格式的数据,随后进行编码过程的错误校验。没有错误的话,会执行下一步操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Updates the content and headers according to the JSON data and callback.
* 根据 json 数据和 callback 内容 配置 content 和 headers
*
* @return $this
*/
protected function update()
{
if (null !== $this->callback) {
// Not using application/javascript for compatibility reasons with older browsers.
$this->headers->set('Content-Type', 'text/javascript');

return $this->setContent(sprintf('/**/%s(%s);', $this->callback, $this->data));
}

// Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback)
// in order to not overwrite a custom definition.
// 如果没有配置 Content-Type 或者 Content-Type 为 text/javascript 时,更新为 application/json
if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) {
$this->headers->set('Content-Type', 'application/json');
}

return $this->setContent($this->data);
}

在 json 响应里是不涉及 callback 调用的,所以这里的 $this->callback 的逻辑是不执行的。往下则是执行 setContent 的逻辑,将 $this->content 的内容更新为 $this->data

至此,response()->json() 所经历的故事我们就看完了,我们最终返回的,就是一个实例化的 JsonResponse 对象。

得到对象后要做什么?

控制器方法完成后,得到了一个 JsonResponse 对象后,会触发 Illuminate\Routing\Router 的 prepareResponse 方法。就是准备返回响应了!!!是不是有点小激动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* Create a response instance from the given value.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param mixed $response
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
}

/**
* Static version of prepareResponse.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param mixed $response
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
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);
}

// 304 响应需要做特殊处理
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}

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

在这个环节,主要是判断响应的类型,如果不是一个已经转为响应的类型,则先生成一个响应类,而我们现在得到是一个 JsonResponse类,也就不需要进行这个处理了。

如果我们返回的是 304 的响应,那么还需要调用 setNotModified() 方法进行处理,就是将一些不需要的头部移除,这块大家自己看看代码就知道了,为了方便大家,我也一起贴上来吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Symfony\Component\HttpFoundation\Response
*
* 修改响应以符合 304 状态码规则的定义
*
* @return $this
*
* @see https://tools.ietf.org/html/rfc2616#section-10.3.5
*/
public function setNotModified()
{
$this->setStatusCode(304);
$this->setContent(null);

// remove headers that MUST NOT be included with 304 Not Modified responses
foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) {
$this->headers->remove($header);
}

return $this;
}

到这一步,我们需要处理 response 的 headers 和 body,先贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* Prepares the Response before it is sent to the client.
*
* This method tweaks the Response to ensure that it is
* compliant with RFC 2616. Most of the changes are based on
* the Request that is "associated" with this Response.
*
* @return $this
*/
public function prepare(Request $request)
{
$headers = $this->headers;

// 判断是否 1开头的状态码 或者 204 304 状态码
if ($this->isInformational() || $this->isEmpty()) {
$this->setContent(null);
$headers->remove('Content-Type');
$headers->remove('Content-Length');
} else {
// 如果没有设置 Content-Type 并且能获取请求的的类型,则设置为请求的类型
if (!$headers->has('Content-Type')) {
$format = $request->getRequestFormat();
if (null !== $format && $mimeType = $request->getMimeType($format)) {
$headers->set('Content-Type', $mimeType);
}
}

// 如果还是没有 Content-Type,则设置成 text-html; 并定义字符
$charset = $this->charset ?: 'UTF-8';
if (!$headers->has('Content-Type')) {
$headers->set('Content-Type', 'text/html; charset='.$charset);
} elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
// 补充 charset 的配置
$headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
}

// Fix Content-Length
if ($headers->has('Transfer-Encoding')) {
$headers->remove('Content-Length');
}

if ($request->isMethod('HEAD')) {
// cf. RFC2616 14.13
$length = $headers->get('Content-Length');
$this->setContent(null);
if ($length) {
$headers->set('Content-Length', $length);
}
}
}

// Response 默认的响应是1.0,如果请求的协议不是 1.0,需要改成 1.1 的版本
if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}

// Check if we need to send extra expire info headers
if ('1.0' == $this->getProtocolVersion() && false !== strpos($headers->get('Cache-Control'), 'no-cache')) {
$headers->set('pragma', 'no-cache');
$headers->set('expires', -1);
}

// Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
// 用户使用 IE9 以下的浏览器时,要检查是否为 SSL 加密下载移除 Cache-Control
$this->ensureIEOverSSLCompatibility($request);

// 检查是否信任的代理并且 X-Forwarded-Proto 的协议为 https、on、ssl或1
if ($request->isSecure()) {
foreach ($headers->getCookies() as $cookie) {
$cookie->setSecureDefault(true);
}
}

return $this;
}

每个步骤的作用我在代码上做了注释,想要了解具体实现的童鞋,可以自己了解相关的方法。

响应是怎么输出的?

处理完成,准备工作就结束了,让我们来到输出的最后一步: index.php 中的 $response->send()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Symfony\HttpFoundation\Response
*
* 输出 HTTP 头和内容
*
* @return $this
*/
public function send()
{
$this->sendHeaders();
$this->sendContent();

if (\function_exists('fastcgi_finish_request')) {
// 此函数冲刷(flush)所有响应的数据给客户端并结束请求。这使得客户端结束连接后,需要大量时间运行的任务能够继续运行。
fastcgi_finish_request();
} elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}

return $this;
}

我们先来看看怎么输出 headers 的信息的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public function sendHeaders()
{
// 检查 HTTP 头是否已发送
if (headers_sent()) {
return $this;
}

// 输出 headers
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
$replace = 0 === strcasecmp($name, 'Content-Type'); // 除了 Content-Type 其它头信息都可以并存
foreach ($values as $value) {
header($name.': '.$value, $replace, $this->statusCode);
}
}

// 输出 cookies
foreach ($this->headers->getCookies() as $cookie) {
header('Set-Cookie: '.$cookie, false, $this->statusCode);
}

// 输出 HTTP 状态码
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);

return $this;
}

相关阅读:

然后输出 content 内容了:

1
2
3
4
5
6
public function sendContent()
{
echo $this->content;

return $this;
}

最后还要执行一个 fastcgi_finish_request() 函数,将缓冲区的内容输出并关闭缓冲,如果 fastcgi_finish_request() 不存在,则调用框架的 closeOutputBuffer(), 以达到相同的目的。

至此,一个 json 响应的故事就结束了,谢谢大家的阅读,我们下期再会~