js 浏览器下载显示进度

项目中下载相关的需求比较多,下载的时候需要更改文件名称。这里有两种方法,一种是通过blob转换,通过a标签下载;另一种是通过插件streamSaver和fetch进行下载。

说下优缺点

  1. 方案一
  • a标签进行下载,如果不用更改文件名称,那就比较方便,并且不需要进行blob转换。但是如果是下载视频录像等,使用a标签就会先打开新的页面,在进行点击下载操作。操作流程比较长。
  • 如果使用a标签并且需要更改文件名称,就需要先进行blob转换操作,通过XMLHttpRequest进行转换,如果是视频类或者文件内容比较大的情况下,耗时长并且页面长时间无响应变化,可能造成用户多次触发下载操作。
  • 下载过程中无进度显示,转成blob之后秒下载,但是转的过程耗时长。

代码如下:

// 无需换名
function downloadFileByUrl(url) {
    var loadBtn = document.createElement('a');
    document.body.appendChild(loadBtn);
    loadBtn.setAttribute('href', url);
    // 如果是pdf格式,则打开新页面预览后下载
    if(url.indexOf('pdf')>-1) {
        loadBtn.setAttribute('target', '_blank');
    }
    loadBtn.setAttribute('download', '');
    loadBtn.click();
    document.body.removeChild(loadBtn)
}

// 需要换名
function download(url, filename) {
    getBlob(url, function(blob) {
        saveAs(blob, filename);
    });
};
// 转blob
function getBlob(url,cb) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'blob';
        xhr.onload = function() {
                if (xhr.status === 200) {
                    cb(xhr.response);
                }
        };
        xhr.send();
}


 
function saveAs(blob, filename) {
    if (window.navigator.msSaveOrOpenBlob) {
            navigator.msSaveBlob(blob, filename);
    } else {
            var link = document.createElement('a');
            var body = document.querySelector('body');

            link.href = window.URL.createObjectURL(blob);
            link.download = filename;

            // fix Firefox
            link.style.display = 'none';
            body.appendChild(link);
            
            link.click();
            body.removeChild(link);

            window.URL.revokeObjectURL(link.href);
    };
}



  1. 方案2
  • 使用streamSaver进行下载,页面会显示当前下载进度及总共下载大小,页面空白等待时间短,用户能看到下载操作正在进行。在这里插入图片描述

  • 缺点是 页面会有一个小弹框。
    总体来说使用streamSaver插件要比方案一体验更好。

代码如下

// 引入streamSaver
<script src="/js/streamSaver.js" type="text/javascript"></script>
				fetch(url, {
                    method: 'GET',
                    cache: 'no-cache'
                }).then(res=> {
                    const fileStream = streamSaver.createWriteStream(name+'.mp4', {
                        size : res.headers.get("content-length")
                    })
                    const readableStream = res.body
                    // more optimized
                    if (window.WritableStream && readableStream.pipeTo) {
                        return readableStream.pipeTo(fileStream)
                            .then(() => console.log('done writing'))
                    }
                    window.writer = fileStream.getWriter()

                    const reader = res.body.getReader()
                    const pump = () => reader.read()
                        .then(res => res.done
                            ? window.writer.close()
                            : window.writer.write(res.value).then(pump))

                    pump()
                })

streamSaver插件地址

streamSaver.js源码如下,github进不去的同学可以直接复制。

/* global chrome location ReadableStream define MessageChannel TransformStream */

;((name, definition) => {
    typeof module !== 'undefined'
        ? module.exports = definition()
        : typeof define === 'function' && typeof define.amd === 'object'
        ? define(definition)
        : this[name] = definition()
})('streamSaver', () => {
    'use strict'

    const global = typeof window === 'object' ? window : this
    if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread')

    let mitmTransporter = null
    let supportsTransferable = false
    const test = fn => { try { fn() } catch (e) {} }
    const ponyfill = global.WebStreamsPolyfill || {}
    const isSecureContext = global.isSecureContext
    // TODO: Must come up with a real detection test (#69)
    let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint
    const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
        ? 'iframe'
        : 'navigate'

    const streamSaver = {
        createWriteStream,
        WritableStream: global.WritableStream || ponyfill.WritableStream,
        supported: true,
        version: { full: '2.0.5', major: 2, minor: 0, dot: 5 },
        mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0'
    }

    /**
     * create a hidden iframe and append it to the DOM (body)
     *
     * @param  {string} src page to load
     * @return {HTMLIFrameElement} page to load
     */
    function makeIframe (src) {
        if (!src) throw new Error('meh')
        const iframe = document.createElement('iframe')
        iframe.hidden = true
        iframe.src = src
        iframe.loaded = false
        iframe.name = 'iframe'
        iframe.isIframe = true
        iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
        iframe.addEventListener('load', () => {
            iframe.loaded = true
        }, { once: true })
        document.body.appendChild(iframe)
        return iframe
    }

    /**
     * create a popup that simulates the basic things
     * of what a iframe can do
     *
     * @param  {string} src page to load
     * @return {object}     iframe like object
     */
    function makePopup (src) {
        const options = 'width=200,height=100'
        const delegate = document.createDocumentFragment()
        const popup = {
            frame: global.open(src, 'popup', options),
            loaded: false,
            isIframe: false,
            isPopup: true,
            remove () { popup.frame.close() },
            addEventListener (...args) { delegate.addEventListener(...args) },
            dispatchEvent (...args) { delegate.dispatchEvent(...args) },
            removeEventListener (...args) { delegate.removeEventListener(...args) },
            postMessage (...args) { popup.frame.postMessage(...args) }
        }

        const onReady = evt => {
            if (evt.source === popup.frame) {
                popup.loaded = true
                global.removeEventListener('message', onReady)
                popup.dispatchEvent(new Event('load'))
            }
        }

        global.addEventListener('message', onReady)

        return popup
    }

    try {
        // We can't look for service worker since it may still work on http
        new Response(new ReadableStream())
        if (isSecureContext && !('serviceWorker' in navigator)) {
            useBlobFallback = true
        }
    } catch (err) {
        useBlobFallback = true
    }

    test(() => {
        // Transferable stream was first enabled in chrome v73 behind a flag
        const { readable } = new TransformStream()
        const mc = new MessageChannel()
        mc.port1.postMessage(readable, [readable])
        mc.port1.close()
        mc.port2.close()
        supportsTransferable = true
        // Freeze TransformStream object (can only work with native)
        Object.defineProperty(streamSaver, 'TransformStream', {
            configurable: false,
            writable: false,
            value: TransformStream
        })
    })

    function loadTransporter () {
        if (!mitmTransporter) {
            mitmTransporter = isSecureContext
                ? makeIframe(streamSaver.mitm)
                : makePopup(streamSaver.mitm)
        }
    }

    /**
     * @param  {string} filename filename that should be used
     * @param  {object} options  [description]
     * @param  {number} size     deprecated
     * @return {WritableStream<Uint8Array>}
     */
    function createWriteStream (filename, options, size) {
        let opts = {
            size: null,
            pathname: null,
            writableStrategy: undefined,
            readableStrategy: undefined
        }

        let bytesWritten = 0 // by streamSaver.js (not the service worker)
        let downloadUrl = null
        let channel = null
        let ts = null

        // normalize arguments
        if (Number.isFinite(options)) {
            [ size, options ] = [ options, size ]
            console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')
            opts.size = size
            opts.writableStrategy = options
        } else if (options && options.highWaterMark) {
            console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')
            opts.size = size
            opts.writableStrategy = options
        } else {
            opts = options || {}
        }
        if (!useBlobFallback) {
            loadTransporter()

            channel = new MessageChannel()

            // Make filename RFC5987 compatible
            filename = encodeURIComponent(filename.replace(/\//g, ':'))
                .replace(/['()]/g, escape)
                .replace(/\*/g, '%2A')

            const response = {
                transferringReadable: supportsTransferable,
                pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
                headers: {
                    'Content-Type': 'application/octet-stream; charset=utf-8',
                    'Content-Disposition': "attachment; filename*=UTF-8''" + filename
                }
            }

            if (opts.size) {
                response.headers['Content-Length'] = opts.size
            }

            const args = [ response, '*', [ channel.port2 ] ]

            if (supportsTransferable) {
                const transformer = downloadStrategy === 'iframe' ? undefined : {
                    // This transformer & flush method is only used by insecure context.
                    transform (chunk, controller) {
                        if (!(chunk instanceof Uint8Array)) {
                            throw new TypeError('Can only write Uint8Arrays')
                        }
                        bytesWritten += chunk.length
                        controller.enqueue(chunk)

                        if (downloadUrl) {
                            location.href = downloadUrl
                            downloadUrl = null
                        }
                    },
                    flush () {
                        if (downloadUrl) {
                            location.href = downloadUrl
                        }
                    }
                }
                ts = new streamSaver.TransformStream(
                    transformer,
                    opts.writableStrategy,
                    opts.readableStrategy
                )
                const readableStream = ts.readable

                channel.port1.postMessage({ readableStream }, [ readableStream ])
            }

            channel.port1.onmessage = evt => {
                // Service worker sent us a link that we should open.
                if (evt.data.download) {
                    // Special treatment for popup...
                    if (downloadStrategy === 'navigate') {
                        mitmTransporter.remove()
                        mitmTransporter = null
                        if (bytesWritten) {
                            location.href = evt.data.download
                        } else {
                            downloadUrl = evt.data.download
                        }
                    } else {
                        if (mitmTransporter.isPopup) {
                            mitmTransporter.remove()
                            mitmTransporter = null
                            // Special case for firefox, they can keep sw alive with fetch
                            if (downloadStrategy === 'iframe') {
                                makeIframe(streamSaver.mitm)
                            }
                        }

                        // We never remove this iframes b/c it can interrupt saving
                        makeIframe(evt.data.download)
                    }
                }
            }

            if (mitmTransporter.loaded) {
                mitmTransporter.postMessage(...args)
            } else {
                mitmTransporter.addEventListener('load', () => {
                    mitmTransporter.postMessage(...args)
                }, { once: true })
            }
        }

        let chunks = []

        return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
            write (chunk) {
                if (!(chunk instanceof Uint8Array)) {
                    throw new TypeError('Can only write Uint8Arrays')
                }
                if (useBlobFallback) {
                    // Safari... The new IE6
                    // https://github.com/jimmywarting/StreamSaver.js/issues/69
                    //
                    // even though it has everything it fails to download anything
                    // that comes from the service worker..!
                    chunks.push(chunk)
                    return
                }

                // is called when a new chunk of data is ready to be written
                // to the underlying sink. It can return a promise to signal
                // success or failure of the write operation. The stream
                // implementation guarantees that this method will be called
                // only after previous writes have succeeded, and never after
                // close or abort is called.

                // TODO: Kind of important that service worker respond back when
                // it has been written. Otherwise we can't handle backpressure
                // EDIT: Transferable streams solves this...
                channel.port1.postMessage(chunk)
                bytesWritten += chunk.length

                if (downloadUrl) {
                    location.href = downloadUrl
                    downloadUrl = null
                }
            },
            close () {
                if (useBlobFallback) {
                    const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
                    const link = document.createElement('a')
                    link.href = URL.createObjectURL(blob)
                    link.download = filename
                    link.click()
                } else {
                    channel.port1.postMessage('end')
                }
            },
            abort () {
                chunks = []
                channel.port1.postMessage('abort')
                channel.port1.onmessage = null
                channel.port1.close()
                channel.port2.close()
                channel = null
            }
        }, opts.writableStrategy)
    }

    return streamSaver
})
  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值