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