首页 > 解决方案 > 使用 Blazor webassembly 在后台上传文件

问题描述

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

如果用户能够选择一个文件,单击提交按钮并在后台上传视频而不是等待视频发布完成时继续写他们的帖子,那该多好。此外,如果它是故障安全的,那就太好了,如果用户在文件上传之前完成了他或她的帖子,通过导航离开,它不会停止视频发布过程。

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

有什么建议么?jquery可以在这里提供帮助吗?

标签: jqueryfile-uploadblazorblazor-webassembly

解决方案


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

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

为上传添加进度

默认设置HttpContent无法获知上传进度。但是,我们可以创建一个新的实现并添加这个功能。我决定使用事件进行通信。这些类是从这篇文章中复制和修改的。

此类FileTransferInfo表示传输状态,同时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();
    }
}

此类被用作ProgressableStreamContent上传HttpClient文件的专用方式。

//copied from https://stackoverflow.com/questions/35320238/how-to-display-upload-progress-using-c-sharp-httpclient-postasync
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正在进行/已完成的上传。

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);
    }

}


推荐阅读