专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  北京大学出的第二份 DeepSeek ... ·  17 小时前  
码农翻身  ·  中国的大模型怎么突然间就领先了? ·  昨天  
码农翻身  ·  漫画 | 为什么大家都愿意进入外企? ·  2 天前  
程序员的那些事  ·  OpenAI ... ·  2 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

Laravel 大将之路由模块

SegmentFault思否  · 公众号  · 程序员  · 2017-12-01 08:00

正文

本文是基于 Laravel 5.4 版本的路由模块代码进行分析书写。

模块组成

下图展示了路由模块中各个文件的关系,并进行简要说明。

剖析

服务提供者

Laravel 模块,首先找 ServiceProvider 文件,这是模块与 IOC 容器交互的入口,从这个文件,可以看出该模块提供向系统提供了哪些服务;

  1. public function register() {

  2.    // 注册路由管理,提供路由注册,路由匹配的功能

  3.    $this->registerRouter();

  4.    // 注册 Url 生成器实例

  5.    $this->registerUrlGenerator();

  6.    // 注册跳转器

  7.    $this->registerRedirector();

  8.    // 绑定 PSR-7 请求实现到 ServerRequestInterface 接口

  9.    $this->registerPsrRequest();

  10.    // 绑定 PSR-7 Response 实现到 ResponseInterface 接口

  11.    $this->registerPsrResponse();

  12.    // 注册 ReponseFactory,提供各式各样的 Response,比如视图响应、Json响应、Jsonp响应、文件下载等

  13.    $this ->registerResponseFactory();

  14. }

路由管理

“路由管理”服务有以下元素需要了解:

  • 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. public function map(Router $router){

  2.    require __DIR__.'/routes.php';

  3. }

这时候,项目就会执行很多 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. public function dispatch(Request $request) {

  2.    $this->currentRequest = $request;

  3.    return $this->dispatchToRoute($request);

  4. }

  5. public function dispatchToRoute(Request $request) {

  6.    // 根据请求的 url 找到匹配的路由

  7.    $route = $this->findRoute($request);

  8.    // 将路由绑定到请求上

  9.    $request->setRouteResolver(function () use ($route) {

  10.        return $route;

  11.    }

  12.    // 触发 RouteMatched 事件

  13.    $this->events->dispatch(new Events\RouteMatched($route, $request));

  14.    // 通过 Pipeline 流水线执行路由上绑定的中间件及对应的方法

  15.    $response = $this->runRouteWithinStack($route, $request);

  16.    // 根据 request 请求设置 response 的响应头

  17.    return $this->prepareResponse($request, $response);

  18. }

  1. 根据请求找匹配的路由 RouteCollection 根据请求的 http 动作缩小要匹配的路由范围;在筛选出来的这些路由中依次遍历,找出第一个符合验证的路由(需要进行较验的验证在 Route 中的 getValidators 方法中声明);

  2. 将路由绑定到请求上

  3. 触发 RouteMatched 事件 初始化的 Laravel 项目没有对 RouteMatched 路由匹配事件进行任何的监听器绑定,如有需要,可以自定义监听器,在模块的 EventServiceProvider 中注册该事件监听;这样一旦请求匹配上某个路由,就可以执行自定义方法了;

  4. 通过 Pipeline 流水线执行路由上绑定的中间件及对应的方法 runRouteWithinStack 方法中,系统会判断是否需要执行中间件,如果 IOC 容器中设置了 middleware . disable 的值为 true ,则需要执行的中间件数组为空;否则会找到所有的中间件,并按照 middlewarePriority 对必要的一些中间件进行排序调整;然后执行 $route -> run () 方法;

  5. 根据 request 请求设置 response 的响应头

项目中会用到的一些方法

  1. 获取路由集合 app ( 'router' )-> getRoutes ()

  2. 获取当前的请求 $request = app ( 'router' )-> getCurrentRequest ()

  3. 获取当前请求所对应的路由 $route = $request -> route () $route = app ( 'router' )-> getCurrentRoute ()

  4. 获取当前路由需要执行的中间件 $middlewares = app ( 'router' )-> gatherRouteMiddleware ( $route )

Url 生成器

Url 生成器是什么?举个例子:

  1. $url = new UrlGenerator(

  2.    $routes = new RouteCollection,

  3.    $request = Request::create('http://www.foo.com/')

  4. );

  5. $url->to('foo/bar'); // 输出 http://www.foo.com/foo/bar

像这种基于当前请求,生成指定路径的 Url

这部分功能由两个文件完成,一个是 UrlGenerator . php ,另一个是 RouteUrlGenerator . php UrlGenerator . php 处理根据路径名生成 Url RouteUrlGenerator . php 处理根据路由生成 Url

列一些常用的使用:

根据路径名生成

使用 to 方法,第一个参数为路径,第二个参数是数组, implode 后会接着路径名,第三个参数决定用不用 https

  1. // 路径名是 foo/bar,当前请求的根路径为 http://www.foo.com,所以输出是 http://www.foo.com/foo/bar

  2. $url->to('foo/bar')

  3. // 路径名是 foo/bar,当前请求的根路径为 http://www.foo.com,第三个参数决定 scheme 是 https,所以输出是 https://www.foo.com/foo/bar

  4. $url->to('foo/bar', [], true)

  5. // 路径名是 foo/bar,第二个参数 是补充路径名,implode 后是 /baz/boom

  6. // 第三个参数决定 scheme 是 https,所以输出是 https://www.foo.com/foo/bar/baz/boom

  7. $url ->to('foo/bar', ['baz', 'boom'], true)

  8. // 路径名是 foo/bar,查询参数是 ?foo=bar ,补充路径是 /baz,所以输出是 https://www.foo.com/foo/bar/baz?foo=bar

  9. $url->to('foo/bar?foo=bar', ['baz'], true)

根据路由的 as 名生成

使用 route 方法,第一个参数为指定路由的 as 名,第二个参数是参数数组,第三个参数决定是否显示根目录(默认为 true)

  1. $route = new Route(['GET'], 'foo/bar', ['as' => 'foo']);

  2. $routes->add($route);

  3. // 输出 'http://www.foo.com/foo/bar

  4. $url->route('foo');

  5. // 第三个参数为 false,表示不显示根目录,于是输出 /foo/bar

  6. $url->route('foo', [], false)

  7. // 路由中的 url 本身不带参数,则第二参数中所有关联数组都将作为查询参数

  8. // 输出 /foo/bar?foo=bar

  9. $url->route('foo', ['foo' => 'bar'], false)

  1. $route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}', ['as' => 'bar']);

  2. $routes->add($route);

  3. // 路由上的 url 带参数,根据参数名找值;剩余多余的为查询参数;

  4. // 输出 http://www.foo.com/foo/bar/otwell/breeze/taylor?fly=wall

  5. $url->route( 'bar', ['boom' => 'taylor', 'baz' => 'otwell', 'fly' => 'wall']);

  6. // 路由上的 url 带参数,找不到对应的参数值,则按顺序作值;剩余多余的为查询参数;

  7. // 输出 http://www.foo.com/foo/bar/taylor/breeze/otwell?fly=wall

  8. $url->route('bar', ['taylor', 'otwell', 'fly' => 'wall']);

根据路由的 action 名生成

使用 action 方法,第一个参数为指定路由的 action 名,第二个参数是参数数组,第三个参数决定是否显示根目录(默认为 true)

  1. $route = new Route(['GET'], 'foo/bam', ['controller' => 'foo@bar']);

  2. $routes->add($route);

  3. // 输出 http://www.foo.com/foo/bam

  4. $url ->action('foo@bar');

  1. $route = new Route(['GET'], 'foo/invoke', ['controller' => 'InvokableActionStub']);

  2. $routes->add($route);

  3. // 输出 http://www.foo.com/foo/invoke

  4. $url->action('InvokableActionStub');

设置全局默认参数
  1. $url->defaults(['locale' => 'en']);

  2. $route = new Route(['GET'], 'foo', ['as' => 'defaults', 'domain' => '{locale}.example.com', function() {}]);

  3. // 路由 url 有参数,但没有传参数值,则会找全局默认参数值;输出 http://en.example.com/foo

  4. $url->route('defaults');

设置全局命名空间

这样调用的时候,不用在 action 上省略这部分命名空间

  1. // 设置全局命名空间

  2. $url->setRootControllerNamespace('namespace');

  3. // 配置添加路由

  4. $route = new Route(['GET'], 'foo/bar', ['controller' => 'namespace\foo@bar']);

  5. $routes->add($route);

  6. $route = new Route(['GET'], 'foo/invoke' , ['controller' => 'namespace\InvokableActionStub']);

  7. $routes->add($route);

  8. // 输出 http://www.foo.com/foo/bar;  action 的值省略 namespace 这个命名空间

  9. $url->action ('foo@bar');

  10. // 输出 http://www.foo.com/foo/invoke;  action 的值省略 namespace 这个命名空间

  11. $url->action('InvokableActionStub');

  12. // 配置添加路由

  13. $route = new Route(['GET'], 'something/else', ['controller' => 'something\foo@bar']);

  14. $routes->add($route);

  15. // 输出  http://www.foo.com/something/else; action 的最前面加了 `\`,全局命名空间下调用

  16. $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. if ($url) {

  2.     return $url;

  3. } elseif ($fallback) {

  4.    return $this-> to($fallback);

  5. } else {

  6.    return $this->to('/');

  7. }

refresh 通过调用 app ( 'redirect' )->







请到「今天看啥」查看全文