分页
介绍
在其他框架中,分页往往是一件非常繁琐的事情。我们希望 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 方法会根据用户当前浏览的页码,自动处理查询的 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
{
/**
* 显示所有应用用户
*/
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 子句来生成查询,但光标分页(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);
获取光标分页实例后,你可以像使用 paginate 和 simplePaginate 方法一样显示分页结果。有关光标分页实例提供的更多方法,请参考光标分页实例方法文档。
光标分页 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 分页具有以下优势:
- 对于大型数据集,如果排序列有索引,光标分页性能更佳。这是因为 offset 子句需要扫描之前匹配的所有数据。
- 对于频繁写入的数据集,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方法
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 会包含一些元信息,例如 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":[
{
// 记录...
},
{
// 记录...
}
]
}
自定义分页视图
默认情况下,分页器渲染分页链接时使用的视图与 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 方法中调用分页器的 defaultView 和 defaultSimpleView 方法:
<?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 方法中调用分页器的 useBootstrapFour 或 useBootstrapFive 方法:
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。 |