Laravel 项目中搭载 Turbolinks

分享 menglr ⋅ 于 3年前 ⋅ 最后回复由 kelby 2年前 ⋅ 5617 阅读

TurbolinksRuby on Rails 的前端组件,它让页面的切换更加流畅。在 Turbolinks 的作用下,每次请求对服务端来说是无区别的,但客户端在 Turbolinks 启动之后就变成一个单页应用,JavaScript 和 CSS 都不会重复编译,同时只对 body 和 title 做对应的替换。

Laravel 在很多方面借鉴了 Rails,尽管语言不同,但在对 Turbolinks 做适配时需要处理的逻辑是类似的。

按照我的想法,在 Laravel 中需要有一个 Turbo 类,可以在前置过滤器 App::before(); 和后置过滤器 App::after(); 中做处理。

期望在 app/filters.php 中可以这样子

<?php

App::before(function($request)
{
    Turbo::setUp();
});

App::after(function($request, $response)
{
    Turbo::handle($request, $response);
});

那么,创建一个 Turbo 类,例如 Acme\Utils\Turbo.php

<?php namespace Acme\Utils;

class Turbo {}

同时, 需要利用 Laravel 提供的 Facade 并且绑定到 $app

例如, app/Acme/Facades/Turbo.php

<?php namespace Acme\Facades;

use Illuminate\Support\Facades\Facade;

class Turbo extends Facade {

    protected static function getFacadeAccessor()
    {
        return 'turbo';
    }
}

接着, 在 app/Acme/Providers/TurboServiceProvider.php

<?php namespace Acme\Providers;

use Illuminate\Support\ServiceProvider;

class TurboServiceProvider extends ServiceProvider {

    protected $defer = true;

    public function register()
    {
        $this->app->bindShared('turbo', function()
        {
            return new \Acme\Utils\Turbo;
        });
    }

    public function provides()
    {
        return ['turbo'];
    }
}

还需要在 app/config/app.php 中添加对应的 providersaliases

到目前为止, 使用 Turbo::fire(); 时, 就会调用对应的 fire 这个方法

可以使用 php artisan tinker 进行调试

Turbolinks 在前置过滤器主要做一件事, 设置 request_method

<?php

class Turbo {

    public function setUp($request = null)
    {
        $request = $request ?: Request::instance();
        $this->setRequestMethodCookie($request->getMethod());
    }

    private function setRequestMethodCookie($method)
    {
        Cookie::queue('request_method', $method);
    }
}

这样是为了告诉客户端, 也就是浏览器, 当前的请求的方法属于什么类型, Turbolinks 只对 GET 请求作处理而无视其它。也就是说,表单提交并不会被 Turbolinks 处理,除非声明了 GET 类型的提交

后置过滤器部分, 主要是适配 HTTP Redirection

我参考了 https://github.com/rails/turbolinks/tree/master/lib 中的 ruby 代码,为了方便阅读,以下是完整的 Turbo

<?php namespace Acme\Utils;

use Cookie;
use Request;
use Session;
use Illuminate\Http\RedirectResponse;

class Turbo {

    private $request;

    public function __construct() {}

    public function setUp($request = null)
    {
        $request = $request ?: Request::instance();
        $this->setRequestMethodCookie($request->getMethod());
    }

    public function handle($request, $response)
    {
        $this->request = $request;

        $this->setXhrRedirectedTo($response);

        if ($response instanceof RedirectResponse)
        {
            $this->storeForTurbolinks($response->getTargetUrl());
            $this->abortXdomainRedirect($response);
        }
    }

    public function redirectViaTurbolinksTo($url, $status = 200)
    {
        return Response::make(join($url, ["Turbolinks.visit('", "');"]), $status)->header('Content-Type', 'application/x-javascript');
    }

    private function setRequestMethodCookie($method)
    {
        Cookie::queue('request_method', $method);
    }

    private function setXhrRedirectedTo($response)
    {
        if (Session::has('_turbolinks_redirect_to'))
        {
            $response->header('X-XHR-Redirected-To', Session::pull('_turbolinks_redirect_to'));
        }
    }

    private function storeForTurbolinks($url)
    {
        if ($this->referer()) Session::put('_turbolinks_redirect_to', $url);
    }

    private function abortXdomainRedirect($response)
    {
        $current = $this->request->headers->get('X-XHR-Referer') ?: null;
        $next = $response->headers->get('Location') ?: null;

        if (! (is_null($current) or is_null($next) or $this->sameOrigin($current, $next)))
        {
            $response->setStatusCode(403);
        }
    }

    private function sameOrigin($current, $next) {
        return $this->getArray($current) == $this->getArray($next);
    }

    private function getArray($url)
    {
        return array_only(parse_url($url), ['scheme', 'host', 'port']);
    }

    private function referer()
    {
        return $this->request->headers->get('X-XHR-Referer') ?: $this->request->headers->get('Referer');
    }
}

除了 Turbolinks, Rails 中还搭载着 jQuery ujs, ujs 全称是 Unobtrusive JavaScript, 在 Laravel 中使用 ujs 只需要把 rails.js 加进去, 当然不要忘了 jQuery

同时, 需要在页面加入

<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="{{ csrf_token() }}">

ujs 的作用是可以更好地对服务端发送 RESTful 请求

例如, 在 Laravel 中, 当用户要注销的时候, 在 view 层可以这样子

HTML::link('logout', '注销', ['data-method'=>'delete'])

那么, 在点击链接的时候, 就不是发出 GET 请求而是 DELETE

app/routes.php 里可以这样子

<?php

Route::delete('logout', 'SessionController@destroy');

例如, 当用户使用点赞功能时, 需要 POST 请求

HTML::link('post/1/like', '赞', ['data-remote'=>'true', 'data-method'=>'post'])

那么, 在点击链接的时候, 请求的类型就是 POST, 同时是通过 ajax 发送

这样子的话, 如果响应回来的是 javascript 脚本, 并且 Content-Typeapplication/x-javascript, 客户端就会执行此脚本

通过 Turbo::redirectViaTurbolinksTo('/home') 返回的内容 Turbolinks.visit('/home'), 可以让客户端跳转到 /home

我并没有使用 Backbone.js 或者 AngularJS 的经验, 所以, Turbolinks 对我来说是不错的选择

相关链接

原文 https://mozilabs.github.io/laravel/turbolinks-on-laravel.html 有错误的话, 希望大家指出

本文章首发在 Laravel China 社区
本帖已被设为精华帖!
回复数量: 4
  • Summer MOD A Life-long learner. 1
    3年前

    好文. 100个 :+1: , 准备给 phphub 用上 Turbolinks, 哈哈哈

  • Summer MOD A Life-long learner.
    3年前

    已推荐到首页.

  • lifesign Learn From Life
    3年前

    大赞 :thumbsup:

  • kelby Ruby/Rails 工程师,这是我之前写的书《Rails 4-2-stable 参考手册(Beta)...
    2年前

    赞!

    turbolinks 源代码里

    Ruby 层面做了:
    
    Controller
        3 个回调方法:设置 X-XHR-Redirected-To,只对 GET 请求有效,某种情况的跨域是不允许的
        重写 referer 方法
        更改 redirect_to 计算规则
        重写 redirect_to 方法,某种情况下使用 Turbolinks.visit 代替
        重写 render 方法,某种情况下使用 Turbolinks.replace 代替
        render 和 redirect_to 可以额外处理参数:turbolinks,keep,change,flush (各个参数对应功能可以查看文档)
    Router
        加上 _turbolinks_redirect_to
    View
        加上 X-XHR-Referer

    对应的楼主完成了 Controller 里的好几项功能,Router 和 View 目测并没有做设置。然后 turbolinks JS 的那部分可以直接使用,再加上 jQuery ujs ... 就可以用了,挺不错的。

    另外,原文已经 404 。。。站长,我点击“回复”报500了...幸好内容没丢

暂无评论~~
您需要登陆以后才能留下评论!

Composer 中国全量镜像

Top 100 扩展包

Lumen 中文文档

Laravel 速查表

Laravel 中文文档

Laravel 项目开发规范

Laravel 开发环境部署

社区文档撰写指南

TDD 构建 Laravel 论坛笔记

PHP PSR 标准规范

PHP 设计模式全集

Dingo API 中文文档