通过 Livewire 在 Laravel 项目中实现基于 PHP 的全栈开发(三):数据绑定及底层实现


上篇教程我们介绍了 Livewire 组件创建和渲染的底层实现逻辑,这篇教程我们来演示下数据绑定的使用示例。和 Vue 组件类似,数据绑定的作用主要是在不刷新页面的情况下实现视图中某些区块数据的动态更新,而且这个绑定是双向的:后端数据修改后可以即时渲染到视图模板,在视图模板中修改数据后也可以将其更新到后端绑定变量。

双向绑定

通过上篇教程的分析我们已经知道,Livewire 组件的数据绑定是通过在服务端组件类中定义 public 属性来实现的,之前在快速上手教程中演示的是单向数据绑定,这里我们来创建一个新组件演示双向绑定:

php artisan make:livewire my-name

然后我们在视图文件 livewire/my-name.blade.php 中编写视图模板代码如下:

<div>
    <input type="text" wire:model="name">
    <hr>
    你好,我的名字是:{{ $name }}
</div>

在这段代码中,我们通过 wire:model 属性定义要双向绑定的数据,对应的变量名为 name,如果服务端返回了这个变量,则将其值显示在输入框中,反之,如果输入框修改了这个数据,也会将其更新到服务端,然后将结果渲染到视图模板中来。

接下来,我们修改服务端组件类 Http/Livewire/MyName.php

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class MyName extends Component
{
    public $name;

    public function mount()
    {
        $this->name = '学院君';
    }

    public function render()
    {
        return view('livewire.my-name');
    }
}

在服务端,我们在 MyName 组件类中定义了一个 $name 属性用于实现数据绑定,此外,我们还通过 mount() 方法初始化了该属性的值,mount 方法我们在上篇教程中提到过,类似 Vue 组件的 created 钩子函数,我们可以在这里对组件数据进行一些初始化操作。

要在前端页面最终渲染这个 Livewire 组件,还需要创建一个引入该组件的视图文件,我们在 resources/views 目录下创建 livewire.blade.php,然后在其中引入 my-name 组件:

@extends("layouts.app")

@section('content')
    @livewire('my-name')
@endsection

最后在路由文件 routes/web.php 中注册测试路由 /livewire-demo

Route::get('/livewire-demo', function () {
    return view('livewire');
});

这样,我们就可以在浏览器中通过 /livewire-demo 路由看到渲染的结果页了:

如果我们修改输入框中的值,则会即时更新文本中的变量值,说明双向绑定功能正常:

目前 Livewire 的数据绑定功能支持以下 HTML 元素:

  • <input type="input">
  • <input type="radio">
  • <input type="checkbox">
  • <select>
  • <textarea>

底层实现

从服务端将数据渲染到客户端视图模板的底层实现我们在上篇教程中已经详细介绍过,这里我们重点关注下客户端变更如何上传到服务端,并且将变化再次下发到客户端。在设置了数据绑定的 HTML 元素上,Livewire 组件自带的 JavaScript 代码 (包含在 livewire.js 中)会实时监听并跟踪绑定数据的变化,当数据有变化后,在防抖动延迟时间之后,客户端会发起一个 POST 请求到 /livewire/message 路由:

对应的路由注册逻辑位于服务提供者 vendor/calebporzio/livewire/src/LivewireServiceProvider.phpregisterRoutes 方法中:

public function registerRoutes()
{
    RouteFacade::get('/livewire/livewire.js', LivewireJavaScriptAssets::class);

    // Don't register route for non-Livewire calls.
    if ($this->isLivewireRequest()) {
        // This should be the middleware stack of the original request.
        $middleware = decrypt(request('middleware'), $unserialize = true);

        RouteFacade::post('/livewire/message', HttpConnectionHandler::class)
            ->middleware($middleware);
    }

    if (request()->headers->get('X-Livewire-Keep-Alive') == true) {
        // This will be hit periodically by Livewire to make sure the csrf_token doesn't expire.
        RouteFacade::get('/livewire/keep-alive', function () {
            return response(200);
        })->middleware('web');
    }
}

当客户端请求这个路由时,会执行 HttpConnectionHandler 类的 __invoke 方法,届时会调用其父类的 handle 方法对请请求进行处理:

public function handle($payload)
{
    $instance = ComponentHydrator::hydrate($payload['name'], $payload['id'], $payload['data'], $payload['checksum']);

    $instance->setPreviouslyRenderedChildren($payload['children']);
    $instance->hashPropertiesForDirtyDetection();

    try {
        foreach ($payload['actionQueue'] as $action) {
            $this->processMessage($action['type'], $action['payload'], $instance);
        }
    } catch (ValidationException $e) {
        $errors = $e->validator->errors();
    }

    $dom = $instance->output($errors ?? null);
    $data = ComponentHydrator::dehydrate($instance);
    $events = $instance->getEventsBeingListenedFor();
    $eventQueue = $instance->getEventQueue();

    return new ResponsePayload([
        'id' => $payload['id'],
        'dom' => $dom,
        'dirtyInputs' => $instance->getDirtyProperties(),
        'children' => $instance->getRenderedChildren(),
        'eventQueue' => $eventQueue,
        'events' => $events,
        'data' => $data,
        'redirectTo' => $instance->redirectTo ?? false,
    ]);
}

在该方法中,首先通过 ComponentHydrator::hydrate 方法传入客户端请求数据获取对应服务端组件实例,比如本例中客户端请求数据格式如下:

根据 my-name 获取到的对应服务端类是 MyName,然后通过 data 数组中的值对 MyName 实例的 public 属性进行初始化设置,这样就把客户端更新后的数据设置到了服务端组件实例中,最后返回给 handle 方法里的 $instance 变量,然后经历和初次渲染组件类似的操作把结果封装到 ResponsePayload 实例中作为响应数据返回,再由 Livewire 组件的 JavaScript 代码把响应结果中的 dom 数据渲染到客户端组件,完成数据的动态绑定效果。

高级使用

防抖动

默认情况下,Livewire 设置了 150ms 的防抖动延迟来避免更新输入框时频繁请求后端接口造成卡顿或假死状态,你可以手动默认的延迟时间,比如设置延迟时间为 300ms,可以这么做:

<input type="text" wire:model.debounce.300ms="name">

还支持延迟单位为秒:

<input type="text" wire:model.debounce.1s="name">

嵌套绑定

此外,我们还可以通过嵌套方式实现数据绑定,比如我们在服务端定义的属性字段类型是 App\User,那么可以在服务端组件类中这样初始化数据:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\User;

class MyName extends Component
{
    public $user;

    public function mount()
    {
        $this->user = User::find(1)->toArray();
    }

    public function render()
    {
        return view('livewire.my-name');
    }
}

我们可以从数据库中获取指定记录来初始化 $user,然后在视图组件模板 my-name.blade.php 中通过「.」号来实现嵌套绑定:

<div>
    <input type="text" wire:model="user.name">
    <hr>
    你好,我的名字是:{{ $user['name'] }}
</div>

实现的效果和前面一样,只是这一次我们可以在客户端视图模板中引入更多与用户相关数据,而不必定义多个 public 属性。

懒更新

默认情况下,Livewire 组件会在每次「input」事件后发送请求到服务端,这对于 <select> 这种更新不是很频繁的标签来说没什么问题,但是对于 <text><textarea> 这种改动很频繁的标签来说就有点浪费了,在这种情况下,我们可以通过懒更新指令 lazy 来监听本地的「change」事件:

<div>
    <input type="text" wire:model.lazy="user.name">
    <hr>
    你好,我的名字是:{{ $user['name'] }}
</div>

这样一来,只有当用户更新完毕,焦点离开输入框时,才会触发更新上传网络请求并同步本地的 $user['name'] 数据渲染,这样一来就极大减少了不必要的网络请求,推荐使用这种机制。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 通过 Livewire 在 Laravel 项目中实现基于 PHP 的全栈开发(二):组件创建和渲染的底层实现

>> 下一篇: 通过 Laravel Trix 扩展包在 Laravel 项目中集成使用 Trix 编辑器