Lzh on GitHub

介绍

在其他框架中,分页往往是一件非常繁琐的事情。我们希望 Laravel 的分页方式能带来一丝清新的体验。Laravel 的分页器与查询构建器和 Eloquent ORM 紧密集成,能够方便、轻松地对数据库记录进行分页,并且无需任何额外配置。

默认情况下,分页器生成的 HTML 与 Tailwind CSS 框架兼容;当然,也支持 Bootstrap 分页样式。

Tailwind

如果你使用 Laravel 默认的 Tailwind 分页视图,并且使用 Tailwind 4.x,那么你应用中的 resources/css/app.css 文件已经配置好,可直接引用 Laravel 的分页视图:

@import 'tailwindcss';

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

基本用法

对查询构建器结果进行分页

有多种方式可以对数据进行分页。最简单的方法是直接在查询构建器或 Eloquent 查询上使用 paginate 方法。paginate 方法会根据用户当前浏览的页码,自动处理查询的 limitoffset。默认情况下,当前页码会通过 HTTP 请求中的 page 查询字符串参数自动检测,Laravel 会自动识别该值,并将其自动插入分页器生成的链接中。

在下面的示例中,传递给 paginate 方法的唯一参数是希望每页显示的记录数。在此例中,我们指定每页显示 15 条记录:

<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * 显示所有应用用户
     */
    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 查询字符串参数来存储当前页码,就会产生冲突。为了解决这个问题,你可以通过 paginatesimplePaginatecursorPaginate 方法的第三个参数,指定用于存储当前页码的查询字符串参数名称:

use App\Models\User;

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

光标分页

虽然 paginatesimplePaginate 方法通过 SQL 的 offset 子句来生成查询,但光标分页(cursor pagination)是通过构建 where 子句来比较查询中已排序列的值,从而实现分页。这种方式在 Laravel 的所有分页方法中提供了最优的数据库性能。光标分页尤其适合大型数据集以及“无限滚动”的用户界面。

与基于 offset 的分页不同,后者会在分页器生成的 URL 查询字符串中包含页码,光标分页会在查询字符串中放置一个 cursor 字符串。该 cursor 是一个编码字符串,包含下一次分页查询的起始位置以及分页方向,例如:

http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0

你可以通过查询构建器提供的 cursorPaginate 方法创建光标分页实例。此方法返回 Illuminate\Pagination\CursorPaginator 的实例:

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

获取光标分页实例后,你可以像使用 paginatesimplePaginate 方法一样显示分页结果。有关光标分页实例提供的更多方法,请参考光标分页实例方法文档。

要使用光标分页,查询必须包含 order by 子句,并且排序的列必须属于你正在分页的表。

光标分页 vs. Offset 分页

为了说明 offset 分页和光标分页的区别,我们来看一些示例 SQL 查询。下面两个查询都会显示按 id 排序的 users 表的“第二页”结果:

# Offset 分页
select * from users order by id asc limit 15 offset 15;

# 光标分页
select * from users where id > 15 order by id asc limit 15;

光标分页相较于 offset 分页具有以下优势:

  1. 对于大型数据集,如果排序列有索引,光标分页性能更佳。这是因为 offset 子句需要扫描之前匹配的所有数据。
  2. 对于频繁写入的数据集,offset 分页可能会跳过记录或显示重复记录,尤其当当前页面的内容被新增或删除时。

然而,光标分页也有以下限制:

  1. simplePaginate 类似,光标分页只能用于显示 “上一页” 和 “下一页” 链接,不支持生成带页码的链接。
  2. 分页必须基于至少一列唯一字段,或唯一列的组合。含有 null 值的列不支持。
  3. order by 子句中的查询表达式仅在被别名化并添加到 select 子句中时才支持。
  4. 带参数的查询表达式不支持。

手动创建分页器

有时你可能希望手动创建一个分页实例,并传入一组已经在内存中的数据。这可以通过创建以下任意一种实例来实现:Illuminate\Pagination\PaginatorIlluminate\Pagination\LengthAwarePaginatorIlluminate\Pagination\CursorPaginator,具体取决于你的需求。

PaginatorCursorPaginator 类并不需要知道结果集的总记录数;但正因为如此,这些类没有获取最后一页索引的方法。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,可以这样调用:

use App\Models\User;

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

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

    // ...
});

如果你希望将当前请求的所有查询字符串参数追加到分页链接中,可以使用 withQueryString 方法:

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

追加哈希片段

如果你需要在分页器生成的 URL 中追加 “哈希片段”,可以使用 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 方法会渲染结果集中其余页面的链接,每个链接都会自动包含正确的页码查询字符串变量。需要注意的是,links 方法生成的 HTML 与 Tailwind CSS 框架兼容。

调整分页链接窗口

当分页器显示分页链接时,会显示当前页码,以及当前页前后各三页的链接。你可以使用 onEachSide 方法来控制在分页器生成的中间滑动窗口中,当前页两侧额外显示的页码数量:

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

在这个例子中,当前页的左右各会显示 5 个额外的页码链接。

将结果转换为 JSON

Laravel 的分页器类实现了 Illuminate\Contracts\Support\Jsonable 接口,并提供了 toJson 方法,因此将分页结果转换为 JSON 非常简单。你也可以通过在路由或控制器方法中直接返回分页器实例来生成 JSON:

use App\Models\User;

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

分页器生成的 JSON 会包含一些元信息,例如 totalcurrent_pagelast_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":[
        {
            // 记录...
        },
        {
            // 记录...
        }
   ]
}

自定义分页视图

默认情况下,分页器渲染分页链接时使用的视图与 Tailwind CSS 框架兼容。但如果你没有使用 Tailwind,也可以自由定义自己的视图来渲染分页链接。在调用分页器实例的 links 方法时,可以将视图名称作为第一个参数传入:

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

<!-- 向视图传递额外数据... -->
{{ $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 方法中调用分页器的 defaultViewdefaultSimpleView 方法:

<?php

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    /**
     * 启动应用服务
     */
    public function boot(): void
    {
        Paginator::defaultView('view-name');

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

使用 Bootstrap

Laravel 提供了基于 Bootstrap CSS 构建的分页视图。如果你希望使用这些视图而不是默认的 Tailwind 视图,可以在 App\Providers\AppServiceProvider 类的 boot 方法中调用分页器的 useBootstrapFouruseBootstrapFive 方法:

use Illuminate\Pagination\Paginator;

/**
 * 启动应用服务
 */
public function boot(): void
{
    Paginator::useBootstrapFive();
    Paginator::useBootstrapFour();
}

这样,分页器生成的分页链接就会使用 Bootstrap 样式,而非默认的 Tailwind 样式。

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。