整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

前端上传大文件怎么处理(迟早用得到,抓紧收藏!)

前端上传大文件怎么处理(迟早用得到,抓紧收藏!)

  • 属于专栏-项目难点
  • 重要程度:★★★★★

背景

当我们在做文件的导入功能的时候,如果导入的文件过大,可能会导所需要的时间够长,且失败后需要重新上传,我们需要前后端结合的方式解决这个问题

思路

我们需要做几件事情如下:

  • 对文件做切片,即将一个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始
  • 通知服务器合并切片,在上传完切片后,前端通知服务器做合并切片操作
  • 控制多个请求的并发量,防止多个请求同时发送,造成浏览器内存溢出,导致页面卡死
  • 做断点续传,当多个请求中有请求发送失败,例如出现网络故障、页面关闭等,我们得对失败的请求做处理,让它们重复发送

实现

前端

示例代码仓库

仓库地址

步骤1-切片,合并切片

在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice通过这个方法,我们就可以对二进制文件进行拆分,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=s, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL='http://localhost:3000'
// 选中的文件
var file=null
// 选择文件
document.getElementById('fileInput').onchange=function({target: {files}}){
    file=files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick=async function(){
    if (!file) return
    // 创建切片   
    // let size=1024 * 1024 * 10 //10MB 切片大小
    let size=1024 * 50  //50KB 切片大小
    let fileChunks=[]
    let index=0 //切片序号
    for(let cur=0; cur < file.size; cur +=size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // 上传切片
    const uploadList=fileChunks.map((item, index)=> {
        let formData=new FormData()
        formData.append('filename', file.name)
        formData.append('hash', item.hash)
        formData.append('chunk', item.chunk)
        return axios({
            method: 'post',
            url: '/upload',
            data: formData
        })
    })
    await Promise.all(uploadList)
    // 合并切片
    await axios({
        method: 'get',
        url: '/merge',
        params: {
            filename: file.name
        }
    });
    console.log('上传完成')
}
</script>
</html>

步骤2-并发控制

结合Promise.race异步函数实现,多个请求同时并发的数量,防止浏览器内存溢出,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=s, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL='http://localhost:3000'
// 选中的文件
var file=null
// 选择文件
document.getElementById('fileInput').onchange=function({target: {files}}){
    file=files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick=async function(){
    if (!file) return
    // 创建切片   
    // let size=1024 * 1024 * 10; //10MB 切片大小
    let size=1024 * 50 //50KB 切片大小
    let fileChunks=[]
    let index=0 //切片序号
    for(let cur=0; cur < file.size; cur +=size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        });
    }
    // 控制并发
    let pool=[]//并发池
    let max=3 //最大并发量
    for(let i=0;i<fileChunks.length;i++){
        let item=fileChunks[i]
        let formData=new FormData()
        formData.append('filename', file.name)
        formData.append('hash', item.hash)
        formData.append('chunk', item.chunk)
        // 上传切片
        let task=axios({
            method: 'post',
            url: '/upload',
            data: formData
        })
        task.then((data)=>{
            //请求结束后将该Promise任务从并发池中移除
            let index=pool.findIndex(t=> t===task)
            pool.splice(index)
        })
        pool.push(task)
        if(pool.length===max){
            //每当并发池跑完一个任务,就再塞入一个任务
            await Promise.race(pool)
        }
    }
    //所有任务完成,合并切片
    await axios({
        method: 'get',
        url: '/merge',
        params: {
            filename: file.name
        }
    });
    console.log('上传完成')
}
</script>
</html>

步骤3-断点续传

在单个请求失败后,触发catch的方法的时候,讲当前请求放到失败列表中,在本轮请求完成后,重复对失败请求做处理,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=s, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL='http://localhost:3000'
// 选中的文件
var file=null
// 选择文件
document.getElementById('fileInput').onchange=function({target: {files}}){
    file=files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick=function(){
    if (!file) return;
    // 创建切片   
    // let size=1024 * 1024 * 10; //10MB 切片大小
    let size=1024 * 50; //50KB 切片大小
    let fileChunks=[];
    let index=0 //切片序号
    for(let cur=0; cur < file.size; cur +=size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // 控制并发和断点续传
    const uploadFileChunks=async function(list){
        if(list.length===0){
            //所有任务完成,合并切片
            await axios({
                method: 'get',
                url: '/merge',
                params: {
                    filename: file.name
                }
            });
            console.log('上传完成')
            return
        }
        let pool=[]//并发池
        let max=3 //最大并发量
        let finish=0//完成的数量
        let failList=[]//失败的列表
        for(let i=0;i<list.length;i++){
            let item=list[i]
            let formData=new FormData()
            formData.append('filename', file.name)
            formData.append('hash', item.hash)
            formData.append('chunk', item.chunk)
            // 上传切片
            let task=axios({
                method: 'post',
                url: '/upload',
                data: formData
            })
            task.then((data)=>{
                //请求结束后将该Promise任务从并发池中移除
                let index=pool.findIndex(t=> t===task)
                pool.splice(index)
            }).catch(()=>{
                failList.push(item)
            }).finally(()=>{
                finish++
                //所有请求都请求完成
                if(finish===list.length){
                    uploadFileChunks(failList)
                }
            })
            pool.push(task)
            if(pool.length===max){
                //每当并发池跑完一个任务,就再塞入一个任务
                await Promise.race(pool)
            }
        }
    }
    uploadFileChunks(fileChunks)

}
</script>
</html>

后端

步骤1.安装依赖

npm i express@4.17.2
npm i multiparty@4.2.2

步骤2.接口实现

const express=require('express')
const multiparty=require('multiparty')
const fs=require('fs')
const path=require('path')
const { Buffer }=require('buffer')
// 上传文件最终路径
const STATIC_FILES=path.join(__dirname, './static/files')
// 上传文件临时路径
const STATIC_TEMPORARY=path.join(__dirname, './static/temporary')
const server=express()
// 静态文件托管
server.use(express.static(path.join(__dirname, './dist')))
// 切片上传的接口
server.post('/upload', (req, res)=> {
    const form=new multiparty.Form();
    form.parse(req, function(err, fields, files) {
        let filename=fields.filename[0]
        let hash=fields.hash[0]
        let chunk=files.chunk[0]
        let dir=`${STATIC_TEMPORARY}/${filename}`
        // console.log(filename, hash, chunk)
        try {
            if (!fs.existsSync(dir)) fs.mkdirSync(dir)
            const buffer=fs.readFileSync(chunk.path)
            const ws=fs.createWriteStream(`${dir}/${hash}`)
            ws.write(buffer)
            ws.close()
            res.send(`${filename}-${hash} 切片上传成功`)
        } catch (error) {
            console.error(error)
            res.status(500).send(`${filename}-${hash} 切片上传失败`)
        }
    })
})
//合并切片接口
server.get('/merge', async (req, res)=> {
    const { filename }=req.query
    try {
        let len=0
        const bufferList=fs.readdirSync(`${STATIC_TEMPORARY}/${filename}`).map((hash,index)=> {
            const buffer=fs.readFileSync(`${STATIC_TEMPORARY}/${filename}/${index}`)
            len +=buffer.length
            return buffer
        });
        //合并文件
        const buffer=Buffer.concat(bufferList, len);
        const ws=fs.createWriteStream(`${STATIC_FILES}/${filename}`)
        ws.write(buffer);
        ws.close();
        res.send(`切片合并完成`);
    } catch (error) {
        console.error(error);
    }
})

server.listen(3000, _=> {
    console.log('http://localhost:3000/')
})

其他实现

如果使用腾讯云阿里云文件上传的服务,它们提供了npm库,例如腾讯云的cos-js-sdk-v5,它自身提供的切片相关的配置

SS加载确实有可能阻塞页面加载,但这并非绝对,具体取决于CSS的加载方式、应用位置以及浏览器的渲染机制。在了解CSS加载如何影响页面加载之前,我们先要明白浏览器渲染页面的基本流程。

浏览器在加载网页时,会按照从上到下的顺序解析HTML文档。当浏览器遇到`<link>`标签引用外部CSS文件时,它会停止HTML的解析,转而加载并应用这个CSS文件。这个过程被称为CSS阻塞。因此,如果这个CSS文件很大或者加载速度很慢,用户可能会看到一个空白页面,直到CSS文件完全加载并应用。

然而,有几种方法可以避免或减轻CSS加载对页面加载的阻塞:

  1. 异步加载CSS:通过将CSS文件的加载设置为异步,可以确保HTML解析不会被阻塞。这可以通过在`<link>`标签中添加`rel="async"`属性来实现。这样,浏览器会在后台加载CSS文件,而不会停止HTML的解析。
  2. 内联CSS:将CSS代码直接写在HTML文件中,而不是通过外部文件引用,可以避免网络请求造成的延迟。但是,这会增加HTML文件的大小,可能导致其他性能问题。
  3. 使用CSS-in-JS库:一些库,如Styled Components或Emotion,允许你在JavaScript中编写CSS。这种方法可以动态生成样式,但也可能增加JavaScript的复杂性。
  4. 分割CSS:将CSS文件分割成多个小文件,每个文件只包含一部分样式。这样,即使某个文件加载较慢,也不会阻塞整个页面的渲染。
  5. 利用媒体查询:通过媒体查询,你可以根据设备的特性(如屏幕大小、分辨率等)加载不同的CSS文件。这样,用户只会下载并应用他们真正需要的样式。
  6. 预加载和预获取:使用`<link rel="preload">`和`<link rel="prefetch">`可以告诉浏览器提前加载CSS文件。虽然这并不能阻止CSS阻塞,但它可以确保文件在需要时立即可用。

此外,值得注意的是,现代浏览器通常具有一些优化机制,如并行下载、缓存等,这些都可以帮助减少CSS加载对页面加载的影响。

总的来说,CSS加载确实有可能阻塞页面加载,但通过一些优化策略和技术,我们可以减轻或避免这种阻塞。选择哪种策略取决于你的具体需求和约束。

**前端实现大文件上传**

**引言:**

随着互联网技术的发展,用户在线处理大量数据的需求日益增强,其中涉及大文件上传的功能已成为许多Web应用不可或缺的一部分。然而,传统表单提交往往受限于浏览器的限制和服务器处理能力,无法很好地满足大文件高效稳定上传的需求。本文将深入探讨前端实现大文件上传的关键技术和策略,辅以实际HTML+JS代码示例,助您构建高性能、用户友好的文件上传体验。

## **一、理解浏览器上传限制**

**1.1 浏览器最大请求大小限制**

大多数现代浏览器默认允许的最大HTTP POST请求大小约为2GB到4GB不等,但具体值会受到服务器配置的影响。因此,在实现大文件上传之前,需要确保服务器端的接收限制足够高。

**1.2 超时问题**

大文件上传过程中,网络状况不佳或文件过大可能导致请求超时。对此,可通过设置合理的超时重试机制,以及使用分片上传来解决。

## **二、分片上传与断点续传**

**2.1 分片上传概念**

分片上传是将大文件分割成多个小块,独立上传每一块,最后在服务器端重组的方式。这样可以有效避免一次性上传大文件可能引发的问题。

```html

<!-- HTML 文件选择器 -->

<input type="file" id="fileInput" accept=".zip,.rar">

<script>

document.getElementById('fileInput').addEventListener('change', function(e) {

const file=e.target.files[0];

// 假设每个分片大小为1MB

const chunkSize=1 * 1024 * 1024;

// 计算分片数量

const chunks=Math.ceil(file.size / chunkSize);

for (let i=0; i < chunks; i++) {

const start=i * chunkSize;

const end=Math.min(start + chunkSize, file.size);


// 创建File Slice

const chunk=file.slice(start, end);

// 发起异步上传请求

uploadChunk(chunk, i, chunks);

}

});

function uploadChunk(chunk, index, total) {

// 这里仅展示发起上传请求的逻辑,实际需要包含chunk索引和总数量等信息

const xhr=new XMLHttpRequest();

xhr.open('POST', '/api/upload/chunk', true);

xhr.setRequestHeader('Content-Type', 'application/octet-stream');

xhr.onload=()=> {

if (xhr.status===200) {

// 上传成功处理逻辑

} else {

// 处理错误或重试

}

};

xhr.onerror=()=> {

// 错误处理

};

xhr.send(chunk);

}

</script>

```

**2.2 断点续传**

断点续传是在分片上传的基础上,记录已上传成功的分片信息,如果上传过程因网络问题中断,可以从上次失败的地方继续上传。这通常需要在客户端存储上传进度信息,并在下次上传时发送给服务器校验。

```javascript

// 假设有本地持久化存储已上传分片信息的方法

function saveUploadProgress(progressData) {

localStorage.setItem('uploadProgress', JSON.stringify(progressData));

}

// 加载已上传的分片信息

function loadUploadProgress() {

const progressData=localStorage.getItem('uploadProgress');

return progressData ? JSON.parse(progressData) : null;

}

// 在初始化上传阶段检查并恢复未完成的上传任务

const previousProgress=loadUploadProgress();

if (previousProgress) {

for (const {index, chunk} of previousProgress.unfinishedChunks) {

// 继续上传未完成的分片

uploadChunk(chunk, index, previousProgress.totalChunks);

}

}

```

## **三、前端上传组件与库推荐**

**3.1 React Dropzone Uploader**

React Dropzone Uploader是一个基于React的组件库,内置了分片上传和断点续传功能,可轻松集成至您的React项目中。

**3.2 Resumable.js**

Resumable.js 是一个轻量级、跨浏览器的大文件上传库,它支持分片上传、断点续传及自定义事件通知等功能。

## **四、实时进度显示与用户体验优化**

**4.1 实现上传进度条**

在每个分片上传完成后更新进度条,让用户体验更加直观。

```javascript

xhr.upload.onprogress=function(event) {

if (event.lengthComputable) {

const percentComplete=event.loaded / event.total;

updateProgressBar(percentComplete);

}

};

function updateProgressBar(percentage) {

// 更新页面上的进度条UI

}

```

**4.2 错误处理与提示**

对于上传过程中可能出现的各类错误,如网络中断、服务器异常等,都需要提供清晰且友好的错误提示,并赋予用户重新上传或恢复上传的能力。

总结:

前端实现大文件上传不仅涉及到技术层面的挑战,还要求关注用户体验的设计。通过合理利用分片上传、断点续传等技术,结合优秀的前端组件或库,我们可以打造出稳定可靠、易用性高的大文件上传功能,从而提升产品的综合竞争力。同时,针对不同的业务场景,还需考虑文件安全性、并发控制、队列管理等问题,确保整个上传流程的健壮性。