【Laravel實戰】20分鐘搞懂Livewire的檔案上傳功能

【Laravel實戰】20分鐘搞懂Livewire的檔案上傳功能

前言

大家好,我是每天在線上陪著你成長的哥布林工程師,今天想要談一個或許正困擾著你的問題:檔案上傳功能

檔案上傳功能是個常會用到的功能,但是在後台程式的檔案處理上卻相當的麻煩,即便是在 Laravel 框架底下還是要寫不少的程式碼。在這裡告訴你們一個好消息,就是Livewire 讓上傳與儲存檔案變得極為簡單,這個超讚,這篇文章將讓你掌握使用Livewire來實作檔案上傳功能的技巧

但在開始今天的介紹之前,要先提醒你們, Livewire 版本須為 1.2.0 以上才能使用這個功能,所以如果你的 Livewire 版本比較舊的話,建議先升到2.0以上唷

快速開始

要在 Livewire 組件內啟動檔案上傳功能非常簡單,只有兩個步驟:

  • 步驟1 加入 withFileUploads trait 到組件裏頭

  • 步驟2 使用wire:model到檔案上傳輸入項

做完上面兩個步驟之後,剩下的就交給Livewire幫你搞定吧!下面是一個處理上傳照片的簡單組件範例:

use Livewire\WithFileUploads;

class UploadPhoto extends Component
{
    use WithFileUploads;

    public $photo;

    public function save()
    {
        $this->validate([
            'photo' => 'image|max:1024', // 1MB Max
        ]);

        $this->photo->store('public/photos');
    }
}
<form wire:submit.prevent="save">
    <input type="file" wire:model="photo">

    @error('photo') <span class="error">{{ $message }}</span> @enderror

    <button type="submit">Save Photo</button>
</form>

發現了嗎?處理檔案輸入項和其他輸入項並沒有啥不同,就是一樣加入 wire:model 到 input 標籤,其他的像是檔案更名.檔案儲存等工作 Livewire 都會替你處理

Livewire底層實作

如果你不關心到底 Livewire做了甚麼,沒關係,你可以跳到下一節。但假如你好奇到底 Livewire會做甚麼,這一節就來談一下它的底層實作

Livewire 幫我們做了相較於其他輸入項更多的事情,當一個新檔案被選取, Livewire 的 JS 讓對伺服器上的組件進行初步請求取得一個暫時的簽名上傳網址。一旦得到網址之後,JS就會對該網址進行真正的上傳,將上傳檔案存到由 Livewire 所指定的臨時資料夾,並返回該臨時檔案的唯一 hash ID

接著我們的公開屬性,比如剛才例子中的 $photo 被賦值先前的臨時上傳檔案,並準備接下來的儲存或驗證作業

儲存上傳的檔案

之前的例子示範了最常見的儲存劇本,也就是移動臨時上傳檔案到 photos 資料夾,該資料夾位於應用的預設檔案系統磁碟

然而,你可能想要自定義儲存檔案的名稱,甚至是指定別的儲存系統來儲存檔案,例如 S3 bucket,這一小節就和你討論可以怎麼做

好消息是 Livewire 依循著 Laravel 用來儲存上傳檔案的 API,所以你可以直接參考 Laravel 的官方文件,不過這裡還是先提供幾個較為常用的儲存劇本給你快速改用

// 儲存上傳檔案到預設檔案系統磁碟的 storage/app/public/photos 資料夾
$this->photo->store('public/photos');

// 將檔案儲存到 S3 bucket 的 photos 資料夾
$this->photo->store('photos', 's3');

// 儲存上傳檔案到預設檔案系統磁碟的 photos 資料夾,並更名為 "avatar.png"
$this->photo->storeAs('photos', 'avatar');

// 將檔案儲存到 S3 bucket 的 photos 資料夾,並更名為 "avatar.png"
$this->photo->storeAs('photos', 'avatar', 's3');

// 將檔案儲存到 S3 bucket 的 photos 資料夾,並設為公開
$this->photo->storePublicly('photos', 's3');

// 將檔案儲存到 S3 bucket 的 photos 資料夾,設為公開,並更名為 "avatar.png"
$this->photo->storePubliclyAs('photos', 'avatar', 's3');

以上的這些方法應該可以提供你足夠的彈性來儲存上傳檔案,你可以根據自己的需求來選擇

處理多檔案上傳

如果你需要一次上船多個檔案也沒關係,改動上不算大。Livewire 會自動偵測 <input> 標籤的 multiple 屬性來自動啟動處理多檔案機制,下面是一個處理多檔案上傳的例子

use Livewire\WithFileUploads;

class UploadPhotos extends Component
{
    use WithFileUploads;

    public $photos = [];

    public function save()
    {
        $this->validate([
            'photos.*' => 'image|max:1024', // 1MB Max
        ]);

        foreach ($this->photos as $photo) {
            $photo->store('photos');
        }
    }
}
<form wire:submit.prevent="save">
    <input type="file" wire:model="photos" multiple>

    @error('photos.*') <span class="error">{{ $message }}</span> @enderror

    <button type="submit">Save Photo</button>
</form>

檔案驗證

就如你之前例子所見,透過 Livewire 來驗證上傳檔案是非常接近先前 Laravel 控制器的標準做法,如果需要了解更多檔案驗證機制,請參考Laravel官方文件

接下來我們來看一下 Livewire針對驗證的需要提供那些機制或功能

即時驗證

即時驗證使用者所上傳的檔案是可能的,你能在使用者按下 "submit" 之前進行檔案驗證,做法和在 Livewire 處理其他輸入項的即時驗證相同

use Livewire\WithFileUploads;

class UploadPhoto extends Component
{
    use WithFileUploads;

    public $photo;

    public function updatedPhoto()
    {
        $this->validate([
            'photo' => 'image|max:1024', // 1MB Max
        ]);
    }

    public function save()
    {
        // ...
    }
}
<form wire:submit.prevent="save">
    <input type="file" wire:model="photo">

    @error('photo') <span class="error">{{ $message }}</span> @enderror

    <button type="submit">Save Photo</button>
</form>

現在,當使用者選擇一個檔案時(時機點就在 Livewire 將上傳檔案放到臨時資料夾之後),該檔案將會被進行驗證,該使用者將會在提交表單前收到驗證錯誤的訊息

臨時預覽網址

就在使用者選擇一個檔案後,你可能想要在提交表單並儲存檔案之前去預覽它,尤其是上傳圖片往往會有這個需求。Livewire 加入了 temporaryUrl() 來滿足你的這個小小需求

注意

考量到安全,臨時預覽網址功能只支持圖檔上傳

這是一個圖檔上傳的預覽範例:

use Livewire\WithFileUploads;

class UploadPhotoWithPreview extends Component
{
    use WithFileUploads;

    public $photo;

    public function updatedPhoto()
    {
        $this->validate([
            'photo' => 'image|max:1024',
        ]);
    }

    public function save()
    {
        // ...
    }
}
<form wire:submit.prevent="save">
    @if ($photo)
        Photo Preview:
        <img src="{{ $photo->temporaryUrl() }}">
    @endif

    <input type="file" wire:model="photo">

    @error('photo') <span class="error">{{ $message }}</span> @enderror

    <button type="submit">Save Photo</button>
</form>

就如同先前所言, Livewire 將暫存檔案放在非公開的資料夾,因此,並沒有甚麼便利方法來讓你以公開網址來對外公開暫存資料夾。因此剛才的例子重點在於,Livewire 為你處理的所有複雜與困難的工作,透過提供一個臨時並有簽名(防止修改)的公開網址來讓你能夠在頁面上預覽使用者上傳的圖檔

這個網址在設計上我相信是經過巧思的,使用者無法從網址看出你真實的檔案系統架構。而因為網址有簽名的關係,使用者也無法隨意改動網址來試圖去預覽資料夾的其他檔案,簡言之,安全性不需要擔心

另外假如你有設定 Livewire 去使用 S3 來作為臨時檔案儲存系統,呼叫 temporaryUrl() 將會生成一個臨時的簽名網址指向到 S3 資料夾,因此這個預覽將不會關連到你的 Laravel 應用伺服器

檔案上傳單元測試

測試在 Livewire 進行檔案上傳非常簡單,透過 Laravel 的檔案上傳測試幫助函式,下面是一個完整的範例,用來說明如何測試 Livewire 的上傳檔案組件

/** @test */
public function can_upload_photo()
{
    Storage::fake('avatars');

    $file = UploadedFile::fake()->image('avatar.png');

    Livewire::test(UploadPhoto::class)
        ->set('photo', $file)
        ->call('upload', 'uploaded-avatar.png');

    Storage::disk('avatars')->assertExists('uploaded-avatar.png');
}

這是 "UploadPhoto" 組件的部分程式片段,用以讓先前的測試能夠通過

class UploadPhoto extends Component
{
    use WithFileUploads;

    public $photo;

    // ...

    public function upload($name)
    {
        $this->photo->storeAs('/', $name, $disk = 'avatars');
    }
}

如想了解更多關於測試檔案上傳的細節,請參考 Laravel 的檔案上傳測試文件

直接上傳到 Amazon S3

如先前所言, Livewire 儲存所有上傳檔案到臨時資料夾,直到開發者決定要永久的儲存這些檔案

一般來說 Livewire 會使用預設的檔案系統資料夾(通常都是本地端),並將這些檔案存在一個名為 "livewire-tmp" 的資料夾,這代表每當檔案上傳勢必會觸及到你的伺服器,就算最終這些檔案是要被存到 S3 bucket 也是一樣,難道沒辦法全交給 S3 bucket 處理嗎?

假如你想要直接把暫存資料夾移到 S3 bucket,沒有問題,你可以輕易地透過設定行為來達成。在你的 config/livewire.php 檔案內,設定 livewire.temporary_file_upload.disk 為 S3 ,又或者是其他使用 S3 driver 的自定義磁碟

//config\livewire.php

return [
    ...
    'temporary_file_upload' => [
        'disk' => 's3',
        ...
    ],
];

現在每當使用者上傳一個檔案,這個檔案將永遠不會存在你的伺服器,它將直接被上傳到 S3 bucket,在一個名為 "livewire-tmp" 的子資料夾

設定S3檔案自動清除

透過剛才的說明,你應該已經知道所有的上傳檔案都會先被存放在一個名為 livewire-temp 的暫存資料夾。如果不做任何處理的話,暫存資料夾將會快速地被檔案所填滿,因此設定 S3 能夠清除超過24個小時的檔案是很重要的

為了設定這個行為,只要在有 S3 bucket 設定的環境執行以下的 artisan 命令

php artisan livewire:configure-s3-upload-cleanup

現在,任何在暫存資料夾內超過24小時的檔案將會自動地被 S3 清除

假如你不使用 S3。不用擔心, Livewire 將會自動地為你清除檔案,也就不需要執行這個命令

載入提示器

雖然 wire:model 用於檔案輸入項的底層實作與其他輸入項有所不同,用於載入顯示器的介面卻是相同的,你能夠透過這樣的方式來顯示載入提示器

<input type="file" wire:model="photo">

<div wire:loading wire:target="photo">上傳中...</div>

現在,當檔案在上傳檔案時會顯示"上傳中..."的訊息,而當上傳結束之後就會隱藏這個訊息,它將會與整個 Livewire 載入狀態 APIs 協同作業

進度提示器

每一個利用 Livewire 進行檔案上傳的 輸入項會發送 JavaScript 事件讓你能夠自定義 JS 程式來進行監聽

以下是會發送的事件:

事件 描述
livewire-upload-start 當開始上傳時發出
livewire-upload-finish 當上傳順利結束後發出
livewire-upload-error 當上傳因不明原因失敗時發出
livewire-upload-progress 在上傳過程中發出,會附帶目前進度,以百分比的形式

這是一個結合 AlpineJS 的 Livewire 檔案上傳組件,示範如何顯示進度條

<div
    x-data="{ isUploading: false, progress: 0 }"
    x-on:livewire-upload-start="isUploading = true"
    x-on:livewire-upload-finish="isUploading = false"
    x-on:livewire-upload-error="isUploading = false"
    x-on:livewire-upload-progress="progress = $event.detail.progress"
>
    <!-- File Input -->
    <input type="file" wire:model="photo">

    <!-- Progress Bar -->
    <div x-show="isUploading">
        <progress max="100" x-bind:value="progress"></progress>
    </div>
</div>

JavaScript 上傳 API

要整合第三方檔案上傳函式庫一般都需要比單純使用 "" 來得進行更多調整,為此 Livewire 開放其所使用的 JavaScript 函式

這個函式存在於 JavaScript 組件物件,能透過便利的 Blade 指令 : @this 來取用。假如你不是太熟悉可以參考這個例子:

<script>
    let file = document.querySelector('input[type="file"]').files[0]

    // Upload a file:
    @this.upload('photo', file, (uploadedFilename) => {
        // Success callback.
    }, () => {
        // Error callback.
    }, (event) => {
        // Progress callback.
        // event.detail.progress contains a number between 1 and 100 as the upload progresses.
    })

    // Upload multiple files:
    @this.uploadMultiple('photos', [file], successCallback, errorCallback, progressCallback)

    // Remove single file from multiple uploaded files
    @this.removeUpload('photos', uploadedFilename, successCallback)
</script>

設定

因為 Livewire 會在開發者有機會去驗證或儲存之前先暫存所有上傳檔案, Livewire 會針對所有的上傳檔案預設一些處理流程

全域驗證

預設情況下, Livewire 將會依據以下的這些規則來驗證所有的上傳暫存檔案,file|max:12288,也就是最大不得超過 12 MB

假如你想要調整這個大小,你能在 config/livewire.php 檔案內去設定成自己想要的驗證規則,像這樣:

return [
    ...
    'temporary_file_upload' => [
        ...
        'rules' => 'file|mimes:png,jpg,pdf|max:102400', // (最大不超過 100MB , 只接受 png, jpeg, 和 pdf.)
        ...
    ],
];

全域中介層

暫時檔案上傳終端預設會有 throttling 中介層,但你可以透過以下的設定參數來自定義自己想要在終端使用哪些中介層

return [
    ...
    'temporary_file_upload' => [
        ...
        'middleware' => 'throttle:5,1',//每個用戶一分鐘限定五次上傳
    ],
];

臨時上傳資料夾

暫存檔案會被上傳到指定磁碟的 livewire-tmp 資料夾,你能夠透過以下設定來修改它

return [
    ...
    'temporary_file_upload' => [
        ...
        'directory' => 'tmp',
    ],
];

希望今天的文章對你能有幫助,如有任何問題歡迎在底下留言,我會盡快地回覆你們唷!


分享這篇文章:

關聯文章:

訂閱電子報,索取 Laravel 學習手冊

價值超過 3000 元,包含常用 Laravel 語法與指令!

Laravel 百萬年薪特訓營

從最基礎的 PHP 語法開始,包含所有你該知道的網頁基礎知識,連同 Laravel 從零開始一直到實戰,最後還將告訴你如何找好工作,讓你及早擁有百萬年薪