本文是基于Laravel 5.4版本的路由模块代码进行分析书写;
官方 API 地址 https://laravel.com/api/5.4/Illuminate/Routing.html
模块组成
下图展示了路由模块中各个文件的关系,并进行简要说明;
剖析
服务提供者
看Laravel模块,首先找ServiceProvider文件,这是模块与IOC容器交互的入口,从这个文件,可以看出该模块提供向系统提供了哪些服务;
1 | public function register() { |
路由管理
“路由管理”服务有以下元素需要了解:
Route:路由;会记录Url、Http动作、Action(路由要执行的具体对象,可能是Closure,也可以是某个Controller中的方法),路由参数,路由参数的约束;RouteCollection:路由集,用来存储所有Route对象的“盒子”;RouteGroup:路由组;只有路由注册过程中会临时用到;存储一批路由公共的一些属性,属性包括domain、prefix、as、middleware、namespace、where;Resource:资源路由;资源路由是一套路由的统称,包含列表(index)、显示增加(create)、保存增加(store)、显示详情(show)、显示编辑详情(edit)、更新编辑(update)、删除详情(destory);同时可以通过调用only或except方法或参数的形式只生成部分路由;Action:路由要执行的对象;有两种表现形式,一是Closure函数,二是类似['uses' => 'FooController@method', 'as' => 'name']这样的字符串;对于不同的表现形式,路由在执行时会调用不同的处理;
注册流程
在项目启动后,会执行所有ServiceProvider的loadRoutes方法,也就是调用map方法,一般情况下map方法如下1
2
3public function map(Router $router){
require __DIR__.'/routes.php';
}
这时候,项目就会执行很多Route::get、Route::post、Route::group方法;
当遇到Route::group方法时,会实例化一个RouteGroup对象,put进Router管理类的路由组栈头部;而后当执行get、post这类具体的注册路由方法时,会把当前路由组栈中所有组的属性合并进新路由中,将新路由存储在RouteCollection这个大盒子里;当Route::group的Closure执行完毕时,会把头部的RouteGroup实例pull出去;
当执行Route::resource时,Router管理类会调用ResourceRegister类来完成批量注册路由;
对于
Router::get这类注册方法,Illuminate\Foudation\helpers提供了简写;Router::get简化成get,Router::post简化成post,Router::put简化成put,Router::patch简化成patch,Router::delete简化成delete,Router::resource简化成resource,
至此,RouteCollection大盒子就存放了所有要注册的路由;
request 请求匹配流程
首先,request请求会经过Foundation/Http/Kernel的handle方法,在这个方法中,请求会执行以下语句1
$this->router->dispatch($request)
这里的$this->router,就是Router管理类;dispatch方法如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public function dispatch(Request $request) {
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request) {
// 根据请求的 url 找到匹配的路由
$route = $this->findRoute($request);
// 将路由绑定到请求上
$request->setRouteResolver(function () use ($route) {
return $route;
}
// 触发 RouteMatched 事件
$this->events->dispatch(new Events\RouteMatched($route, $request));
// 通过 Pipeline 流水线执行路由上绑定的中间件及对应的方法
$response = $this->runRouteWithinStack($route, $request);
// 根据 request 请求设置 response 的响应头
return $this->prepareResponse($request, $response);
}
- 根据请求找匹配的路由
RouteCollection根据请求的http动作缩小要匹配的路由范围;在筛选出来的这些路由中依次遍历,找出第一个符合验证的路由(需要进行较验的验证在Route中的getValidators方法中声明); - 将路由绑定到请求上
- 触发
RouteMatched事件
初始化的Laravel项目没有对RouteMatched路由匹配事件进行任何的监听器绑定,如有需要,可以自定义监听器,在模块的EventServiceProvider中注册该事件监听;这样一旦请求匹配上某个路由,就可以执行自定义方法了; - 通过
Pipeline流水线执行路由上绑定的中间件及对应的方法
在runRouteWithinStack方法中,系统会判断是否需要执行中间件,如果IOC容器中设置了middleware.disable的值为true,则需要执行的中间件数组为空;否则会找到所有的中间件,并按照middlewarePriority对必要的一些中间件进行排序调整;然后执行$route->run()方法; - 根据 request 请求设置 response 的响应头
项目中会用到的一些方法
- 获取路由集合
app('router')->getRoutes()- 获取当前的请求
$request = app('router')->getCurrentRequest()- 获取当前请求所对应的路由
$route = $request->route()或$route = app('router')->getCurrentRoute()- 获取当前路由需要执行的中间件
$middlewares = app('router')->gatherRouteMiddleware($route)
Url 生成器
Url 生成器是什么?
举个例子,1
2
3
4
5
6$url = new UrlGenerator(
$routes = new RouteCollection,
$request = Request::create('http://www.foo.com/')
);
$url->to('foo/bar'); // 输出 http://www.foo.com/foo/bar
像这种基于当前请求,生成指定路径的Url;
这部分功能由两个文件完成,一个是UrlGenerator.php,另一个是RouteUrlGenerator.php;UrlGenerator.php处理根据路径名生成Url,RouteUrlGenerator.php处理根据路由生成Url;
列一些常用的使用:
根据路径名生成
使用to方法,第一个参数为路径,第二个参数是数组,implode后会接着路径名,第三个参数决定用不用https1
2
3
4
5
6
7
8
9// 路径名是 foo/bar,当前请求的根路径为 http://www.foo.com,所以输出是 http://www.foo.com/foo/bar
$url->to('foo/bar')
// 路径名是 foo/bar,当前请求的根路径为 http://www.foo.com,第三个参数决定 scheme 是 https,所以输出是 https://www.foo.com/foo/bar
$url->to('foo/bar', [], true)
// 路径名是 foo/bar,第二个参数 是补充路径名,implode 后是 /baz/boom
// 第三个参数决定 scheme 是 https,所以输出是 https://www.foo.com/foo/bar/baz/boom
$url->to('foo/bar', ['baz', 'boom'], true)
// 路径名是 foo/bar,查询参数是 ?foo=bar ,补充路径是 /baz,所以输出是 https://www.foo.com/foo/bar/baz?foo=bar
$url->to('foo/bar?foo=bar', ['baz'], true)
根据路由的 as 名生成
使用route方法,第一个参数为指定路由的 as 名,第二个参数是参数数组,第三个参数决定是否显示根目录(默认为 true)1
2
3
4
5
6
7
8
9
10
11
12$route = new Route(['GET'], 'foo/bar', ['as' => 'foo']);
$routes->add($route);
// 输出 'http://www.foo.com/foo/bar
$url->route('foo');
// 第三个参数为 false,表示不显示根目录,于是输出 /foo/bar
$url->route('foo', [], false)
// 路由中的 url 本身不带参数,则第二参数中所有关联数组都将作为查询参数
// 输出 /foo/bar?foo=bar
$url->route('foo', ['foo' => 'bar'], false)
1 | $route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}', ['as' => 'bar']); |
根据路由的 action 名生成
使用action方法,第一个参数为指定路由的 action 名,第二个参数是参数数组,第三个参数决定是否显示根目录(默认为 true)1
2
3
4
5$route = new Route(['GET'], 'foo/bam', ['controller' => 'foo@bar']);
$routes->add($route);
// 输出 http://www.foo.com/foo/bam
$url->action('foo@bar');
1 | $route = new Route(['GET'], 'foo/invoke', ['controller' => 'InvokableActionStub']); |
设置全局默认参数
1 | $url->defaults(['locale' => 'en']); |
设置全局命名空间
这样调用的时候,不用在 action 上省略这部分命名空间1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 设置全局命名空间
$url->setRootControllerNamespace('namespace');
// 配置添加路由
$route = new Route(['GET'], 'foo/bar', ['controller' => 'namespace\foo@bar']);
$routes->add($route);
$route = new Route(['GET'], 'foo/invoke', ['controller' => 'namespace\InvokableActionStub']);
$routes->add($route);
// 输出 http://www.foo.com/foo/bar; action 的值省略 namespace 这个命名空间
$url->action('foo@bar');
// 输出 http://www.foo.com/foo/invoke; action 的值省略 namespace 这个命名空间
$url->action('InvokableActionStub');
// 配置添加路由
$route = new Route(['GET'], 'something/else', ['controller' => 'something\foo@bar']);
$routes->add($route);
// 输出 http://www.foo.com/something/else; action 的最前面加了 `\`,全局命名空间下调用
$url->action('\something\foo@bar');
跳转器
跳转器内部提供了以下跳转;
home
通过调用app('redirect')->home()会跳转至根目录下\;1
public function home($status = 302)
back
通过调用app('redirect')->back()会跳转至上一次访问页面;或者全局帮助函数back()也可以;1
public function back($status = 302, $headers = [], $fallback = false)
第三个参数表示,如果没有前一次访问请求,访问哪个页面,具体源码如下:1
2
3
4
5
6
7if ($url) {
return $url;
} elseif ($fallback) {
return $this->to($fallback);
} else {
return $this->to('/');
}
refresh
通过调用app('redirect')->refresh()会刷新当前访问页面;1
public function refresh($status = 302, $headers = [])
to
通过调用app('redirect')->to('path')会跳转至指定路径页面;或者全局帮助函数redirect('path')也可以;
这里的 path 路径是不包含根目录的,例如(foo/bar);
1 | public function to($path, $status = 302, $headers = [], $secure = null) |
第四个参数表示是否使用https;
away
通过调用app('redirect')->away('path')会跳转至指定路径页面;
这里的 path 路径是包含根目录的,例如(http://xx.com/foo/bar);1
public function away($path, $status = 302, $headers = [])
secure
通过调用app('redirect')->secure('path')会跳转至指定路径页面;这里的path路径是不包含根目录的;1
public function secure($path, $status = 302, $headers = [])
其本质是调用了to方法1
return $this->to($path, $status, $headers, true);
route
通过调用app('redirect')->route('route_as_name'),根据路由的as名会跳转至与路由一致的url路径页;1
public function route($route, $parameters = [], $status = 302, $headers = [])
action
通过调用app('redirect')->action('route_action'),根据路由的action名会跳转至与路由一致的url路径页;1
public function action($action, $parameters = [], $status = 302, $headers = [])
guest
跳到指定的路径页的同时,将当前url存放至session中,键名为url.intended;1
public function guest($path, $status = 302, $headers = [], $secure = null)
intended
跳转至session中键名为url.intended的值所对应的Url;如果不存在,则跳转至第一个参数所传的值;1
public function intended($default = '/', $status = 302, $headers = [], $secure = null)
响应工厂(ResponseFactory)
ResponseFactory文件提供了两部分 API,分别是与响应类型相关和与跳转相关;
响应
response()会返回ResponseFactory实例;
视图响应1
response()->view('hello', $data, 200);
Jsop响应1
response()->json(['name' => 'Abigail', 'state' => 'CA']);
Jsonp响应1
response()->json(['name' => 'Abigail', 'state' => 'CA'])->withCallback($request->input('callback'));
文件响应
直接在浏览器显示文件,而不是下载,例如图片或PDF;file方法第一参数为文件路径,第二参数选填为头信息数组;1
file($pathToFile, $headers);
文件下载download方法第一参数为文件路径,第二参数选填为文件名,第三参数选填为头信息数组;1
return response()->download($pathToFile, $name, $headers);
跳转
这里的跳转方法,其实调用的还是跳转器中的方法,不过是在暴露更多的接口,方便调用与使用;
| 方法名 | 调用 | 实际调用的是跳转器中的哪个方法 |
|---|---|---|
redirectTo |
response()->redirectTo(…) | to方法 |
redirectToRoute |
response()->redirectToRoute(…) | route方法 |
redirectToAction |
response()->redirectToAction(…) | action方法 |
redirectGuest |
response()->redirectGuest(…) | guest方法 |
redirectToIntended |
response()->redirectToIntended(…) | intended方法 |