响应,指的是 HTTP 通信过程中服务端对客户端应答的一种方式。
我们知道,这包括状态行,响应头和响应正文三部分。
举一个例子
test.php1
2
3
4
5
6
7
8
9
10
11
12echo "hello world";
curl发起请求,我们看一下响应的信息
$ curl -i -XGET http://test.dev/test.php
HTTP/1.1 200 OK
Date: Wed, 13 Dec 2017 12:27:52 GMT
Server: Apache/2.4.17 (Win32) OpenSSL/1.0.2d PHP/5.6.15
X-Powered-By: PHP/5.6.15
Content-Length: 11
Content-Type: text/html; charset=UTF-8
hello world
可以很清晰的看出,响应码等于200,多个响应头信息以及响应体内容 hello world。
假如我们想改变状态行的信息,比如200变404, OK变 Not Found,php脚本如何处理呢?
我们知道,只需要在输出hello world之前,调用header函数处理一下就好了。
修改test.php脚本如下1
2
3
4
5<?php
header("HTTP/1.1 404 Not Found");
echo "hello world";
依然是通过上面的curl命令发起请求,看结果1
2
3
4
5
6
7
8
9$ curl -i -XGET http://test.dev/test.php
HTTP/1.1 404 Not Found
Date: Wed, 13 Dec 2017 12:36:05 GMT
Server: Apache/2.4.17 (Win32) OpenSSL/1.0.2d PHP/5.6.15
X-Powered-By: PHP/5.6.15
Content-Length: 11
Content-Type: text/html; charset=UTF-8
hello world
有没有注意到,真的仅仅是状态行有所改变。
当然,还有很多cookie以及其他头信息,都可以通过header函数操作。
说白了,响应,其实就这么回事,并没有太多复杂的东西。
以上,算是我们引出的一点点知识。
下面我们看yii2中,响应是如何实现的。
在yii2中,用response组件来表示响应组件,即 yii\web\Response 。
其结构如下1
2
3
4
5
6
7class Response extends \yii\base\Response
{
}
class Response extends Component
{
}
对比下 Request 组件,Response组件同样也没有过于复杂的父类结构。
我们可以使用 Yii::$app->get(‘response’) 或者 Yii::$app->getResponse() 获取 Response 组件。
下面我们准备接着【应用的生命周期之执行请求】一文,看看yii2是如何构建响应以及发送数据给客户端的。
找到 yii\web\Application::handleRequest 方法,我们从 $this->runAction 方法的调用处开始分析。
$result = $this->runAction($route, $params);
这里调用的是 yii\base\Module::runAction,前面我们详细的分析过这一过程,希望你还没忘得彻底。
以 /index.php?r=site/index 的请求为例,假设是一个正常的web请求且 site/index 操作返回的是一段文本,比如return 123;再或者return 一段html内容。
那么这段文本,便是 yii\base\Module::runAction 的结果值,即 $result = ‘一段文本’(实际上$result 并不局限于一段文本,这里只是拿一段文本为例)。
所以,从下面的代码中可以看出,else 成立。1
2
3
4
5
6
7
8
9
10if ($result instanceof Response) {
return $result;
} else {
$response = $this->getResponse();
if ($result !== null) {
$response->data = $result;
}
return $response;
}
随后,获取 response 组件,为 yii\web\Response::data 赋值并返回 response 实例。这便开始构建响应了。
yii\web\Response::data 属性,是响应的原始数据,注意哦,这个属性的值,并不是最终要发送给客户端的数据。最终发送给客户端的数据是经过格式化处理之后的内容,即 yii\web\Response::content 属性,等会我们再做介绍。
回到 yii\web\Application::handleRequest 方法,看下面这句return。1
return $response;
估计不少同学瞬间懵了,这 return 给谁了呢?
下面这行代码你肯定记得。1
(new yii\web\Application($config))->run();
是不是恍然大悟,我们是在 yii\base\Application::run 方法内调用的yii\web\Application::handleRequest 。
接着 yii\base\Application::run 方法分析。1
2
3
4
5
6
7
8
9$response = $this->handleRequest($this->getRequest());
$this->state = self::STATE_AFTER_REQUEST;
$this->trigger(self::EVENT_AFTER_REQUEST);
$this->state = self::STATE_SENDING_RESPONSE;
$response->send();
$this->state = self::STATE_END
yii\web\Application::handleRequest 方法被调用之后,标记应用的状态为请求结束 yii\base\Application::STATE_AFTER_REQUEST,并触发yii\base\Application::EVENT_AFTER_REQUEST事件。触发该事件之后,才算是对请求的结束画上圆满句号。
下面主要看这两行代码1
2$this->state = self::STATE_SENDING_RESPONSE;
$response->send();
这包括两个步骤
标记应用当前请求状态为 yii\base\Application::STATE_SENDING_RESPONSE,即标记下发送请求前的一个状态
调用 yii\web\Response::send 方法向客户端发送响应信息
下面我们着重分析一下 yii\web\Response,在看 yii\web\Response::send 方法之前,我们有必要了解一下 yii\web\Response::init 初始化 Response 组件时做了哪些工作。1
2
3
4
5
6
7
8
9
10
11
12
13
14public function init()
{
if ($this->version === null) {
if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
$this->version = '1.0';
} else {
$this->version = '1.1';
}
}
if ($this->charset === null) {
$this->charset = Yii::$app->charset;
}
$this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
}
首先是 HTTP 协议版本和响应的字符集charset的确定,接着是转化响应的数据格式集,默认的核心格式被定义在 yii\web\Response::defaultFormatters 方法内,包括 html,xml,json等。
后面我们就需要 yii\web\Response::formatters 属性定义的格式转化映射类处理要发送给客户端的数据。
接着看 yii\web\Response::send 方法。1
2
3
4
5
6
7
8
9
10
11
12
13public function send()
{
if ($this->isSent) {
return;
}
$this->trigger(self::EVENT_BEFORE_SEND);
$this->prepare();
$this->trigger(self::EVENT_AFTER_PREPARE);
$this->sendHeaders();
$this->sendContent();
$this->trigger(self::EVENT_AFTER_SEND);
$this->isSent = true;
}
yii\web\Response::isSent 属性,标记下响应是否已经发送了,如果发送了再调用send就避免重复发送了。
我们注意到yii\web\Response::send 方法内有多达三个事件的触发,别着急,我们一个一个的介绍。
首先是 yii\web\Response::EVENT_BEFORE_SEND 事件的触发,这允许我们在真正发送之前做一些什么。
前几天还有一位同学,大胆留言,事件这东西,用是会用了,但就是搞不懂事件有什么好处。
这里我再啰嗦一下。假设没有事件,也就是说没有官方给我们预留的那么多后门,你说说,万一我们真的想在客户端发送之前做点什么,怎么办?动手改改官方的源码?这肯定不行。
事实上大家都知道,通过composer安装下来的源码我们都不建议改动。
同样,在我们自己写的代码中,也有着相同的道理。这不光光是程序的调用问题,它也是一种解耦的方式,值得体会一番。
说远了,回来看 yii\web\Response::EVENT_BEFORE_SEND 触发,事实上这个事件的触发机制,我们大有用途,卖个关子,等会再说。
我们知道,response响应阶段,大部分会通知客户端,我(服务器)给你(客户端)的数据是什么格式的,数据又是什么。 这便是 yii\web\Response::prepare 方法的实现。
以 index.php?r=site/index 且返回的内容是一段文本为例,yii\web\Response::prepare 方法内便会发起对 yii\web\HtmlResponseFormatter::format 的调用。
yii\web\HtmlResponseFormatter::format 方法做两件事
设置响应头的 Content-Type
将响应的原始数据转化为响应的最终数据转换
当然,还有其他格式的数据转换,比如json,参考 yii\web\JsonResponseFormatter::format 的实现。
回到 yii\web\Response::send方法,yii\web\Response::prepare 方法执行后,将会触发 yii\web\Response::EVENT_AFTER_PREPARE 事件。
一切准备就绪后,php脚本开始着手响应头的构建。
yii\web\Response::sendHeaders 方法中,有两句值得注意的代码,我们说一说。1
2$statusCode = $this->getStatusCode();
header("HTTP/{$this->version} {$statusCode} {$this->statusText}");
在这里,便通过header函数修改响应状态行的信息。
所以,如果我们想在应用程序内,指定响应的状态码,状态描述等信息,最好在调用 yii\web\Response::sendHeaders 方法之前操作。
怎么操作?动动手指改改源码?
跟改源码说拜拜吧,这是不可行的方案!
记得我们刚刚说过 yii\web\Response::send函数内触发的三个事件吗?其中有两个事件是在 yii\web\Response::sendHeaders 方法调用之前触发,所以,我们就可以在触发 yii\web\Response::EVENT_BEFORE_SEND事件或者yii\web\Response::EVENT_AFTER_PREPARE事件修改状态码等信息。
yii\web\Response 通过 getStatusCode 函数获取状态码,所以,这也告诉我们,应该有一个函数 setStatusCode 可以修改状态码,事实也是如此。
关于状态码以及状态码的描述,yii\web\Response::httpStatuses 属性有一些定义,但是,你也可以自定义状态码以及httpStatuses属性来改变这一现状。这便是在开发rest api的时候,很多人留言要的自定义状态码和状态码描述。
除此之外,yii\web\Response::sendHeaders 方法更多的是向客户端发送响应头信息以及cookie信息。
最后,调用 yii\web\Response::sendContent 函数发送响应正文。
响应正文发送之后,触发 yii\web\Response::EVENT_AFTER_SEND 事件,设置 yii\web\Respons::isSent为true,标识 send 结束。
回到 yii\base\Application::run 方法。
$response->send 方法被调用之后,标记当前应用的状态为 yii\base\Application::STATE_END,return yii\web\Response::exitStatus。
以上,便是整个应用的生命周期过程。当然,这是在一切都正常的情况下。比如不抛出异常等。