Laravel5.5源码详解 -- Laravel-debugbar及使用elementUI-ajax的注意事项

Laravel5.5源码详解 – Laravel-debugbar 及使用elementUI - ajax的注意事项

关于laravel对中间件的处理,请参中间件考另文,
Laravel5.5源码详解 – 中间件MiddleWare分析
这里只是快速把debugbar的事务处理流程记录一遍。

我在Illuminate\Pipeline\Pipeline的then函数中进行中间件捕获,发现有下面这些中间件,

array:6 [▼
  0 => "Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode"
  1 => "Illuminate\Foundation\Http\Middleware\ValidatePostSize"
  2 => "App\Http\Middleware\TrimStrings"
  3 => "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"
  4 => "App\Http\Middleware\TrustProxies"
  5 => "Barryvdh\Debugbar\Middleware\InjectDebugbar"
]
array:6 [▼
  0 => "App\Http\Middleware\EncryptCookies"
  1 => "Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse"
  2 => "Illuminate\Session\Middleware\StartSession"
  3 => "Illuminate\View\Middleware\ShareErrorsFromSession"
  4 => "App\Http\Middleware\VerifyCsrfToken"
  5 => "Illuminate\Routing\Middleware\SubstituteBindings"
]

其中就包括这个Barryvdh\Debugbar\Middleware\InjectDebugbar,它是在larave启动时,在vendor\composer\installed.json发现并引入,

laravel-debugbar的配置在Barryvdh\laravel-debugbar\config\debugbar,里面解释比较详尽,这里也不再重复。顺便说一下,这个类是在Barryvdh\Debugbar\ServiceProvider中注册的,

<?php namespace Barryvdh\Debugbar;

use Barryvdh\Debugbar\Middleware\DebugbarEnabled;
use Barryvdh\Debugbar\Middleware\InjectDebugbar;
use DebugBar\DataFormatter\DataFormatter;
use DebugBar\DataFormatter\DataFormatterInterface;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
use Illuminate\Session\SessionManager;

class ServiceProvider extends \Illuminate\Support\ServiceProvider
{
    protected $defer = false;

    public function register()
    {
        $configPath = __DIR__ . '/../config/debugbar.php';
        $this->mergeConfigFrom($configPath, 'debugbar');

        $this->app->alias(
            DataFormatter::class,
            DataFormatterInterface::class
        );

        $this->app->singleton(LaravelDebugbar::class, function () {
                $debugbar = new LaravelDebugbar($this->app);

                if ($this->app->bound(SessionManager::class)) {
                    $sessionManager = $this->app->make(SessionManager::class);
                    $httpDriver = new SymfonyHttpDriver($sessionManager);
                    $debugbar->setHttpDriver($httpDriver);
                }

                return $debugbar;
            }
        );

        $this->app->alias(LaravelDebugbar::class, 'debugbar');

        $this->app->singleton('command.debugbar.clear',
            function ($app) {
                return new Console\ClearCommand($app['debugbar']);
            }
        );

        $this->commands(['command.debugbar.clear']);
    }

    // 这里注册了很多事件处理功能,都是后面在处理request的时候可能会用到的
    public function boot()
    {
        $configPath = __DIR__ . '/../config/debugbar.php';
        $this->publishes([$configPath => $this->getConfigPath()], 'config');

        $routeConfig = [
            'namespace' => 'Barryvdh\Debugbar\Controllers',
            'prefix' => $this->app['config']->get('debugbar.route_prefix'),
            'domain' => $this->app['config']->get('debugbar.route_domain'),
            'middleware' => [DebugbarEnabled::class],
        ];

        $this->getRouter()->group($routeConfig, function($router) {
            $router->get('open', [
                'uses' => 'OpenHandlerController@handle',
                'as' => 'debugbar.openhandler',
            ]);

            $router->get('clockwork/{id}', [
                'uses' => 'OpenHandlerController@clockwork',
                'as' => 'debugbar.clockwork',
            ]);

            $router->get('assets/stylesheets', [
                'uses' => 'AssetController@css',
                'as' => 'debugbar.assets.css',
            ]);

            $router->get('assets/javascript', [
                'uses' => 'AssetController@js',
                'as' => 'debugbar.assets.js',
            ]);
        });

        $this->registerMiddleware(InjectDebugbar::class);
    }
    protected function getRouter()
    {
        return $this->app['router'];
    }
    protected function getConfigPath()
    {
        return config_path('debugbar.php');
    }
    protected function publishConfig($configPath)
    {
        $this->publishes([$configPath => config_path('debugbar.php')], 'config');
    }
    protected function registerMiddleware($middleware)
    {
        $kernel = $this->app[Kernel::class];
        $kernel->pushMiddleware($middleware);
    }
    public function provides()
    {
        return ['debugbar', 'command.debugbar.clear', DataFormatterInterface::class, LaravelDebugbar::class];
    }
}

重点在这里,实际处理response和request的handle函数在Barryvdh\Debugbar\Middleware\InjectDebugbar中,

<?php namespace Barryvdh\Debugbar\Middleware;

use Error;
use Closure;
use Exception;
use Illuminate\Http\Request;
use Barryvdh\Debugbar\LaravelDebugbar;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Symfony\Component\Debug\Exception\FatalThrowableError;

class InjectDebugbar
{
    protected $container;  
    protected $debugbar;
    protected $except = [];

    public function __construct(Container $container, LaravelDebugbar $debugbar)
    {
        $this->container = $container;
        $this->debugbar = $debugbar;
        $this->except = config('debugbar.except') ?: [];
    }

    public function handle($request, Closure $next)
    {
        // 如果debugbar没有使能,或传入的request是空的,则直接返回。
        if (!$this->debugbar->isEnabled() || $this->inExceptArray($request)) {
            return $next($request);
        }

        // 注册事务处理功能
        $this->debugbar->boot();

        try {
            /** @var \Illuminate\Http\Response $response */
            // 可以看到,handle是处理后置的,也就是在(来回两次经过handle)回途中处理函数,
            // 所以这里先$next()
            $response = $next($request);

        } catch (Exception $e) {
            $response = $this->handleException($request, $e);
        } catch (Error $error) {
            $e = new FatalThrowableError($error);
            $response = $this->handleException($request, $e);
        }

        // 处理后置,接上面的next()之后才是debugbar干活的时间
        // Modify the response to add the Debugbar
        $this->debugbar->modifyResponse($request, $response);

        // 处理完毕,返回结果
        return $response;

    }

上面这段,真正起作用的就是这句:$this->debugbar->modifyResponse($request, $response); ,它是debugbar 修改response的地方所在,具体请看在Barryvdh\Debugbar\LaravelDebugbar,请注意其中的注释,

public function modifyResponse(Request $request, Response $response)
{
        // 如果没有使能,就直接返回response
        $app = $this->app;
        if (!$this->isEnabled() || $this->isDebugbarRequest()) {
            return $response;
        }

        // Show the Http Response Exception in the Debugbar, when available
        // 如果有Http异常,则打印显示出来
        if (isset($response->exception)) {
            $this->addThrowable($response->exception);
        }

        // 要不要调试设置信息,默认是不需要的
        if ($this->shouldCollect('config', false)) {
            try {
                $configCollector = new ConfigCollector();
                $configCollector->setData($app['config']->all());
                $this->addCollector($configCollector);
            } catch (\Exception $e) {
                $this->addThrowable(
                    new Exception(
                        'Cannot add ConfigCollector to Laravel Debugbar: ' . $e->getMessage(),
                        $e->getCode(),
                        $e
                    )
                );
            }
        }

        // 如果绑定session调试
        if ($this->app->bound(SessionManager::class)){
            /** @var \Illuminate\Session\SessionManager $sessionManager */
            $sessionManager = $app->make(SessionManager::class);
            $httpDriver = new SymfonyHttpDriver($sessionManager, $response);
            $this->setHttpDriver($httpDriver);

            if ($this->shouldCollect('session') && ! $this->hasCollector('session')) {
                try {
                    $this->addCollector(new SessionCollector($sessionManager));
                } catch (\Exception $e) {
                    $this->addThrowable(
                        new Exception(
                            'Cannot add SessionCollector to Laravel Debugbar: ' . $e->getMessage(),
                            $e->getCode(),
                            $e
                        )
                    );
                }
            }
        } else {
            $sessionManager = null;
        }

        // 貌似这句的意思是,如果只调试一个session? 还没进入源码深究。
        if ($this->shouldCollect('symfony_request', true) && !$this->hasCollector('request')) {
            try {
                $this->addCollector(new RequestCollector($request, $response, $sessionManager));
            } catch (\Exception $e) {
                $this->addThrowable(
                    new Exception(
                        'Cannot add SymfonyRequestCollector to Laravel Debugbar: ' . $e->getMessage(),
                        $e->getCode(),
                        $e
                    )
                );
            }
        }

        // 如果要支持Clockwork调试,(比如支持Chrome插件Clockwork调试)
        if ($app['config']->get('debugbar.clockwork') && ! $this->hasCollector('clockwork')) {

            try {
                $this->addCollector(new ClockworkCollector($request, $response, $sessionManager));
            } catch (\Exception $e) {
                $this->addThrowable(
                    new Exception(
                        'Cannot add ClockworkCollector to Laravel Debugbar: ' . $e->getMessage(),
                        $e->getCode(),
                        $e
                    )
                );
            }

            $this->addClockworkHeaders($response);
        }

        // 首先判断一下,这是不是一个redirect()的请求(刷新页面)
        // 这个判断的语句原型是$this->statusCode >= 300 && $this->statusCode < 400;
        // 函数原型在vendor\symfony\http-foundation\Response.php中,
        if ($response->isRedirection()) {
            try {
                $this->stackData();
            } catch (\Exception $e) {
                $app['log']->error('Debugbar exception: ' . $e->getMessage());
            }
        } elseif (
        // 如果是ajax请求,并且已经设置了对ajax进行调试,则这在里处理
            $this->isJsonRequest($request) &&
            $app['config']->get('debugbar.capture_ajax', true)
        ) {
            try {
                $this->sendDataInHeaders(true);

                if ($app['config']->get('debugbar.add_ajax_timing', false)) {
                    $this->addServerTimingHeaders($response);
                }

            } catch (\Exception $e) {
                $app['log']->error('Debugbar exception: ' . $e->getMessage());
            }
        } elseif (
        // 如果headers有Content-Type这个标签,并且不是html,那么就应该是JSON数据
        // 很明显,这里只对Content-Type=JSON的数据进行操作,
        // 对其他类型的数据,如图片,MSWORD等,则直接抛出异常
            ($response->headers->has('Content-Type') &&
                strpos($response->headers->get('Content-Type'), 'html') === false)
            || $request->getRequestFormat() !== 'html'
            || $response->getContent() === false
        ) {
            try {
                // Just collect + store data, don't inject it.
                $this->collect();
            } catch (\Exception $e) {
                $app['log']->error('Debugbar exception: ' . $e->getMessage());
            }
        } elseif ($app['config']->get('debugbar.inject', true)) {
        // 对普通的情况,debugbar会在这里修改的response,并注入渲染
            try {
                $this->injectDebugbar($response);
            } catch (\Exception $e) {
                $app['log']->error('Debugbar exception: ' . $e->getMessage());
            }
        }
        return $response;
    }

用到的 $app['config'] 的原貌是这样的,

Repository {#24 ▼
  #items: array:13 [▼
    "app" => array:13 [▶]
    "auth" => array:4 [▶]
    "broadcasting" => array:2 [▶]
    "cache" => array:3 [▶]
    "database" => array:4 [▶]
    "filesystems" => array:3 [▶]
    "mail" => array:9 [▶]
    "queue" => array:3 [▶]
    "services" => array:4 [▶]
    "session" => array:15 [▶]
    "view" => array:2 [▶]
    "debugbar" => array:13 [▼
      "enabled" => null
      "except" => []
      "storage" => array:5 [▶]
      "include_vendors" => true
      "capture_ajax" => true
      "add_ajax_timing" => false
      "error_handler" => false
      "clockwork" => false
      "collectors" => array:21 [▶]
      "options" => array:7 [▶]
      "inject" => true
      "route_prefix" => "_debugbar"
      "route_domain" => null
    ]
    "trustedproxy" => array:2 [▶]
  ]
}

比如,这个inject是true,就对应了上面的普通调试情况,现在来到下一步的重点,

    public function injectDebugbar(Response $response)
    {
        $content = $response->getContent();

        $renderer = $this->getJavascriptRenderer();
        if ($this->getStorage()) {
            $openHandlerUrl = route('debugbar.openhandler');
            $renderer->setOpenHandlerUrl($openHandlerUrl);
        }

        $renderedContent = $renderer->renderHead() . $renderer->render();

        $pos = strripos($content, '</body>');
        if (false !== $pos) {
            $content = substr($content, 0, $pos) . $renderedContent . substr($content, $pos);
        } else {
            $content = $content . $renderedContent;
        }

        // Update the new content and reset the content length
        // 在这里注入页面渲染与调试信息
        $response->setContent($content);        
        $response->headers->remove('Content-Length');
    }

整个大致流程就是这样子的。

我使用的laravel+vuejs+elementUI做练习,发现在使用elementUI时,el-upload默认并没有支持ajax,虽然它采用了XMLHttpRequest()来处理上传,但头文件并没有XHR处理,所以laravel收到其发出的只是一个普通的post请求,这种情况下,laravel-debugbar会把所有的调试信息和相关渲染,全部加入到response中返回(对axios也是同样如此),而这恰恰不是我们所需要的,所以有必要特别细说一下。

如果是ajax请求,debugbar会在这里判断

protected function isJsonRequest(Request $request)
    {
        // If XmlHttpRequest, return true
        if ($request->isXmlHttpRequest()) {
            return true;
        }

        // Check if the request wants Json
        $acceptable = $request->getAcceptableContentTypes();
        return (isset($acceptable[0]) && $acceptable[0] == 'application/json');
    }

其中有对request是否为ajax的判断 $request->isXmlHttpRequest() ,它实际是在vendor\symfony\http-foundation\Request.php里面,

public function isXmlHttpRequest()
{
    return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
}

所以不难明白,为什么在headers里面,必须加入'X-Requested-With': 'XMLHttpRequest' 这一行,或者是这样,

options.headers['X-Requested-With'] = 'XMLHttpRequest';

其目的就是让laravel-debugger知道,发送的是ajax请求,不要再把调试信息和页面渲染再注入response了。

如果不加这一行的话,laravel会默认这是一个普通的Post请求,此时,larave-debugbar这样的插件,就会对response进行注入渲染,最后这些注入的代码会返回给页面,造成混乱和难以处理。

猜你喜欢

转载自blog.csdn.net/tanmx219/article/details/78915890