使用 Blazor webassembly 在后台上传文件

Upload a file in background with Blazor webassembly

我正在使用 wasm 构建一个网站,用户可以在其中 post 一些文本并将图片和视频附加到他们的 post。这些视频使用他们的 API posted 到 vimeo(就像 YouTube),这基本上是一个带有类型 file 输入元素的网络表单和一个提交按钮,该按钮 POST 将文件发送到他们的服务器直接从客户端。有些视频可能很大,因此 post 将那么多数据传输到 vimeo 服务器可能需要时间。

如果用户能够选择一个文件,点击提交按钮并在后台上传视频时继续编写他们的 post 而不是等待 post 结束视频。此外,如果它是故障安全的,如果用户完成他或她的 post 比文件上传更早,通过导航离开,它不会停止视频 posting 过程。

请注意,vimeo 有其他方法 post 将视频发送到他们的服务器(例如拉取方法)。但是所有这些都涉及在上传到他们的服务器之前首先登陆我的服务器的视频。这在带宽和存储方面成本很高。

有什么建议吗? jquery 可以帮忙吗?

解决方案有点长,并且有很多活动部分。

我建议通过独立服务实施故障保护。此服务由您的 CreatePost 组件和 UploadManager 组件使用。请注意,此解决方案并不完美。缺少整个“取消上传”和错误处理部分。此外,没有实现队列。

正在为上传添加进度

默认 HttpContent 无法获知上传进度。但是,我们可以创建一个新的实现并添加此功能。我决定使用事件进行通信。 classes 是从 post.

复制和修改的

这个classFileTransferInfo表示一个传输的状态,同时FileTransferingProgressChangedEventArgs复制更改应用程序可能感兴趣的任何部分。

public class FileTransferingProgressChangedEventArgs : EventArgs
{
    public String Filename { get; set; }
    public Int64 BytesSent { get; init; }
    public Int64 TotalBytes { get; init; }
    public FileTransferInfo.States State { get; init; }
}

public class FileTransferInfo
{
    public enum States
    {
        Init,
        Pending,
        Transfering,
        PendingResponse,
        Finished
    }

    public States State { get; private set; }
    public Int64 BytesSent { get; private set; }
    public String Filename { get; private set; }
    public Int64 TotalSize { get; private set; }

    public FileTransferInfo(String filename, Int64 totalSize)
    {
        Filename = filename;
        TotalSize = totalSize;
        BytesSent = 0;
        State = States.Init;
    }

    private void SendEventHandler() => Progress?.Invoke(this,
        new FileTransferingProgressChangedEventArgs
        {
            BytesSent = BytesSent,
            State = State,
            TotalBytes = TotalSize,
            Filename = Filename
        });

    public event EventHandler<FileTransferingProgressChangedEventArgs> Progress;

    public void Start()
    {
        State = States.Transfering;
        SendEventHandler();
    }

    public void UpdateProgress(int length)
    {
        State = States.Transfering;
        BytesSent += length;
        SendEventHandler();
    }

    internal void UploadFinished()
    {
        State = States.PendingResponse;
        //just in case
        BytesSent = TotalSize;
        SendEventHandler();
    }

    internal void ResponseReceived()
    {
        State = States.Finished;
        SendEventHandler();
    }
}

此 class 由 ProgressableStreamContent 使用,HttpClient 将使用它作为上传文件的专门方式。

//copied from 
public class ProgressableStreamContent : HttpContent
{
    private const int defaultBufferSize = 4096;

    private readonly Stream _content;
    private readonly Int32 _bufferSize;
    private Boolean _contentConsumed;
    private readonly FileTransferInfo _progressInfo;

    public ProgressableStreamContent(Stream content, FileTransferInfo progressInfo) : this(content, defaultBufferSize, progressInfo) { }

    public ProgressableStreamContent(Stream content, Int32 bufferSize, FileTransferInfo progressInfo)
    {
        if (bufferSize <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(bufferSize));
        }

        this._content = content ?? throw new ArgumentNullException(nameof(content));
        this._bufferSize = bufferSize;
        this._progressInfo = progressInfo;
    }

    protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        PrepareContent();

        var buffer = new Byte[this._bufferSize];

        _progressInfo.Start();

        using (_content)
        {
            while (true)
            {
                var length = await _content.ReadAsync(buffer, 0, buffer.Length);
                if (length <= 0) break;

                _progressInfo.UpdateProgress(length);

                stream.Write(buffer, 0, length);
            }
        }

        _progressInfo.UploadFinished();
    }

    protected override bool TryComputeLength(out long length)
    {
        length = _content.Length;
        return true;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _content.Dispose();
        }
        base.Dispose(disposing);
    }


    private void PrepareContent()
    {
        if (_contentConsumed)
        {
            // If the content needs to be written to a target stream a 2nd time, then the stream must support
            // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target 
            // stream (e.g. a NetworkStream).
            if (_content.CanSeek)
            {
                _content.Position = 0;
            }
            else
            {
                throw new InvalidOperationException("SR.net_http_content_stream_already_read");
            }
        }

        _contentConsumed = true;
    }
}

上传管理器

上传管理器可以初始化上传并获取 FileTransferInfo 的 ongoing/finished 上传。

public class UploadManager
{
    private readonly HttpClient _vimeoApiClient;
    private List<FileTransferInfo> _transfers = new();

    public IReadOnlyList<FileTransferInfo> Transfers => _transfers.AsReadOnly(); 

    public UploadManager(HttpClient vimeoApiClient)
    {
        _vimeoApiClient = vimeoApiClient;
    }

    public void StartFileUpload(Stream stream, String fileName, Int64 fileSize)
    {
        FileTransferInfo uploaderInfo = new FileTransferInfo(fileName, fileSize);
        uploaderInfo.Progress += UpdateFileProgressChanged;
        _transfers.Add(uploaderInfo);

        var singleFileContent = new ProgressableStreamContent(stream, uploaderInfo);
        //read the docs if vimeo expected such an encoded content
        var multipleFileContent = new MultipartFormDataContent();
        multipleFileContent.Add(singleFileContent, "file", fileName);

        Task.Run(async () =>
        {
            var result = await _vimeoApiClient.PostAsync("<YourURLHere>", multipleFileContent);
            uploaderInfo.ResponseReceived();

            uploaderInfo.Progress -= UpdateFileProgressChanged;
        });
    }

    private void UpdateFileProgressChanged(Object sender, FileTransferingProgressChangedEventArgs args) => FileTransferChanged?.Invoke(sender, args);

    public event EventHandler<FileTransferingProgressChangedEventArgs> FileTransferChanged;
}

实际的上传逻辑可能会根据API的需要而改变。此示例显示如何将其与 MultipartForm 一起使用,您可以在其中添加更多文件或其他内容。

如果需要,事件 FileTransferChanged 可以做得更好。现在,它就像一个流动式加热器。

需要为 DI 注册上传管理器。

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    ....
    builder.Services.AddSingleton( sp => new UploadManager(
        new HttpClient { 
                BaseAddress = new Uri("<BaseURL>") } 
        ));
    await builder.Build().RunAsync();
}

最多可以在此处进行改进。上传管理器是排队和取消的地方。

上传视图 上传视图非常简单明了,可以根据您的需要轻松定制。

@inject UploadManager UploadManager
@implements IDisposable


<h3>Uploads</h3>
<ul>
    @foreach (var item in UploadManager.Transfers)
    {
        <li><span>@item.Filename</span> @(Math.Round( (100.0 * item.BytesSent / item.TotalSize),2 )) %</li>
    }
</ul>

@code {

    protected override void OnInitialized()
    {
        base.OnInitialized();
        UploadManager.FileTransferChanged += TransferProgressChanged;
    }

    private async void TransferProgressChanged(Object sender, FileTransferingProgressChangedEventArgs args)
    {
        await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        UploadManager.FileTransferChanged -= TransferProgressChanged;
    }
}

** NewPost 组件```

UploadManager这个“举重运动员”,文件上传变得简单。

@page "/NewPost"
@inject UploadManager UploadManager

<EditForm Model="_model">
    <InputText @bind-Value="_model.Name" class="form-control" />
    <InputFile OnChange="UploadFile" />
</EditForm>

<UploadManagerView />

@code {

    private void UploadFile(InputFileChangeEventArgs args)
    {
        // to make the demo easier, we assume that only one file is uploaded at a time
        if (args.FileCount > 1) { return; }

        UploadManager.StartFileUpload(args.File.OpenReadStream(/*SetMaxFileSizeHere*/), args.File.Name, args.File.Size);
    }

}