Angular - Giải Pháp Force Download File Trên Web application

Angular - Giải Pháp Force Download File Trên Web application

Khi làm về các chức năng download file, bạn sẽ rất hay bắt gặp về requirement click vào link, button là phải download luôn thay vì bật hoặc load đè lên trang web hiện tại. Vấn đề nhảy sang tab khác hay load đè lên web đang bật này rất gây khó chịu với user.

Bài toán đặt ra:

Tuy nhiên khi implement chức năng download file, nếu xử lý ở tầng native mobile hay là desktop application thì việc này rất dễ dàng vì có thể can thiệp được sâu về luồng download rồi UI nhưng trên browser thì sẽ gắp nhiều vấn đề về tương thích vì chỉ làm được thông qua các API web nhất định (do giới hạn về security). Ngoài ra còn có nhiều các vấn đề phát sinh như sau:

  • Các browser Ex: Chrome, Edge, IE11, Firefox, mobile web (Android OS) đều có các behavior khác nhau, cái thì bật tab mới cái thì load lại cả trang
  • Tính năng mới html5 Download attribute của <a> tag chưa support cho download image và luôn bật tab mới hoặc load đè lên page hiện tại
  • IE11, Edge đều chưa hỗ trợ html5 Download attribute của <a> tag
  • Không save được theo file name tùy ý, trong trường hợp link filename hash sẽ là trải nghiệm xấu cho user

Giải pháp là gì?

Dưới đây mình share lại solution đã giải quyết tất cả các vấn đề trên bao gồm:

  • support tất cả browser phổ biến hiện tại bao gồm cả mobile
  • force download save theo file name chứ không theo link url
  • trả về progress download theo %

1)  Source code

File: fire-download.service.ts

@Injectable({ providedIn: 'root' })
export class FileDownloadService {


    constructor(private http: HttpClient) { }


    /**
     * download file with report progress and save dialog.
     * 
     * @param url 
     * @param fileName 
     * 
     * @return Observable contains progress by percent
     * 
     */
    downloadFile(url: string, fileName: string): Observable<number> {
        return new Observable(observer => {
            this.requestDownload(url).subscribe((event: HttpEvent<Blob>) => {
                if (event.type === HttpEventType.DownloadProgress) {
                    const percentDone = Math.round(100 * event.loaded / event.total);
                    observer.next(percentDone);
                }
                if (event.type === HttpEventType.Response) {
                    this.saveDownloadResult(event.body, fileName);
                    observer.complete();
                }
            });
        }
        );
    }

    private saveDownloadResult(blob: Blob, fileName) {
        if (window.navigator &amp;amp;&amp;amp; window.navigator.msSaveOrOpenBlob) {
            window.navigator.msSaveOrOpenBlob(
                blob,
                fileName
            );
        }
        else {
            const windowURL = window.URL || window['webkitURL'];
            const downloadLink = document.createElement('a');
            const urlBlob = windowURL.createObjectURL(new Blob([blob]));
            downloadLink.href = urlBlob;
            downloadLink.download = fileName;
            downloadLink.click();
            setTimeout(function () { URL.revokeObjectURL(downloadLink.href) }, 4E4);
        }
    }

    private requestDownload(url: string): Observable<HttpEvent<Blob>> {
        return this.http.get(url, {
            responseType: 'blob',
            reportProgress: true,
            observe: 'events',
        })
    }
}

2) Cách sử dụng:

 Inject FileDownloadService vào component cần sử dụng chức năng download

constructor(
    private fileDownloadService: FileDownloadService,
  ) { }
  

Code callback onDownloadFile để trigger khi có event click

  public onDownloadFile(name: string, url: string ){
    this.fileDownloadService.downloadFile(url, name).subscribe((progress)=>{
	   console.log(progress);
	});
    return false;
  }

Set event click down load file cho <a> tag, chú ý thêm $event.preventDefault() để chặn việc trigger default event của <a> tag 

 <a href="{{ attachmentUrl }}" 
(click) ="onDownloadFile(attachmentUrl,attachmentName); $event.preventDefault()" target="_blank">{{attachmentName}}</a>
  

Kết luận

Giải pháp trên là dành cho việc download attach file tầm 20 MB đổ xuống. Vì content file download sẽ lưu trên RAM.

Mục đích chính là dành cho các file nhỏ user muốn click download ngay lập tức.

Đối với việc download file lớn thì nên dùng Content-Disposition response header, solution này cũng tương thích với nhiều Browser nhưng sẽ cần sửa cả bên phía Backend.

Chúc các bạn 1 ngày may mắn.

Đừng quên vote để tác giả thêm động lực viết tiếp nhé!