响应预处理

简介

上一章,我们了解了控制器与方法的运行原理。

由于控制器与方法,都是我们自己自定义的,那么返回的数据类型也会出现不确定的情况。

我总结一下,最常用的两类返回写法:

  • view 辅助函数返回的数据---一般情况下,web 路由的返回格式
return view('auth.register', [
    'message' => 'good',
]);
  • response 辅助函数返回的 json 字符串---一般情况下,api 路由的返回格式
return response()->json([
    'code' => 200,
    'message' => 'good',
]);

我想上面这两种,大家应该非常熟悉了,但是有没有两种情况的变种,使代码更加简练,更加优雅呢

答案:是有的。

这就涉及到响应预处理的源码执行原理了。

先从上一章过渡一下。

Illuminate\Routing\Router

protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            // 这个 run 函数就是我们控制器与方法执行后的 return。
                            $request, $route->run()
                        );
                    });
}

$route->run(),run 函数执行完毕取得的数据,就是我们控制器与方法执行完毕,返回的 第一手数据

prepareResponse:响应预处理

Illuminate\Routing\Router

public function prepareResponse($request, $response)
{
    return static::toResponse($request, $response);
}

toResponse:将我们自行定义返回的烂七八糟的数据类型,统一转换成 Response 或 JsonResponse。

Illuminate\Routing\Router

public static function toResponse($request, $response)
{
    /*
    * 如果我们返回的数据属于 Responsable 接口的实现,那么调用 toResponse 方法。
    * toResponse 方法一定会有的,因为 Responsable 接口规定了必须有 toResponse 方法。
    * 这段作用我想应该是:让我们在返回控制器与方法数据时,统一一个返回数据处理方式,来替代系统默认。
    */
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);
    }

    // if elseif 结构,只能运行一个哦。
    if ($response instanceof PsrResponseInterface) {
        // 当 $response 属于 PSR 规范的响应接口时,执行此段代码,用的比较少,不做多介绍
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        /*
        * 当 $response 是模型示例时,且模型示例 save 时,是新增数据,而不是修改数据时,此段代码。
        * 默认返回的是 jsonResponse,内容包括新增数据的全部内容。
        * 此时 HTTP 状态码为 201,代表资源创建成功。
        */
        $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 不属于 SymfonyResponse ,且 $response 只要满足以下条件任意一个,就执行此代码
        * 1、$response 属于 Arrayable,此时 $response 必有 toArray 方法;
        * 2、$response 属于 Jsonable,此时 $response 必有 toJson 方法;
        * 3、$response 属于 ArrayObject;
        * 4、$response 属于 JsonSerializable,此时 $response 必有 jsonSerialize 方法;
        * 5、$response 是 PHP 数组。
        * 结果,同 $response 是模型示例时,唯一区别这个的返回码是 200,仅代表请求成功。
        */
        $response = new JsonResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        // 最后 $response 上面的条件都不满足;那就仅剩 Response 了,直接实例化。。
        $response = new Response($response);
    }

    // 最后如果 $response 的状态码是 304,就返回的内容和上次一样,直接告诉浏览器从缓存加载。
    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

    // 往 $response 中写入一些必要的响应头信息,返回 Response。。。
    return $response->prepare($request);
}

通过上面源码分析,我们知道了,响应就两种:JsonResponse 和 Response。但是它们两个的最终归宿仅仅只有一种,为什么呢?

因为给前端(无论是浏览器还是安卓)就只有一个响应,没毛病吧,所以这两种响应最终继承自 Symfony\Component\HttpFoundation\Response

JsonResponse:满足 api 接口要求返回 json 字符串的要求,它继承自 Symfony\Component\HttpFoundation\JsonResponse,然而 Symfony\Component\HttpFoundation\JsonResponse 又继承自 Symfony\Component\HttpFoundation\Response

Response:满足浏览器获取网页数据的要求,它继承自 Symfony\Component\HttpFoundation\Response

Response 变种写法

我们熟悉的写法:

return view('auth.register', [
    'message' => 'good',
]);

原理:view 函数最终返回 Illuminate\View\View 类对象。到执行到 toResponse 时,View 对象不满足转换成 JsonResponse 的条件,那么只能转成 Response 。

那么我们看一下 Response 类做了什么,先看构造函数

Symfony\Component\HttpFoundation\Response

public function __construct($content = '', int $status = 200, array $headers = array())
{
    // 初始化响应头信息
    $this->headers = new ResponseHeaderBag($headers);
    // 设置响应体内容
    $this->setContent($content);
    // 设置 HTTP 状态码
    $this->setStatusCode($status);
    // 设置 HTTP 协议版本
    $this->setProtocolVersion('1.0');
}

我们重点看 setContent 方法:先子类后父类

Illuminate\Http\Response

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

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

        $content = $this->morphToJson($content);
    }

    // 这一行比较有意思,因为 Laravel-admin 框架就是运用这里
    elseif ($content instanceof Renderable) {
        $content = $content->render();
    }

    // View 不满足以上条件,到父类找
    parent::setContent($content);

    return $this;
}

Symfony\Component\HttpFoundation\Response

public function setContent($content)
{
    if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable(array($content, '__toString'))) {
        throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content)));
    }

    // View 一定在这里执行了。我们知道一个对象转换成字符串,那么对象中一定有 __toString() 魔术方法。
    $this->content = (string) $content;

    return $this;
}

View 中的 __toString 魔术方法

Illuminate\View\View

public function __toString()
{
    return $this->render();
}

那么我们来看 render 方法,字面意思:渲染。

Illuminate\View\View

public function render(callable $callback = null)
{
    try {

        // renderContents 会调用 blade 模板引擎,加载响应数据,获取原始 html 代码字符串。
        $contents = $this->renderContents();

        $response = isset($callback) ? call_user_func($callback, $this, $contents) : null;

        $this->factory->flushStateIfDoneRendering();

        // 最终返回 html 字符串。。。
        return ! is_null($response) ? $response : $contents;
    } catch (Exception $e) {
        $this->factory->flushState();

        throw $e;
    } catch (Throwable $e) {
        $this->factory->flushState();

        throw $e;
    }
}

关于 blade 模板引擎如何运行,这里不展开了,篇幅太长。。。我们仅知道 render 方法返回是 html 字符串就行了,浏览器会对这些字符进行编译和渲染,然后我们就看到了页面。

讲了半天,也没说怎么变种去写,,,汗!!!源码太复杂,写着写着就跑调啦。

变种一:函数实例化 Response

return response(view('auth.register', [
    'message' => 'good',
]));

变种二:直接实例化 Response

return new Response(view('auth.register', [
    'message' => 'good',
]));

需要 use Illuminate\Http\Response

变种三:直接 render

return view('auth.register', [
    'message' => 'good',
])->render();

变种四:View 门面

return View::make('auth.register', [
    'message' => 'good',
])

当然后两种与前两种随便组合。。。

最后,推荐原始写法,那种最简便。。。

JsonResponse 变种写法

return response()->json([
    'code' => 200,
    'message' => 'good',
]);

原理: response() 没有参数故返回 response 工厂类对象,当有参数时,直接返回 Response 对象

我们来看 json 方法

Illuminate\Routing\ResponseFactory

public function json($data = [], $status = 200, array $headers = [], $options = 0)
{
    return new JsonResponse($data, $status, $headers, $options);
}

哦,原来是直接实例化 JsonResponse 类。

关于 JsonResponse 内部如何运行的,就不展开了。我简单讲一下:首先在构造函数中调用 setData 方法,setData 方法将数组转换成 json 字符串,然后调用 setJson,将 json 字符串赋值给 data 属性,最后调用 update 方法,设置 Content-Typeapplication/json,最后调用 setContent,将 json 字符串设置到 Response 的响应体中。

变种一:当通过 api 接口新增数据时,我们可以直接返回 新增的模型

public function store(Request $request) :Model
{
    // 开启数据库事物是个好习惯。
    DB::beginTransaction();
    try {
        $article = new Article;
        $article->title = $request->input('title', '');
        $article->content = $request->input('content', '');
        // 尝试保存
        $article->save();
        // 没异常,提交事物
        DB::commit();
        // 返回模型对象
        return $article;
    } catch (Exception $e) {
        // 有异常,回滚数据库操作
        DB::rollBack();
        // 向上抛出异常
        throw $e;
    }
}

变种二:直接返回集合

return collect([
    'code' => 200,
    'message' => 'good',
]);

理由:看下面

class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{
    //...
}

实现了 Arrayable 和 Jsonable 的接口。

变种三:直接返回数组(推荐方法,简单。。。)

return [
    'code' => 200,
    'message' => 'good',
];

最后讲一下

下一章。我们把 Response 的 prepare 方法看一下。
最后一章。就是 Laravel 生命周期结束的时候。

还有两章了,加油。。。

本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。

本文章首发在 LearnKu.com 网站上。
上一篇 下一篇
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
发起讨论 只看当前版本


暂无话题~