支持 Laravel.io 的持续开发 →

使用 Livewire 进行分块文件上传

2023年2月8日 阅读时间:8分钟

今天我们使用Livewire分块上传文件。使用Fly.io,您可以快速部署运行您的Laravel应用!

服务器配置了接受请求的大小限制,这是为了避免长时间处理时间,从而可能导致不可用,以及处理大型请求可能带来的潜在安全风险。

当用户请求上传超过配置限制的文件时会发生什么?很显然,上传将失败,无论是我们自定义的提示消息还是服务器返回的默认413状态码错误

我们面对的难题和之前的开发者一样,也将在未来被开发者面对:处理大文件上传。

问题

一个明显的解决办法是更新我们的配置限制。我们可以在服务器本身和PHP中增加几个配置限制。

问题是,这并不是动态的。文件上传大小可能会随时间增加,这就需要我们重新配置限制。随着限制的提高,请求处理时间也会增加。

有没有一种方法可以避免这种不断的重新配置和增加请求处理时间呢?

解决方案

解决方案并不总是关于增加限制,有时仅仅调整现有的方法就可以力挽狂澜。与其发送整个文件,为什么不分批次发送呢?

没错!今天,我们不会在不断发展变化的限制压力下改变我们的配置。相反,我们将不改变配置来解决这一问题。

今天我们将切片、切块并将文件块合并——使用Livewire

计划

我们有三个步骤的计划来切片和合并

  1. 首先,我们将通过Livewire让知晓将接收的从所有合并的块中的预期总$fileSize
  2. 然后,我们开始逐个切片、上传并在我们的服务器中将块合并成“最终文件”,每次都使用Livewire。
  3. 当我们的最终文件的大小达到给定的$fileSize时,这意味着所有块都已合并。因此我们将最终文件喂给Livewire的临时上传文件类,以便利用Livewire的上传文件功能

要拼凑出这些,您可以访问我们的仓库的readme并检查相关文件。

视图

让我们从创建一个Livewire组件开始,运行命令php artisan make:livewire chunked-file-upload。之后,更新我们的Livewire视图,包括一个包含文件输入元素和提交按钮的表单标签。

<form wire:submit.prevent="submit">
  <input type="file" id="myFile"/>
  <button type="button" onClick="uploadChunks()">Submit</button>
</form>

每次用户单击提交按钮时,我们的自定义JavaScript函数uploadChunks()将选择文件并将其切分成块,并请求Livewire上传每个块。

分享期望

为了上传一个大型文件,我们将将其切割成小于服务器请求大小限制的更小的块。我们将逐个上传每个块,以便可以立即将上传的块合并成一个“最终文件”。

但是,服务器如何知道所有块都已合并到我们的最终文件中呢?当然,它需要知道最终文件的预期大小!

Livewire属性非常适合在客户端和服务器之间共享信息,因此让我们在我们的Livewire组件中将文件信息如$fileName$fileSize作为公共属性包含进来。今天,我们将文件分开成1MB块,所以让我们声明一个用于上传的块$fileChunk和预期最大块大小$chunkSize的单独属性。

// app/Http/Livewire/ChunkedFileUpload.php
public $chunkSize = 1000000; // 1 MB
public $fileChunk; 

public $fileName;
public $fileSize; 

让我们回到我们的Livewire视图并修改由我们的提交按钮触发的uploadChunks()函数。每次用户提交文件以进行上传时,我们都会设置$fileName$fileSize的值,以便稍后发送到我们的Livewire组件。

// resources/views/livewire/chunked-file-upload.blade.php
function uploadChunks()
{
    const file = document.querySelector('#myFile').files[0];

    // Send the following later at the next available call to component
    @this.set('fileName', file.name, true);
    @this.set('fileSize', file.size, true);

注意我们在这里使用Livewire的set方法。这让我们可以在客户端设置公共属性,但不会立即调用服务器。对$fileName$fileSize的更改将发送到Livewire的下一次即时组件请求中。

现在,我们的最终文件详情已经准备好与Livewire组件共享,我们可以从文件的第0字节开始对我们的第一个块进行切片。

    livewireUploadChunk( file, 0 );
}

切片一个块

我们如何从文件中切片出一个块呢?

嗯,我们需要知道块开始和结束的位置。对于我们的文件第一个块,起点是已知的:0。但是块在哪里结束呢?

块的结束总是在块的起点1MB(我们的$chunkSize)或者文件大小,两者中较小的一个。

// resources/views/livewire/chunked-file-upload.blade.php
function livewireUploadChunk( file, start ){
    const chunkEnd  = Math.min( start + @js($chunkSize), file.size );
    const chunk     = file.slice( start, chunkEnd ); 

现在我们有了这个块,我们需要将它发送到我们的服务器。我们可以使用Livewire的upload JavaScript函数来上传并将块与上面声明的$fileChunk属性关联起来。

    @this.upload('fileChunk', chunk);

在上传第一个块之后,让我们也发送下一个。但是,我们需要确保当前的块完全上传,为此,我们可以连接到上传函数的进度回调事件。

-    @this.upload('fileChunk', chunk);
+    @this.upload('fileChunk', chunk,(uName)=>{}, ()=>{}, (event)=>{
+        if( event.detail.progress == 100 ){
+          // We recursively call livewireUploadChunk from within itself
+          start = chunkEnd;
+          if( start < file.size ){
+            livewireUploadChunk( file, start );
+          }
+        }
+    });
}

上传完成后,当event.detail.progress的值达到100时。一旦这样做,我们就递归地调用当前的函数livewireUploadChunk()以上传我们的下一个块。

file.slice方法的范围是chunkEndexclusive。例如,slice(0,10)的范围实际上是0到9,但不包括10!这意味着我们的下一个起点将是chunkEnd

保存和合并

现在,我们的Livewire视图的 JavaScript已经设置好了用于切片和上传块,我们来到了切片和切块之旅的最后一步:在我们的Livewire组件中保存和合并块!

我们将使用Livewire的WithFileUploads特质,让文件上传变得容易。这个特质允许我们声明一个可上传的属性——在我们的例子中是$fileChunk

// app/Http/Livewire/ChunkedFileUpload.php

+ use WithFileUploads;

// Chunks info
public $chunkSize = 1000000; // 1M
public $fileChunk;

// Final file 
public $fileName;
public $fileSize;

+ public $finalFile;

一旦一个块被上传,Livewire必须将其合并到一个“最终文件”中。为了做到这一点,我们需要在块上传后拦截Livewire的流程。

幸运的是,Livewire提供了我们可以用来拦截Livewire公共属性生命周期流的“钩子”。在我们的特定情况下,我们可以连接到$fileChunk属性上的updated钩子。

从我们的updatedFileChunk 钩子中,我们将使用getFileName()方法检索Livewire为当前块生成的文件名。

public function updatedFileChunk()
{
    $chunkFileName = $this->fileChunk->getFileName();

然后我们将这个块合并到最终文件中,并在合并后删除该块。

      $finalPath = Storage::path('/livewire-tmp/'.$this->fileName);
      $tmpPath   = Storage::path('/livewire-tmp/'.$chunkFileName);
      $file = fopen($tmpPath, 'rb');
      $buff = fread($file, $this->chunkSize);
      fclose($file);

      $final = fopen($finalPath, 'ab');
      fwrite($final, $buff);
      fclose($final);
      unlink($tmpPath);

最终,所有的块将逐个到来并合并成我们的最终文件。为了确定所有块是否已合并,我们只需将最终文件的大小与预期的$fileSize进行比较。

当然,这个新生成的文件是我们自定义的文件。我们需要将它封装在Livewire的TemporaryUploadedFile类中,以便利用Livewire的上传文件功能

      $curSize = Storage::size('/livewire-tmp/'.$this->fileName);
      if( $curSize == $this->fileSize ){
          $this->finalFile = 
          TemporaryUploadedFile::createFromLivewire('/'.$this->fileName);
      }
}

别忘了导入TemporaryUploadedFile类,并声明一个新的公共属性$finalFile

例如,像这样在我们的视图中预览一个新、临时的图像。

@if ($finalFile)
    Photo Preview:
    <img src="{{ $finalFile->temporaryUrl() }}">
@endif

使用Livewire进行实现通常更加流畅,上传文件块也不例外!

上次更新1年前。

driesvints, elkdev喜欢这篇文章

2
喜欢这篇文章吗? 告诉作者并为他们鼓掌!

你可能还喜欢这些文章

2024年3月11日

如何使用Larastan让你的Laravel应用从0到9

使用Larastan在Laravel应用执行前找到bug是可能的...

阅读文章
2024年7月19日

无需特性实现API响应的标准化

我发现大多数用于API响应的库都是使用特性实现的...

阅读文章
2024年7月17日

通过Discord通知在你的Laravel项目中收集反馈

如何在Laravel项目中创建反馈模块,并在收到电子邮件...

阅读文章

我们感谢这些 令人惊叹的公司 支持

您的标志在此?

Laravel.io

Laravel问题解决、知识共享和社区建设的门户。

© 2024 Laravel.io - 版权所有。