为多个Livewire 3组件构建可复用的排序与分页逻辑
我太懂你这种痛点了——写完一个带排序分页的食谱列表组件后,又要做点赞、收藏的同款列表,重复写逻辑不仅麻烦,后续改需求时要改好几处,简直是维护噩梦。刚好在Livewire里有非常优雅的解决方式,我给你分享一套实际项目里在用的方案:
核心思路:用PHP Trait抽离通用逻辑
Trait是PHP里复用代码的绝佳工具,尤其适合这种多个组件需要相同功能,但又不想被单继承限制灵活性的场景。我们把排序、分页的通用逻辑封装成一个Trait,所有需要的组件直接引入就能用。
第一步:创建复用的Trait文件
先在app/Http/Livewire/Traits目录下新建WithSortingAndPagination.php(没有目录就自己建一个),把通用的属性和方法都放进去:
<?php namespace App\Http\Livewire\Traits; use Livewire\WithPagination; trait WithSortingAndPagination { // 先引入Livewire自带的分页Trait use WithPagination; // 默认排序字段,可根据需求修改 public $sortField = 'created_at'; // 默认排序方向 public $sortDirection = 'desc'; // 默认每页显示数量 public $perPage = 10; // 切换排序方向的方法,点击表头时调用 public function sortBy($field) { if ($this->sortField === $field) { // 点击的是当前排序字段,切换排序方向 $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { // 点击的是新字段,默认降序排列 $this->sortDirection = 'desc'; $this->sortField = $field; } } // 封装排序逻辑,给查询构建器调用 public function applySorting($query) { return $query->orderBy($this->sortField, $this->sortDirection); } }
第二步:改造原有的RecipeList组件
现在把RecipeList.php里的排序分页逻辑删掉,直接引入我们的Trait,代码瞬间清爽很多:
<?php namespace App\Http\Livewire; use App\Http\Livewire\Traits\WithSortingAndPagination; use Livewire\Component; use App\Models\Recipe; class RecipeList extends Component { // 引入Trait,直接拥有排序分页功能 use WithSortingAndPagination; // 保留你原来的筛选参数 public $category = ''; public $search = ''; public function render() { $recipes = Recipe::query() ->when($this->category, fn($q) => $q->where('category', $this->category)) ->when($this->search, fn($q) => $q->where('title', 'like', "%{$this->search}%")) // 调用Trait里的排序方法 ->tap([$this, 'applySorting']) // 直接用Trait里的perPage属性分页 ->paginate($this->perPage); return view('livewire.recipe-list', compact('recipes')); } }
第三步:快速创建新的Liked/Saved食谱列表组件
现在搞点赞和收藏的列表组件就太简单了,比如LikedRecipeList.php:
<?php namespace App\Http\Livewire; use App\Http\Livewire\Traits\WithSortingAndPagination; use Livewire\Component; use App\Models\Recipe; class LikedRecipeList extends Component { // 同样引入Trait,直接拥有排序分页功能 use WithSortingAndPagination; public $user; public function mount() { $this->user = auth()->user(); } public function render() { $likedRecipes = $this->user->likedRecipes() // 调用Trait的排序方法 ->tap([$this, 'applySorting']) // 直接分页 ->paginate($this->perPage); return view('livewire.liked-recipe-list', compact('likedRecipes')); } }
SavedRecipeList的写法几乎一模一样,只要把关联关系改成savedRecipes()就行,完全不用重复写排序分页的代码。
第四步:复用视图里的排序分页UI
光后端逻辑复用还不够,前端的按钮、分页控件也可以抽成Blade组件复用。比如创建resources/views/components/sort-pagination-controls.blade.php:
<div class="flex justify-between items-center mb-4"> <!-- 排序按钮组 --> <div class="space-x-2"> <button wire:click="sortBy('title')" class="px-3 py-1 rounded {{ $sortField === 'title' ? 'bg-blue-500 text-white' : 'bg-gray-100' }}"> 标题 {{ $sortField === 'title' ? ($sortDirection === 'asc' ? '↑' : '↓') : '' }} </button> <button wire:click="sortBy('created_at')" class="px-3 py-1 rounded {{ $sortField === 'created_at' ? 'bg-blue-500 text-white' : 'bg-gray-100' }}"> 创建时间 {{ $sortField === 'created_at' ? ($sortDirection === 'asc' ? '↑' : '↓') : '' }} </button> </div> <!-- 每页数量选择 --> <div> <select wire:model="perPage" class="px-3 py-1 border rounded"> <option value="10">10条/页</option> <option value="20">20条/页</option> <option value="50">50条/页</option> </select> </div> </div> <!-- Livewire分页组件 --> <div class="mt-6"> {{ $items->links() }} </div>
然后在各个列表视图里直接引入这个组件就行,比如livewire/recipe-list.blade.php:
<div> <!-- 你的筛选表单,保留原来的逻辑 --> <div class="mb-4"> <input wire:model.live="search" placeholder="搜索食谱..." class="px-4 py-2 border rounded"> <select wire:model.live="category" class="px-4 py-2 border rounded ml-2"> <option value="">全部分类</option> <!-- 你的分类选项 --> </select> </div> <!-- 复用排序分页控件 --> <x-sort-pagination-controls :items="$recipes" /> <!-- 食谱列表内容 --> <div class="grid grid-cols-3 gap-4"> @foreach($recipes as $recipe) <div class="border rounded p-4"> <h3 class="font-bold">{{ $recipe->title }}</h3> <!-- 其他食谱信息 --> </div> @endforeach </div> <!-- 底部再放一次分页控件(可选) --> <x-sort-pagination-controls :items="$recipes" /> </div>
这样不管是哪个列表页面,UI风格统一,还不用重复写HTML代码。
为什么选Trait而不是父类?
相比用父类继承,Trait的灵活性更高——PHP只支持单继承,如果你以后还要加别的通用逻辑(比如通用筛选),可以再抽成另一个Trait,组件可以同时引入多个,完全不会有继承冲突的问题。而且如果某个组件需要特殊定制排序逻辑,直接在组件里重写Trait里的方法就行,非常方便。
内容来源于stack exchange

