node實現檔案分片上傳之multer篇


node實現檔案分片上傳

前端在做檔案上傳時,考慮到網速的快慢,如果檔案過大的話可能會導致上傳時間過長而請求超時,檔案上傳失敗。因此檔案過大需要對檔案進行分片上傳。

那檔案分片上傳的具體過程是怎樣的呢?

進行了許多搜尋搜尋之後,參照眾多資源進行修改,得到了自己的簡易實現流程。

首先列出來node需要用到的模組:

1
2
3
4
5
6
const express = require('express');
var multer = require('multer');
var fs=require('fs');
var path = require('path');
var app = express();
var fse = require('fs-extra');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "name": "fileloader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "start": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "art-template": "^4.13.2",
    "express": "^4.17.1",
    "express-art-template": "^1.0.1",
    "fs-extra": "^9.1.0",
    "jquery": "^3.6.0",
    "multer": "^1.4.2"
  }
}

伺服器例項採用express框架快速建置。配合art-template 和 express-art-template進行頁面處理

multer 模組用於處理檔案上傳

fs-extra模組倒不是必須的,只是參照其他的文章,採用這個模組來刪除資料夾較為方便。

multer配置介紹

multer用於作為處理檔案上傳的中介軟體,可以透過 var upload = multer({dest: '路徑'})來例項化multer,然後在路由中配置upload.single('file') 作為中介軟體。如官方的檔案介紹所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var express = require('express')
var multer  = require('multer')
var upload = multer({ dest: 'uploads/' })
 
var app = express()
 
app.post('/profile', upload.single('avatar'), function (req, res, next) {
 
})
 
app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
 
})
 
var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])

app.post('/cool-profile', cpUpload, function (req, res, next) {
 
})

官方的範例中,upload例項有三種主要的使用形式,即upload.single()、upload.array()、upload.fileds(),這三者簡單來說應該是single用於處理單檔案,array和fieds都能處理多檔案,由於專案僅演示單檔案處理過程,所以這兩個不過多贅述,想要了解更多請閱讀multer中文翻譯檔案

.single(filename) --> 接受一個以 fieldname 命名的檔案。這個檔案的訊息儲存在 req.file

另外,multer也會向req中新增body欄位,如果想要將multer作為body-parser的替代,需要配置類似如下的路由

1
2
3
app.post('/merge',upload.none(),function(req,res){

})

此外,如果前端使用jquery的ajax進行上傳,需要配置一些特殊的選項,傳遞的資料也需要是一個 FormData()物件,我不知道這是不是必須,但是我用這種方法暫時未出現問題,這也是我用multer替代body-parser 出現的問題。

當然,這只是簡單地配置,如果需要對檔案儲存路徑和檔名進行特殊的設定,需要配置 storage 引數,這是磁碟儲存引擎,可以控制檔案的儲存。

它有兩個選項可用,destinationfilename。他們都是用來確定檔案儲存位置的函式。

destination 是用來確定上傳的檔案應該儲存在哪個資料夾中。也可以提供一個 string (例如 '/tmp/uploads')。如果沒有設定 destination,則使用作業系統預設的臨時資料夾。

注意: 如果你提供的 destination 是一個函式,你需要負責建立資料夾。當提供一個字串,multer 將確保這個資料夾是你建立的。

filename 用於確定資料夾中的檔名的確定。 如果沒有設定 filename,每個檔案將設定為一個隨機檔名,並且是沒有副檔名的。

程式碼範例

前端程式碼範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>document</title>
    <style>
        body,div,p,h1,h2,h3,h4,h5,h6,ul,li{margin:0;padding:0;list-style:none;}
        ul,ol{list-style:none;}
        input{outline-style: none;padding: 0;}
        .wrap{
            width: 600px;
            height: 400px;
            background-color: lightblue;
            border-radius: 20px;
            margin: 50px auto;
        }
        label[for="file"]{
            display: inline-block;
            width: 100px;
            height: 100px;
            background-color: lightcoral;
            text-align: center;
            /* vertical-align: middle; */
            line-height: 100px;
        }
        #file{
            opacity: 0;
        }
        input[type="button"]{
            width: 80px;
            height: 32px;
            margin: 10px 10px;
            background-color: rgba(0, 0, blue, 0.5);
            outline-style: none;
        }
    </style>
</head>
<body>
    <div class="wrap">
        <div class="form">
            <label for="file">選擇檔案</label>
            <input type="file" id="file" name="file" accept="*" >
            <br>
            <input type="button" value="上傳" onclick="upload(0)">
        </div>
    </div>
    <script src="/node_modules/jquery/dist/jquery.js"></script>
    <script>
       
       var chunkSize = 1024*1024;

        var fileInput = document.querySelector("#file");

        function upload(index){
            let files = fileInput.files;
            if(!files[0]){
                alert("請選擇檔案")
                return
            }
            let file = files[0];// file物件
            //取得檔名和副檔名
            let [fname,fext] = file.name.split('.')

            // 分片
            let start = index * chunkSize
            if(start > file.size){
                merge(file.name);
                return;
            }

            let blob = file.slice(start,start + chunkSize)
            let blobName = `${fname}.${index}.${fext}`;
            let blobFile = new File([blob],blobName);

            // 上傳檔案
            let formData = new FormData()
            formData.append('file',blobFile);
            $.ajax({
                type: 'post',
                url: '/upload',
                data: formData,
                contentType: false,
                processData: false,
                success: function(res){
                    console.log(res);
                    upload(++index);//遞迴上傳檔案分片
                }
            })
        }

        // 合併檔案請求
        function merge(filename){
            var formData = new FormData()
            formData.append('name',filename)
            $.ajax({
                type: 'post',
                url:"/merge",
                data: formData,
                contentType: false,
                processData: false,
                dataType: 'json',
                success: function(res){
                    console.log(res);
                }
            })
        }
    </script>
</body>
<html>

透過input核選框,使用者選擇檔案後該元素回傳一個File物件,存在input.files中。

File物件繼承自Blob物件,擁有slice方法,回傳一個blob子物件,是file的一個切片。

得到檔案分片後,設定其分片的順序,並將其新增到一個FormData物件中。

用jquery的ajax進行上傳,需要配置兩個引數:

contentType: false,
processData: false,

不配置可能會報錯。

所有分片上傳之後,需要發送請求進行檔案合併,函式merge即為請求合併檔案的函式。

後台程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const express = require('express');
var multer = require('multer');
var fs=require('fs');
var path = require('path');
var app = express();
var fse = require('fs-extra');


// 用於檢測是否存在用於存放檔案的路徑,不存在則建立路徑
const createFolder = function(folder){
    try{
        fs.accessSync(folder);
    }catch(e){
        fs.mkdirSync(folder);
    }
};
// 檔案上傳的路徑
var uploadFolder = './uploads/';
createFolder(uploadFolder);

// 用於傳給multer進行複雜的檔案上傳配置
var storage = multer.diskStorage({
    destination: function(req,file,cb){
        // 用於進行複雜的路徑配置,此處考慮分片上傳,先將分片檔案儲存在臨時目錄中
        let [fname,index,fext] = file.originalname.split(".");
        let chunkDir = `${uploadFolder}/${fname}`;
        if(!fse.existsSync(chunkDir)){
            fse.mkdirsSync(chunkDir);
        }
        cb(null,chunkDir); //內部提供的回呼函式
    },
    filename: function(req,file,cb){
        // 根據上傳的檔名,按分片順序用分片索引命名,
        // 由於是分片檔案,請不要加副檔名,在最後檔案合併的時候再新增副檔名
        let fname = file.originalname;
        cb(null,fname.split('.')[1]);

    }
})

var upload = multer({storage: storage});// multer例項

// 配置模板引擎
app.engine('html',require('express-art-template'))
app.set('views',__dirname+'/views')

// 靜態資源路由
app.use('/node_modules',express.static('node_modules'))
app.use('/upload',express.static('uploads'))

// 主頁路由
app.get('/',function(req,res){
    res.render('index.html')
})

// 上傳路由
app.post('/upload',upload.single('file'),function(req,res,next){
    res.end("ok");
})

// 檔案合併路由
app.post('/merge',upload.none(),function(req,res){
    let name = req.body.name;
    let fname = name.split('.')[0];
    let chunkDir = path.join(uploadFolder,fname);

    let chunks = fs.readdirSync(chunkDir); // 同步讀取以防檔案合併順序混亂

    chunks.sort((a,b)=>a-b).map(chunkPath=>{
        fs.appendFileSync(
            path.join(uploadFolder,name),
            fs.readFileSync(`${chunkDir}/${chunkPath}`)
        )
    })

    fse.removeSync(chunkDir);
    res.send({msg:'合併成功',url:`http://localhost:8080/upload/${name}`});
})




app.listen(8080,()=>{
    console.log("success.....localhost:8080")
})

配置 multer的磁碟儲存引擎時, 在destination中設定了儲存分片檔案的臨時資料夾,在filename 中設定了按照分片索引來儲存檔案,不新增副檔名。

在 merge 路由中進行檔案合併,同步讀取檔案臨時目錄,用 fs 模組對檔案進行排序之後進行檔案合併。

專案地址:我的gitee倉庫