1. 程式人生 > >Laravel5.5原始碼詳解 -- Laravel-debugbar及使用elementUI-ajax的注意事項

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進行注入渲染,最後這些注入的程式碼會返回給頁面,造成混亂和難以處理。