スキップしてメイン コンテンツに移動

[Angular] Sending file with Observable and showing loading screen

Intro

When I tried sending file data on last time, I had confused with "promise.then", "async/await" and "Observable".
[Angular][ASP.NET Core] Upload chunked files
So I wanted to distinct them, and this time, I tried to use "Observable" because HttpClient return Observable<any>.

Call observables in order

I sended file in these steps.
  1. Read file by FileReader
  2. Create directory for saving chunks
  3. send and saving chunks
  4. merge chunks to one file and delete chunks
Each steps used the former steps result.
So I could write by Promise.then like below.

this.executeStep1() // return Promise<UploadResult>
    .then(result => this.executeStep2(result)) // return Promise<UploadResult>
    .then(result => this.executeStep3(result)) // return Promise<UploadResult>
    .catch(reason => console.log(reason));

Result

I could write with pipe & flatMap.

file-uploader.service.ts


public upload(file: File): Observable<UploadResult> {
    const reader = new FileReader();
    const fileName = file.name;
    reader.readAsArrayBuffer(file);
    return fromEvent(reader, 'load')
      .pipe(
        flatMap(r => this.sendBufferData(reader.result, fileName)),
        flatMap(r => this.sendChunks(r.result, r.file)),
        flatMap(r => this.endSendingData(fileName, r)),
        take(1) // for call oncomplete
      );
  }

Each flatMap arguments were former methods return values.

FileReader.onload

I thought only DOM event like Button.onclick could be used by fromEvent.
But fileReader.onload was also used.

Merge observables

When I sent chunks, I used promiseAll to await all sending chunks operations.
This time, I used forkJoin.

file-uploader.service.ts


private sendChunks(startResult: UploadFileResult, buffer: Uint8Array): Observable<UploadFileResult>{
...
    let fileIndex = 0;
    const chunkSenders = new Array<Observable<UploadResult>>();
    for (let i = 0; i < buffer.length; i += this.chunkSize) {
      let indexTo = i + this.chunkSize;
      if (indexTo >= buffer.length) {
        // [2020-05-20 Update] end index of subarray was as same as buffer.length.
      	indexTo = buffer.length; // for last data.
      }
      const formData = new FormData();
      formData.append('file', new Blob([buffer.subarray(i, indexTo)]));
      const sender = this.httpClient.post<UploadResult>(
        'http://localhost:5000/files/chunk',
        formData, {
          headers: {
            tmpDirectory: startResult.tmpDirectoryName,
            index: fileIndex.toString()
          }
        });
      chunkSenders.push(sender);
      fileIndex += 1;
    }
    return forkJoin(chunkSenders)
      .pipe(
        // to return Observable<UploadResult>
        map(result => {
          const failedResult = result.find(r => r.succeeded === false);
          if (failedResult != null) {
            return FileUploaderService.getFailedFileResult(failedResult.errorMessage);
          }
          return {
            result: FileUploaderService.getSucceededResult(),
            tmpDirectoryName: startResult.tmpDirectoryName
          };
        })
      );
  }

Codes

file-uploader.service.ts


import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable, Observer, fromEvent, of, merge, forkJoin} from 'rxjs';
import { flatMap, map, mergeAll, catchError, take } from 'rxjs/operators';
import {UploadFileResult} from './upload-file-result';
import {UploadResult} from './upload-result';

@Injectable({
  providedIn: 'root'
})
export class FileUploaderService {
  constructor(private httpClient: HttpClient) { }
  private chunkSize = 1048576;
  private static getSucceededResult(): UploadResult {
    return {
      succeeded: true,
      errorMessage: '',
    };
  }
  private static getFailedResult(reason: string): UploadResult {
    return {
      succeeded: false,
      errorMessage: reason,
    };
  }
  private static getFailedFileResult(reason: any): UploadFileResult {
    return {
      result: this.getFailedResult(reason),
      tmpDirectoryName: ''
    };
  }
  public upload(file: File): Observable<UploadResult> {
    const reader = new FileReader();
    const fileName = file.name;
    reader.readAsArrayBuffer(file);
          
    return fromEvent(reader, 'load')
      .pipe(
        flatMap(r => this.sendBufferData(reader.result, fileName)),
        flatMap(r => this.sendChunks(r.result, r.file)),
        flatMap(r => this.endSendingData(fileName, r)),
        take(1)
      );
  }
  private sendBufferData(readResult: string|ArrayBuffer,
                         fileName: string): Observable<{ result: UploadFileResult,
                                                         file: Uint8Array|null }> {
    if (readResult == null) {
      return of({
        result: FileUploaderService.getFailedFileResult('failed reading file'),
        file: null });
    }
    const fileData = new Uint8Array(readResult as ArrayBuffer);
    if (fileData == null || fileData.length <= 0) {
      return of({
        result: FileUploaderService.getFailedFileResult('failed reading file'),
        file: null });
    }
    return this.startSendingData(fileName)
      .pipe(
        map(result => {
          return {
            result,
            file: (result.result.succeeded === true)? fileData as Uint8Array: null,
          };
        }));
  }
  private startSendingData(fileName: string): Observable<UploadFileResult> {
    const formData = new FormData();
    formData.append('fileName', fileName);
    return this.httpClient.post<UploadFileResult>(
      'http://localhost:5000/files/start',
      formData, { });
  }
  private sendChunks(startResult: UploadFileResult, buffer: Uint8Array): Observable<UploadFileResult>{
    if (startResult.result.succeeded === false) {
      return of(FileUploaderService.getFailedFileResult(startResult.result.errorMessage));
    }
    let fileIndex = 0;
    const chunkSenders = new Array<Observable<UploadResult>>();
    for (let i = 0; i < buffer.length; i += this.chunkSize) {
      let indexTo = i + this.chunkSize;
      if (indexTo >= buffer.length) {
        indexTo = buffer.length; // for last data.
      }
      const formData = new FormData();
      formData.append('file', new Blob([buffer.subarray(i, indexTo)]));
      const sender = this.httpClient.post<UploadResult>(
        'http://localhost:5000/files/chunk',
        formData, {
          headers: {
            tmpDirectory: startResult.tmpDirectoryName,
            index: fileIndex.toString()
          }
        });
      chunkSenders.push(sender);
      fileIndex += 1;
    }
    return forkJoin(chunkSenders)
      .pipe(
        map(result => {
          const failedResult = result.find(r => r.succeeded === false);
          if (failedResult != null) {
            return FileUploaderService.getFailedFileResult(failedResult.errorMessage);
          }
          return {
            result: FileUploaderService.getSucceededResult(),
            tmpDirectoryName: startResult.tmpDirectoryName
          };
        })
      );
  }
  private endSendingData(fileName: string, sendChunkResult: UploadFileResult): Observable<UploadResult> {
    if (sendChunkResult.result.succeeded === false) {
      return of(sendChunkResult.result);
    }
    const formData = new FormData();
      formData.append('fileName', fileName);
      formData.append('tmpDirectory', sendChunkResult.tmpDirectoryName);
      return this.httpClient.post<UploadResult>(
        'http://localhost:5000/files/end',
        formData, { });
  }
}

Resources

RxJS in Action
Operators - RxJS

Show loading screen

I wanted show the loading screen.
If the page size was smaller than screen, I had no problems.
But now, the page height was bigger than the screen size, I had to set the loading screen as same as the page size or move it to scrolled position.
And I choised the later one.

Get scrolled position

I could get scrolled position by "window.scrollY".
And I could set the value to "top" of CSS to set the loading screen positionY.

The problem was IE hadn't been able to understand "window.scrollY".
So I had had to use "window.pageYOffset".
Window.scrollY - Web APIs | MDN

loading-screen.component.css


.loading-background{
  background-color: rgba(0, 0, 0, 0.6);
  color: white;
  height: 100vh;
  width: 100vw;
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 0;
  left: 0;
}

loading-screen.component.html


<div *ngIf="shown" class="loading-background"
  [ngStyle]="{'top': screenPositionY}">
  Uploading...
</div>

loading-event.service.ts


import {EventEmitter, Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root'
})
/** show/hide loading screen service */
export class LoadingEventService {
  public loadingStarted = new EventEmitter();
  public loadingStopped = new EventEmitter();
  constructor() { }
  public startLoading() {
    this.loadingStarted.emit();
  }
  public stopLoading() {
    this.loadingStopped.emit();
  }
}

loading-screen.component.ts


import { Component, OnInit } from '@angular/core';
import {LoadingEventService} from './loading-event.service';

@Component({
  selector: 'app-loading-screen',
  templateUrl: './loading-screen.component.html',
  styleUrls: ['./loading-screen.component.css']
})
export class LoadingScreenComponent implements OnInit {
  public shown = false;
  public screenPositionY = '0px';
  constructor(private loadingEventService: LoadingEventService) {
  }
  ngOnInit() {
    this.loadingEventService.loadingStarted
      .subscribe(_ => this.startLoading());
    this.loadingEventService.loadingStopped
      .subscribe(_ => this.stopLoading());
  }
  private startLoading() {
    // window.scrollY -> window.pageYOffset for IE
    this.screenPositionY = `${window.pageYOffset}px`;
    this.shown = true;
  }
  private stopLoading() {
    this.shown = false;
  }
}

Stop mouse scroll

I wanted to stop mouse scroll when the file had been uploading to avoid showing not masked area.
So I added stopping mouse scroll function.

loading-screen.component.ts


...
export class LoadingScreenComponent implements OnInit {
  public shown = false;
  public screenPositionY = '0px';
...
  private startLoading() {
    // window.scrollY -> window.pageYOffset for IE
    this.screenPositionY = `${window.pageYOffset}px`;
    document.body.style.overflow = 'hidden';
    this.shown = true;
  }
  private stopLoading() {
    document.body.style.overflow = 'auto';
    this.shown = false;
  }
}

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests)

[Nest.js] Use WebSocket with ws

Intro Until last time , I had used node-web-rtc to try WebRTC. But because the example was a little complicated for I understood the core functions of using WebRTC. So I look for other frameworks or libraries. PeerJS is a famous library for WebRTC. peers/peerjs: Peer-to-peer data in the browser. - GitHub peers/peerjs-server: Server for PeerJS - GitHub PeerJS - Simple peer-to-peer with WebRTC A problem is I don't know how to integrate to the Nest.js project. I couldn't find examples. So I don't choose at least this time. What shall I choose? According MDN, WebRTC doesn't specify strictly what technology is used on server application for connecting two devices. Signaling and video calling - Web APIs | MDN But in many examples include MDN's one use WebSocket. samples-server/s/webrtc-from-chat at master · mdn/samples-server · GitHub So I try WebSocket in the Nest.js project. Use WebSocket in a Nest.js project Nest.js has a function for using We

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu