正文
这篇文章我们将学习 Laravel 项目中一个很重要的主题 -- 「路由」。
可以说几乎所有的框架都会涉及到「路由」的处理,简单一点讲就将用户请求的 url 分配到对应的处理程序。
那么还等什么,赶紧上车吧!
路由加载原理
这节我们将重点讲解如何加载我们在
routes
目录下的定义的
web.php
路由配置文件(仅考虑典型的 Web 应用)。
预备知识
通过之前 Laravel 内核解读文章我们知道在 Laravel 中,所有的服务都是通过「服务提供者」的
register
方法绑定到「Laralvel 服务容器」中, 之后才可以在 Laravel 项目中使用。
我想你自然的会想到:加载路由文件任务本质是一种服务,它实现的功能是将路由文件中定义的路由加载到 Laravel 内核中, 然后再去匹配正确的路由并处理 HTTP 请求。所以,这里我们应该查找到与路由有关的「服务提供者」去注册和启动路由相关服务。
现在让我们到
config/app.php
配置文件中的
providers
节点去查找与路由相关的「服务提供者」,没错就是
App\Providers\RouteServiceProvider::class
类。
提示:有关「服务提供者」的运行原理,你可以阅读「
深入剖析 Laravel 服务提供者实现原理
」一文,这篇文章深入讲解「服务提供者」 注册和启动原理。对此不太了解的朋友可以后续补充一下这方面知识。
这里有必要简单介绍下「服务提供者」的加载和执行过程:
-
首先,HTTP 内核程序会去执行所有「服务提供者」
register
方法,将所有的服务注册到服务容器内,这里的注册指的是将服务绑定(bind)到容器;
-
当所有「服务提供者」注册完后,会执行已完成注册「服务提供者」的
boot
方法启动服务。
「服务提供者」的注册和启动处理由
Illuminate\Foundation\Http\Kernel
这个 HTTP 内核程序完成。
了解完「服务提供者」的基本概念后,我们不难知道
RouteServiceProvider
路由提供者服务,同样由
注册(register)
和
启动(boot)
这两个处理去完成服务加载工作。
深入 RouteServiceProvider 服务提供者
进入到
RouteServiceProvider
源码中,让我们看看它在注册和启动时究竟如何工作才能载入路由配置。
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
/**
* @see https://github.com/laravel/laravel/blob/5994e242152764a3aeabd5d88650526aeb793b90/app/Providers/RouteServiceProvider.php
*/
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to your controller routes. 定义当前 Laravel 应用控制器路由的命名空间。
*/
protected $namespace = 'App\Http\Controllers';
/**
* Define your route model bindings, pattern filters, etc. 定义路由绑定、正则过滤等。
*/
public function boot()
{
parent::boot();
}
/**
* Define the routes for the application. 定义应用的路由。
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
/**
* Define the "web" routes for the application. 定义应用 Web 路由。
*
* These routes all receive session state, CSRF protection, etc. 这里定义的所有路由都会处理会话状态和 CSRF 防护等处理。
*/
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
/**
* Define the "api" routes for the application. 定义应用 API 路由。
*
* These routes are typically stateless. 在此定义的路由为典型的无状态路由。
*/
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
}
没错阅读方便,我删除了源码中部分的注释和空白行。
所以,我们仅需要将目光集中到
RouteServiceProvider
的
boot
方法中就可以了,其实在它方法体中只是去调用父类的
boot
方法完成服务启动处理。
另外,在类的内部还声明了
mapXXX()
系列方法,这些方法是用于定义应用程序的路由的实际操作,有关
map
系列函数的解读会在稍后进一步讲解。
还是先让我们看看
Illuminate\Foundation\Support\Providers\RouteServiceProvider
父类是如何处理
启动(boot)
服务的吧:
<?php
namespace Illuminate\Foundation\Support\Providers;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Routing\UrlGenerator;
/**
* @mixin \Illuminate\Routing\Router
* @see https://github.com/laravel/framework/blob/5.4/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php
*/
class RouteServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot()
{
$this->setRootControllerNamespace();
if ($this->app->routesAreCached()) {
$this->loadCachedRoutes();
} else {
$this->loadRoutes();
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
});
}
}
/**
* Set the root controller namespace for the application. 设置应用控制器根命名空间。
*/
protected function setRootControllerNamespace()
{
if (! is_null($this->namespace)) {
$this->app[UrlGenerator::class]->setRootControllerNamespace($this->namespace);
}
}
/**
* Load the cached routes for the application. 从缓存中加载路由。
*/
protected function loadCachedRoutes()
{
$this->app->booted(function () {
require $this->app->getCachedRoutesPath();
});
}
/**
* Load the application routes. 加载应用路由。
*/
protected function loadRoutes()
{
// 加载应用的路由通过执行服务容器的 call 方法调用相关加载类
// 这里既是调用子类 App\\Providers\\RouteServiceProvider::class 的 map 方法读取配置。
if (method_exists($this, 'map')) {
$this->app->call([$this, 'map']);
}
}
}
「路由服务提供者」启动过程总结起来一共分为以下几个步骤:
-
将我们 Laravel 应用的控制器所在的命名空间设置到 URL 生成器中(UrlGenerator)供后续使用;
-
处于系统性能上的考量,会率先检测是否启用路由缓存。已缓存路由的话直接从缓存文件中读取路由配置;
-
未缓存则由
loadRoutes
方法执行缓存处理。最终回到由
App\Providers\RouteServiceProvider
类中定义的
map
方法执行路由载入处理。
学习到这,大家对路由的整个加载过程应该已经建立起一个比较宏观上的概念了。
深入研究 map 定义路由系列方法
建立起宏观上的路由加载流程后,我们百尺竿头更进一步,继续深入到
mapXXX()
系列方法,因为这些方法才是实际去执行路由加载处理的组件。
在之前的源码清单中,我们看到在
map
方法内部会分别调用并执行了
mapWebRoutes()
和
mapApiRoutes()
这两个方法,它们的工作是分别加载 Web 路由和 Api 路由配置。
由于篇幅所限,这里我们只解析 Web 路由
mapWebRoutes
的载入原理,因为这两个加载路由处理过程几乎完全一样,不是么朋友?
...
/**
* Define the "web" routes for the application. 定义应用 Web 路由。
*
* These routes all receive session state, CSRF protection, etc. 这里定义的所有路由都会处理会话状态和 CSRF 防护等处理。
*/
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
...
mapWebRoutes
在处理 Web 路由加载时,通过
Route
门面(Facade)所代理的
Illuminate\Routing\Router
服务依次执行:
-
执行
Route::middleware('web')
将
web
中间件注册到路由;
-
执行
namespace($this->namespace)
方法,将控制器命名空间设置到路由中;
-
最后执行以路由文件
base_path('routes/web.php')
目录为参数的
group
方法完成 Web 路由组的设置。
大致如此,我们继续,看看它是如何执行 middleware 等方法的 !
打开 Router 门面的服务
Illuminate\Routing\Router
类的内部,可能你无法找到
middleware
方法声明。
没错它是通过实现
__call
魔术方法动态的执行反射功能,完成调用
middleware
方法,并返回
RouteRegistrar
实例。
<?php
namespace Illuminate\Routing;
/**
* @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Routing/Router.php
*/
class Router implements RegistrarContract, BindingRegistrar
{
/**
* The route group attribute stack.
*/
protected $groupStack = [];
/**
* Create a route group with shared attributes. 创建拥有公共属性(中间件、命名空间等)的路由组。
*/
public function group(array $attributes, $routes)
{
$this->updateGroupStack($attributes);
// Once we have updated the group stack, we'll load the provided routes and
// merge in the group's attributes when the routes are created. After we
// have created the routes, we will pop the attributes off the stack.
$this->loadRoutes($routes);
array_pop($this->groupStack);
}
/**
* Update the group stack with the given attributes. 将给定属性(中间件、命名空间等)更新到路由组栈中。
*/
protected function updateGroupStack(array $attributes)
{
if (! empty($this->groupStack)) {
$attributes = RouteGroup::merge($attributes, end($this->groupStack));
}
$this->groupStack[] = $attributes;
}
/**
* Load the provided routes. 载入定义的路由
*
* @param \Closure|string $routes
* @return void
*/
protected function loadRoutes($routes)
{
if ($routes instanceof Closure) {
$routes($this);
} else {
$router = $this;
require $routes;
}
}
/**
* Dynamically handle calls into the router instance. 动态处理 router 实例中的方法调用。
*/
public function __call($method, $parameters)
{
// if (static::hasMacro($method)) {
// return $this->macroCall($method, $parameters);
// }
// 请看这里,在这里通过反射动态的调用 middleware 方法,完成中间件的处理
if ($method == 'middleware') {
return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
}
// return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
}
}
由于篇幅所限,这篇文章将不展开对
RouteRegistrar
源码的研究,感兴趣的朋友可以自行研究。
简短截说,最终在
RouteRegistrar::group
方法内部完成对
Illuminate\Routing\Router::group
方法的调用,实现载入路由文件处理。
最终在
Illuminate\Routing\Router::group
方法里去执行路由文件引入处理:
-
通过
updateGroupStack
方法,更新路由组中的属性(即由 Route::middleware(...)->namespace(...) 设置的中间件和命名空间等);
-
使用
loadRoutes
方法引入
base_path('routes/web.php')
文件中定义的路由。
到这我们就完整的分析完路由文件的加载流程,由于涉及到的模块较多,还需要读者朋友们再琢磨琢磨才能消化。
提示:在 Laravel 中门面是一种提供了操作简单的能够使用静态方法来方式访问 Laravel 服务的机制。对「门面 Facade」不太了解的朋友可以阅读「
深入浅出 Laravel 的 Facade 外观系统
」。
路由分发
这一节我们主要讲解 HTTP 如何被分发到相关路由并执行路由设置的回调(或控制器)。
如果你有了解过 Laravel 生命周期的话,应该知道所有的 HTTP 请求都是由
Illuminate\Foundation\Http\kernel::class
内核处理的,而捕获 HTTP 请求操作位于项目的入口文件
public/index.php
中。
接收 HTTP 请求
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
具体一点讲就是先从服务容器解析出
Illuminate\Contracts\Http\Kernel::class
服务实例,再执行服务的
handle
方法处理 HTTP 请求。
本文不涉及讲解如何捕获一个 HTTP 请求
Illuminate\Http\Request::capture()
,如果后续有时间会开设一篇文章详细讲解一下,作为本文的补充资料。但在这里你只需要知道,我们的
handle
处理器接收用户的
Request
作为参数,然后去执行。
所以我们需要深入到
handle
才能知道 HTTP 请求是如何被匹配路由和处理回调(或控制器)的。
由 HTTP 内核处理 HTTP 请求
此处略去 N 个解析,嗯,我们找到了
Illuminate\Foundation\Http\kernel::class
服务实例,相信对于你这不是什么难事。
<?php
namespace Illuminate\Foundation\Http;
use Exception;
use Throwable;
use Illuminate\Routing\Router;
use Illuminate\Routing\Pipeline;
use Illuminate\Support\Facades\Facade;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Http\Kernel as KernelContract;
use Symfony\Component\Debug\Exception\FatalThrowableError;
class Kernel implements KernelContract
{
/**
* Handle an incoming HTTP request. 处理 HTTP 请求
* @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php#L111
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
...
} catch (Throwable $e) {
...
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
/**
* Send the given request through the middleware / router. 将用户请求发送到中间件和路由
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
/**
* Get the route dispatcher callback. 获取分发路由回调(或者控制器)
* @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php#L171
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
}
处理整个 HTTP 请求的过程分完几个阶段:
-
清空已解析的请求(clearResolvedInstance);
-
执行应用的引导程序(bootstrap),这部分的内容请查阅
深入剖析 Laravel 服务提供者实现原理
的服务提供者启动原理小结。
-
将请求发送到中间件和路由中,这个由管道组件完成(Pipeline)。
对于前两个阶段的处理可以阅读我给出的相关文章。另外补充两篇有关中间件的文章
Laravel 中间件原理
和
Laravel 管道流原理
,可以去研究下
Laravel 中间件如何工作的。
路由分发处理
好了经历过千锤百炼后,我们的请求终于顺利到达
then($this->dispatchToRouter())
路由处理了,真是不容易。那么现在,让我们看看
dispatchToRouter
是如何分发路由的。
<?php
...
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
...
从这段源码我们知道路由分发接收 $request 请求实例,然后执行分发(dispatch)操作,这些处理会回到
Illuminate\Routing\Router
服务中处理:
<?php
namespace Illuminate\Routing;
...
class Router implements RegistrarContract, BindingRegistrar
{
...
/**
* Dispatch the request to the application. 将 HTTP 请求分发到应用程序。
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
/**
* Dispatch the request to a route and return the response. 将请求分发到路由,并返回响应。
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
/**
* Find the route matching a given request. 查找与请求 request 匹配的路由。
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
// 从 RouteCollection(由 Router::get('/', callback) 等设置的路由) 集合中查找与 $request uri 相匹配的路由配置。
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
/**
* Return the response for the given route. 执行路由配置的闭包(或控制器)返回响应 $response。
*
* @param Route $route
* @param Request $request
* @return mixed
*/
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
/**
* Run the given route within a Stack "onion" instance. 运行给定路由,会处理中间件等处理(这里的中间件不同于 Kernel handle 中的路由,是仅适用当前路由或路由组的局部路由)。
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
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(
// $route->run() 将运行当前路由闭包(或控制器)生成结果执行结果。
$request, $route->run()
);
});
}
/**
* Create a response instance from the given value.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param mixed $response
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
}
...
}
路由分发流程
Illuminate\Routing\Router
服务将接收被分发到的请求($request)然后执行路由设置是配置的闭包(或控制器)函数,整个过程包括: