分页

简介

在其他框架里,分页可能相当让人头疼。希望 Laravel 处理分页的方式能让你眼前一亮。Laravel 的分页器与查询构造器和 Eloquent ORM 深度集成,零配置就能提供便捷、易用的数据库记录分页。

默认情况下,分页器生成的 HTML 与 Tailwind CSS 框架兼容;同时也提供 Bootstrap 分页支持。

Tailwind

如果你使用的是 Laravel 默认的 Tailwind 分页视图并搭配 Tailwind 4.x,那么应用的 resources/css/app.css 文件里应该已经正确配置了对 Laravel 分页视图的 @source 声明:

@import 'tailwindcss';

@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';

基本用法

对查询构造器结果分页

分页的方式有好几种,最简单的就是在查询构造器或 Eloquent 查询上调用 paginate() 方法。paginate() 会自动根据用户当前浏览的页码设置查询的「limit」和「offset」。当前页码默认从 HTTP 请求的 page 查询字符串参数中读取——这个值由 Laravel 自动检测,也会自动被写入到分页器生成的链接里。

下面这个例子中,传给 paginate() 方法的唯一参数就是「每页显示多少条」。这里我们指定每页显示 15 条:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\View\View;

class UserController extends Controller
{
    /**
     * Show all application users.
     */
    public function index(): View
    {
        return view('user.index', [
            'users' => DB::table('users')->paginate(15)
        ]);
    }
}

简单分页

paginate() 方法在从数据库取出记录之前,会先数一遍查询匹配的记录总数——这样分页器才能知道总共有多少页记录。可如果你不打算在应用 UI 上展示总页数,那这次统计查询就没必要跑。

因此,如果你只需要在应用 UI 中展示简单的「下一页」「上一页」链接,可以使用 simplePaginate() 方法,它只执行一次高效的查询:

$users = DB::table('users')->simplePaginate(15);

对 Eloquent 结果分页

你也可以对 Eloquent 查询做分页。下面这个例子里,我们对 App\Models\User 模型分页,指定每页 15 条。可以看到,语法与查询构造器分页几乎一致:

use App\Models\User;

$users = User::paginate(15);

当然,可以先在查询上设置其他约束(比如 where 子句),再调用 paginate():

$users = User::where('votes', '>', 100)->paginate(15);

对 Eloquent 模型也可以使用 simplePaginate() 方法:

$users = User::where('votes', '>', 100)->simplePaginate(15);

同样地,可以用 cursorPaginate() 方法对 Eloquent 模型做游标分页:

$users = User::where('votes', '>', 100)->cursorPaginate(15);

同一页内使用多个分页器实例

有时你可能需要在应用渲染的同一屏中显示两个独立的分页器。但如果两个分页器实例都用 page 查询字符串参数来存储当前页,就会互相冲突。要解决这个冲突,可以通过 paginate()、simplePaginate() 和 cursorPaginate() 方法的第三个参数,指定用于存储分页器当前页的查询字符串参数名:

use App\Models\User;

$users = User::where('votes', '>', 100)->paginate(
    $perPage = 15, $columns = ['*'], $pageName = 'users'
);

游标分页

paginate 和 simplePaginate 使用 SQL 的「offset」子句构建查询,而游标分页则通过构造「where」子句、比较查询中排序列的值来工作,是 Laravel 所有分页方式中性能最好的一种。这种分页方式特别适合大数据集和「无限滚动」用户界面。

与基于偏移量的分页在 URL 查询字符串中放页码不同,游标分页会在查询字符串里放一个「游标」字符串。这个游标是一个编码后的字符串,包含下一次分页查询应该从哪里开始,以及分页方向:

http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0

你可以通过查询构造器提供的 cursorPaginate() 方法创建游标分页器实例。该方法返回一个 Illuminate\Pagination\CursorPaginator 实例:

$users = DB::table('users')->orderBy('id')->cursorPaginate(15);

拿到游标分页器实例后,就可以像使用 paginate() 和 simplePaginate() 时那样展示分页结果。关于游标分页器提供的实例方法的更多信息,请查阅游标分页器实例方法文档。

注意

你的查询必须包含「order by」子句才能享受游标分页。此外,用于排序的列必须属于你正在分页的那张表。

游标分页 vs. 偏移分页

为了说明偏移分页与游标分页的区别,我们来看几条示例 SQL。下面两条查询都会展示按 id 排序的 users 表的「第二页」结果:

# Offset Pagination...
select * from users order by id asc limit 15 offset 15;

# Cursor Pagination...
select * from users where id > 15 order by id asc limit 15;

相较于偏移分页,游标分页查询有下列优势:

  • 对于大数据集,如果「order by」用到的列有索引,游标分页会有更好的性能。这是因为「offset」子句会扫过所有先前已匹配的数据。
  • 对于写入频繁的数据集,如果用户当前浏览页里有记录被新增或删除,偏移分页可能会漏掉记录或出现重复。

不过,游标分页有下列限制:

  • 与 simplePaginate 一样,游标分页只能展示「下一页」和「上一页」链接,不支持生成带页码的链接。
  • 排序必须基于至少一个唯一列或一组组合起来唯一的列。不支持包含 null 值的列。
  • 「order by」子句中只有当查询表达式使用别名并同时加入「select」子句时才被支持。
  • 不支持带参数的查询表达式。

手动创建分页器

有时你希望手动创建一个分页器实例,把你已经在内存里的项目数组传进去。你可以根据需求分别创建 Illuminate\Pagination\Paginator、Illuminate\Pagination\LengthAwarePaginator 或 Illuminate\Pagination\CursorPaginator 实例。

Paginator 和 CursorPaginator 类不需要知道结果集里的记录总数;但也正因如此,这两个类没有取「最后一页页码」的方法。LengthAwarePaginator 接收的参数几乎和 Paginator 相同,但它要求提供结果集的记录总数。

换句话说,Paginator 对应查询构造器的 simplePaginate() 方法,CursorPaginator 对应 cursorPaginate() 方法,LengthAwarePaginator 对应 paginate() 方法。

注意

手动创建分页器实例时,你需要手动对传给分页器的结果数组做「切片」。如果不清楚怎么做,请查看 PHP 的 array_slice 函数。

自定义分页 URL

默认情况下,分页器生成的链接会匹配当前请求的 URI。但分页器的 withPath() 方法允许你自定义生成链接时使用的 URI。举例来说,如果你希望分页器生成 http://example.com/admin/users?page=N 这样的链接,就把 /admin/users 传给 withPath():

use App\Models\User;

Route::get('/users', function () {
    $users = User::paginate(15);

    $users->withPath('/admin/users');

    // ...
});

追加查询字符串参数

你可以使用 appends() 方法向分页链接的查询字符串追加参数。比如要在每个分页链接后追加 sort=votes,就这样调用 appends():

use App\Models\User;

Route::get('/users', function () {
    $users = User::paginate(15);

    $users->appends(['sort' => 'votes']);

    // ...
});

如果你想把当前请求里所有查询字符串参数都追加到分页链接上,可以使用 withQueryString() 方法:

$users = User::paginate(15)->withQueryString();

追加 Hash 片段

如果你需要给分页器生成的 URL 追加「hash 片段」,可以使用 fragment() 方法。比如要在每个分页链接末尾追加 #users,可以这样调用:

$users = User::paginate(15)->fragment('users');

展示分页结果

调用 paginate() 方法时,你会拿到一个 Illuminate\Pagination\LengthAwarePaginator 实例;调用 simplePaginate() 方法则会返回一个 Illuminate\Pagination\Paginator 实例;最后,调用 cursorPaginate() 方法会返回一个 Illuminate\Pagination\CursorPaginator 实例。

这些对象提供了若干描述结果集的方法。除了这些辅助方法外,分页器实例本身也是迭代器,可以像数组一样循环。所以,取到结果后,就可以用 Blade 展示结果并渲染页面链接:

<div class="container">
    @foreach ($users as $user)
        {{ $user->name }}
    @endforeach
</div>

{{ $users->links() }}

links() 方法会渲染出通往结果集其余页的链接。每个链接都会自动带上正确的 page 查询字符串。别忘了,links() 方法生成的 HTML 与 Tailwind CSS 框架兼容。

分页器展示分页链接时,会显示当前页码,以及当前页前后各三页的链接。使用 onEachSide() 方法,你可以控制分页器生成的中间滑动窗口里当前页两侧额外显示多少个链接:

{{ $users->onEachSide(5)->links() }}

把结果转成 JSON

Laravel 的分页器类实现了 Illuminate\Contracts\Support\Jsonable 接口契约并暴露了 toJson() 方法,因此把分页结果转成 JSON 非常简单。你也可以直接在路由或控制器动作里返回分页器实例,Laravel 会自动把它转成 JSON:

use App\Models\User;

Route::get('/users', function () {
    return User::paginate();
});

分页器输出的 JSON 会包含 total、current_page、last_page 等元信息。结果记录放在 JSON 数组的 data 键下。下面是从路由直接返回分页器实例后生成的 JSON 示例:

{
   "total": 50,
   "per_page": 15,
   "current_page": 1,
   "last_page": 4,
   "current_page_url": "http://laravel.app?page=1",
   "first_page_url": "http://laravel.app?page=1",
   "last_page_url": "http://laravel.app?page=4",
   "next_page_url": "http://laravel.app?page=2",
   "prev_page_url": null,
   "path": "http://laravel.app",
   "from": 1,
   "to": 15,
   "data":[
        {
            // Record...
        },
        {
            // Record...
        }
   ]
}

自定义分页视图

默认情况下,用于渲染分页链接的视图与 Tailwind CSS 框架兼容。如果你没用 Tailwind,也可以自由定义自己的视图来渲染这些链接。在分页器实例上调用 links() 方法时,可以把视图名作为第一个参数传进去:

{{ $paginator->links('view.name') }}

<!-- Passing additional data to the view... -->
{{ $paginator->links('view.name', ['foo' => 'bar']) }}

不过最方便的定制方式是通过 vendor:publish 命令,把分页视图发布到 resources/views/vendor 目录中:

php artisan vendor:publish --tag=laravel-pagination

这条命令会把视图放到应用的 resources/views/vendor/pagination 目录里。目录里的 tailwind.blade.php 文件对应的就是默认分页视图,你可以编辑这个文件来修改分页 HTML。

如果你想指定另一个文件作为默认分页视图,可以在 App\Providers\AppServiceProvider 类的 boot() 方法里调用分页器的 defaultView() 和 defaultSimpleView() 方法:

<?php

namespace App\Providers;

use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Paginator::defaultView('view-name');

        Paginator::defaultSimpleView('view-name');
    }
}

使用 Bootstrap

Laravel 也内置了基于 Bootstrap CSS 构建的分页视图。要用这些视图替代默认的 Tailwind 视图,可以在 App\Providers\AppServiceProvider 类的 boot() 方法里调用分页器的 useBootstrapFour() 或 useBootstrapFive() 方法:

use Illuminate\Pagination\Paginator;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Paginator::useBootstrapFive();
    Paginator::useBootstrapFour();
}

Paginator / LengthAwarePaginator 实例方法

每个分页器实例都通过以下方法暴露额外的分页信息:

方法 说明
$paginator->count() 获取当前页的记录数。
$paginator->currentPage() 获取当前页码。
$paginator->firstItem() 获取当前结果中第一条记录的序号。
$paginator->getOptions() 获取分页器选项。
$paginator->getUrlRange($start, $end) 生成一段分页 URL 范围。
$paginator->hasPages() 判断是否有足够的记录可以拆成多页。
$paginator->hasMorePages() 判断数据存储中是否还有更多记录。
$paginator->items() 获取当前页的记录。
$paginator->lastItem() 获取当前结果中最后一条记录的序号。
$paginator->lastPage() 获取最后一页的页码(使用 simplePaginate 时不可用)。
$paginator->nextPageUrl() 获取下一页的 URL。
$paginator->onFirstPage() 判断分页器是否在第一页。
$paginator->onLastPage() 判断分页器是否在最后一页。
$paginator->perPage() 每页显示的记录数。
$paginator->previousPageUrl() 获取上一页的 URL。
$paginator->total() 数据存储中匹配到的记录总数(使用 simplePaginate 时不可用)。
$paginator->url($page) 获取指定页码的 URL。
$paginator->getPageName() 获取用于存储页码的查询字符串变量名。
$paginator->setPageName($name) 设置用于存储页码的查询字符串变量名。
$paginator->through($callback) 通过回调转换每一条记录。

Cursor Paginator 实例方法

每个游标分页器实例都通过以下方法暴露额外的分页信息:

方法 说明
$paginator->count() 获取当前页的记录数。
$paginator->cursor() 获取当前的游标实例。
$paginator->getOptions() 获取分页器选项。
$paginator->hasPages() 判断是否有足够的记录可以拆成多页。
$paginator->hasMorePages() 判断数据存储中是否还有更多记录。
$paginator->getCursorName() 获取用于存储游标的查询字符串变量名。
$paginator->items() 获取当前页的记录。
$paginator->nextCursor() 获取下一组记录对应的游标实例。
$paginator->nextPageUrl() 获取下一页的 URL。
$paginator->onFirstPage() 判断分页器是否在第一页。
$paginator->onLastPage() 判断分页器是否在最后一页。
$paginator->perPage() 每页显示的记录数。
$paginator->previousCursor() 获取上一组记录对应的游标实例。
$paginator->previousPageUrl() 获取上一页的 URL。
$paginator->setCursorName() 设置用于存储游标的查询字符串变量名。
$paginator->url($cursor) 获取指定游标实例对应的 URL。