文件上传与下载

2024-03-20  本文已影响0人  欢西西西
new File([u8arr], filename, { type: 'image/jpeg' });

// Blob对象通常用于存储大量二进制数据,可以包含任意类型的数据
new Blob([data], { type: 'application/octet-stream; charset=utf-8' })

// Uint8Array是ArrayBuffer的一种视图类型,是一个类数组对象,
// 每项都是一个8 位无符号整数值,可以直接读取和写入二进制数据。
u8ay = new Uint8Array(arraybuffer);
u8ay[0] = 10
// u8ay.buffer === arraybuffer
let formData = new FormData();
formData.append("Content-Disposition", `attachment; filename="${fileName}"`);
formData.append('key', key);
formData.append('file', file);
image.png readAsArrayBuffer.png

1. 如何将input选择的图片展示在页面上

需要使用fileReader来转换文件:readAsDataURL

const reader = new FileReader();
reader.readAsDataURL(input.files[0]); // input.files 是一个FileList ,里面可以包含多个File文件
reader.onload = function (e) {
    console.log(e.target.result); // 拿到base64,再赋值给img节点的src
}

2. 分片上传 - 本文最下面有伪代码

分片上传视频

image.png
  1. slice将文件切分为多个Blob类型对象,存储在数组里
  2. 计算文件的哈希值,先询问服务端,该文件是否上传过,上传到第几片了
  3. 按分片顺序,循环-依次传给服务器(携带文件的哈希值,用以标识文件),同时携带当前处于第几分片
  4. 当最后一片上传成功后,发送一个结束上传的请求 - 携带文件哈希值- 后端返回文件地址

3. md5计算文件的哈希值——用以唯一标识文件

4. 断点续传的触发节点是什么?

4.1. 选择文件上传,服务端检查发现此文件以前上传过一部分(可能原因:1. 之前上传过程中用户关闭页面 2. 之前上传过程中网络故障 3. 之前上传手动暂停)
4.2. 用户手动点了暂停,再点击继续,从暂停的部分继续上传
4.3. window监听online事件,发现有未完成的上传任务,继续上传(例如断网导致上传中断,前端记录了断点信息。但现在遇到的用户的操作一般不是单纯的上传,上传完后要拿到url去做后续的操作,例如把文件地址保存到某个单据上, 但如果断网导致上次的用户操作已经中断了,现在即使续传完毕,也不知道把拿到的url怎么办了。既然用户操作中断,下次应该会重新操作,在重新上传文件时触发第一种情况下的断点续传,我觉得这样更合理一点,而不是online时自动继续上传)

5. 上传进度

6. 粘贴图片后上传

6.1 监听paste事件,获取clipboardData

el.addEventListener('paste', function (e) {
    const cbd = e.clipboardData;
    if (!(cbd && cbd.items)) {
        return;
    }
    let pasteItems = cbd.items;
}, false);

6.2 如果直接能获取到文件数据:getAsFile可以拿到file类型,此时就可以上传了,用 FileReader读取base64后也能展示到页面上

for (let i = 0, lth = pasteItems.length; i < lth; i++) {
    let item = pasteItems[i];
    if (item.kind === 'file') {
        let file = item.getAsFile(); // 第一步:getAsFile
        if (!file || file.size === 0) {
            continue;
        }
        // 第二步:这里再用 FileReader 来读这个 blob: readAsDataURL,结果就能作为 src 赋值给 img 了
    }
}
image.png

6.3 否则尝试以html格式解析文本:getAsStringnew DOMParser().parseFromString(a, 'text/html'),拿到内部img标签的src属性

image.png
for (let i = 0, lth = pasteItems.length; i < lth; i++) {
    pasteItems[i].getAsString(function (a) {
        if (!a) {
            return;
        }
        let nodes = new DOMParser().parseFromString(a, 'text/html'), // 这步很重要
            imgDomList = nodes.getElementsByTagName('img'); // 获取IMG DOM
        // 接下来就可以取 IMG 节点的 src 属性了
    });
}

从 img dom 取出来的 src 可能是:

  1. file:///C:/Users/cache/Image/2008208101184.png —— 【本地文件系统】的地址
    (本地路径我们是无法展示的,目前借助客户端,先把本地路径给客户端,客户端上传后返给我们在线链接)
  2. https://upload-images.jianshu.io/upload_images/1552a9bb5bd4.png —— 【在线】地址

7. 在浏览器地址栏输入文件地址时,为什么有的是预览,有的直接会下载

服务器可以通过设置响应头来指定文件的 MIME 类型和处理方式。如果服务器发送的响应头中包含了 "Content-Disposition: attachment",则浏览器会将其视为需要下载的文件,而不是直接打开预览。

image.png image.png

8. 为什么有的网站图片只能在这个网站内部才能访问?

有的网站设置了防盗链:

9. 下载碰到过的一个问题

image.png

下载文件时发现含有中文逗号的无法下载。而这个报错码表示服务器在响应头部中设置了多个 Content-Disposition 字段。

原因:上传文件的时候会把到时下载要用的Content-Disposition头的值传给后端,但是由于filename没有包双引号,导致设置请求头的时候变成 a=xxxx,aaaa,文件名中的逗号当作属性分割了。

修改:上传时用双引号包上文件名,前后对比:

- image.png - image.png -

10. StreamSaver流式下载文件,并读取下载进度

(参见另一篇 service worker

const fileStream = streamSaver.createWriteStream(fileName); // WritableStream实例
const writer = fileStream.getWriter();
fetch(url).then(res => {
    const totalSize = parseInt(res.headers.get('content-length'));
    let loadedSize = 0;
    const readableStream = res.body;
    const reader = readableStream.getReader();
    reader.read().then(function processResult(result) {
        if (result.done) {
            writer.close(); // 下载完成
            return;
        }
        loadedSize += result.value.length;
        console.log(`下载进度:${loadedSize / totalSize * 100}%`);

        // 读过进度之后,可以继续写入可写流当中
        writer.write(result.value).catch(error => {
            writer.close();
        });
        return reader.read().then(processResult);
    });
})

11. 分片上传伪代码

const STATE_UPLOADING = 'sending', // 正在上传
    STATE_STOP = 'stop', // 各种原因导致的中断
    STATE_SUCCESS = 'success'; // 成功

const CHUNK_SIZE = 10 * 1024 * 1024; // 切成10M
// const CHUNK_SIZE = 10 * 1024; // 切成10K
const uploadFile = Symbol('uploadFile');
const continueUpload = Symbol('continueUpload');
const compPort = Symbol('componentPort');
const changeState = Symbol('changeState');
const handleUploadFail = Symbol('handleUploadFail');
const updatePercent = Symbol('updatePercent');

const tools = {
    calcSign(file) { },
    getFileUrl(fileSign, fileSize) {  },
    postToServer(fileSign, chunkIndex, chunkFile, signal) { },
    checkFileFromServer(fileSign) { }
};

export default class Upload {
    state = STATE_UPLOADING
    stopChunkIndex = 0
    fileSign = null // 真正用的地方就continueUpload一个,调用位置在:初始化、从暂停处继续
    file = null
    fileUrl = ''
    messagePort = null
    percent = 0
    constructor(f, options = {}) {
        this.file = f;
        this.fileSign = options.fileSign || tools.calcSign(f);
        if (options.startIndex) {
            this.stopChunkIndex = options.startIndex;
        }
        this[uploadFile]();

        // 接下来用于实例之间通信:去通知实例上传状态发生变化、进度发生变化
        // 实例需要知道的话就用messagePort去监听message事件
        const { port1, port2 } = new MessageChannel();
        this[compPort] = port1;
        this.messagePort = port2;
    }
    // 暂停上传
    suspend() {
        this[changeState](STATE_STOP);
        this.abortController.abort();
    }
    // 继续上传
    proceed() {
        if (this.state === STATE_STOP) {
            this[changeState](STATE_UPLOADING);
            this[continueUpload]().catch((...args) => {
                this[handleUploadFail](...args);
            });
        }
    }
    // 上传文件,成功则resolve url,失败会reject error
    async [uploadFile]() {
        // 检查这个文件是否上传过
        let res = await tools.checkFileFromServer(this.fileSign);

        // 已上传完毕:秒传
        if (res.url) {
            this.fileUrl = res.url;
            this[updatePercent](100);
            this[changeState](STATE_SUCCESS);
            return res.url;
        }
        // 上传了一部分:从上次位置续传
        if (res.uploadedChunkCount > 0) {
            this.stopChunkIndex = res.uploadedChunkCount;
        }
        // 没传过:传
        return this[continueUpload]().catch((...args) => {
            this[handleUploadFail](...args);
        });
    }
    async [continueUpload]() {
        this.abortController = new AbortController();
        const startIndex = this.stopChunkIndex;
        const fileSize = this.file.size;
        let transferSize = startIndex * CHUNK_SIZE, // 已上传的部分
            chunkIndex = startIndex;
        let isErr = false;
        while (transferSize < fileSize) {
            if (this.state === STATE_STOP) { // 暂停
                this.stopChunkIndex = chunkIndex;
                return Promise.reject({ code: 300 });
            }
            let chunkFile = this.file.slice(transferSize, transferSize + CHUNK_SIZE);
            try {
                let res = await tools.postToServer(this.fileSign, chunkIndex, chunkFile, this.abortController.signal);
                if (res.ok) {
                    transferSize += chunkFile.size;
                    chunkIndex++;
                    this[updatePercent](transferSize / fileSize * 100);
                } else {
                    isErr = true;
                    break;
                }
            } catch (err) {
                console.log('fetch error:', err);
                isErr = true;
                break;
            }
        }
        if (isErr) { // 分片上传中断
            this.stopChunkIndex = chunkIndex;
            return Promise.reject({ code: 100 });
        }
        // 上传完成,获取下载地址
        return tools.getFileUrl(this.fileSign, fileSize).then(url => {
            this.fileUrl = url;
            this[changeState](STATE_SUCCESS);
            return url;
        }).catch(() => {
            return Promise.reject({ code: 400, });
        });
    }
    [changeState](state) {
        if (this.state === state) {
            return;
        }
        // 成功了就不能再变更状态了
        if (this.state === STATE_SUCCESS) {
            return;
        }
        this.state = state;
        this[compPort].postMessage({ action: 'statechange', state });
    }
    // 该文件的上传进度
    [updatePercent](value) {
        value = parseInt(value);
        value = Math.min(value, 100);
        this[compPort].postMessage({ action: 'progress', value, text: value + '%' });
    }
    // 统一处理上传失败
    [handleUploadFail](err) {
        const errInfo = {
            '100': '分片上传中断',
            '400': '已全部上传,但获取文件地址失败',
            '300': '用户暂停上传'
        };
        this[changeState](STATE_STOP);
        if (err.code === 100) {
            //  存到localStorage,便于下次续传,记录file、fileSign、断点位置
            //  在合适的节点监听online-如果localStorage有未完成的任务则继续上传 -> new Upload(file, {fileSign, startIndex})
            // localStorage.setItem();
            return;
        }
    }
};

12. 遇到的问题记录:

问题1:通知实例状态的更新和进度的更新
→ 现在用的MessageChannel,给实例一个port让它自己去监听messgae

问题2:点了暂停之后进度条还会再更新一次
→ 因为我是在下一个分片发送前才校验并停止发送请求,所以即使点了暂停,本次分片还是会上传完,并且更新一次进度。所以如果在最后一个分片正在上传时点暂停,实际上是没有用的,你看着进度条没满的时候点的暂停,但上传结束了。
→ 所以点暂停时应该abort当前正在发生的请求(给请求一个abortController.signal,在点击暂停时调用abortController.abort()

上一篇 下一篇

猜你喜欢

热点阅读