博客 / 詳情

返回

前端文件上傳方式探究&Node異步文件操作

通過FormData的方式上傳文件

簡單實現

前端代碼

<div>
  <input id="file" type="file" />
  <input type="button" value="文件上傳" onclick="uploadFile()" />
</div>
<script>
function uploadFile() {
  const file = document.getElementById('file').files[0];
  const xhr = new XMLHttpRequest();
  const fd = new FormData();
  fd.append('file', file);
  xhr.open('POST', 'http://127.0.0.1:8000/upload', true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      alert(xhr.responseText);
    }
  };
  xhr.send(fd);
}
</script>

Node接收端代碼

if(url ==='/upload' && method === 'POST') {
    //文件類型
    const arr = []
    req.on('data', (buffer) => {
      arr.push(buffer);
    })
    req.on('end', () => {
      const buffer = Buffer.concat(arr);
      const content = buffer.toString();
      const result = decodeContent(content);
      const fileName = content.match(/(?<=filename=").*?(?=")/)[0];
      fileStream(fileName).write(result);
      res.writeHead(200, {  'Content-Type': 'text/html; charset=utf-8' });
      res.end('上傳完成')
    })
  }

/**
 * @step1 過濾第一行
 * @step2 過濾最後一行
 * @step3 過濾最先出現Content-Disposition的一行
 * @step4 過濾最先出現Content-Type:的一行
 */
const decodeContent = content => {
  let lines = content.split('\n');
  const findFlagNo = (arr, flag) => arr.findIndex(o => o.includes(flag));
  // 查找 ----- Content-Disposition Content-Type 位置並且刪除
  const startNo = findFlagNo(lines, '------');
  lines.splice(startNo, 1);
  const ContentDispositionNo = findFlagNo(lines, 'Content-Disposition');
  lines.splice(ContentDispositionNo, 1);
  const ContentTypeNo = findFlagNo(lines, 'Content-Type');
  lines.splice(ContentTypeNo, 1);
  // 最後的 ----- 要在數組末往前找
  const endNo = lines.length - findFlagNo(lines.reverse(), '------') - 1;
  // 先反轉回來
  lines.reverse().splice(endNo, 1);
  return Buffer.from(lines.join('\n'));
}

基於formidable庫的文件上傳

Node.js

// server.js 
// 以下代碼在mac環境中運行成功
const http = require('http')
const formidable = require('formidable')
const fs = require('fs')

http.createServer(function(req, res) {
    if(req.url === '/uploadform') {
        const formStr = `
            <form action="fileupload" method="post" enctype="multipart/form-data">
                <input type="file" name="filetoupload">
                <input type="submit" value="Upload">
            </form>
            `

        res.writeHead(200, { 'Content-Type': "text/html; charset=UTF-8"})
        res.write(formStr)
        return res.end()
    } else if(req.url === '/fileupload') {
        const form = new formidable.IncomingForm()

        form.parse(req, (err, fields, files) => {
            const file = files.filetoupload
            const uploadPath = process.cwd() + '/upload'
            if(!fs.existsSync(uploadPath)) {
                fs.mkdirSync(uploadPath)
            }

            fs.rename(file.filepath, uploadPath + '/' + file.originalFilename, function(err) {
                if(err) throw err

                res.write('File uploaded and moved!')
                res.end()
            })
        })
    }
}).listen(8086)

安裝對應依賴包,執行node server.js命令,然後訪問localhost:8086/uploadform即可訪問文件上傳界面

Koa.js

const koa = require('koa')
const formidable = require('formidable')

const app = new koa()

app.on('error', err => {
    console.error('server error', err)
})

app.use(async (ctx, next) => {
    if (ctx.url === '/uploadform') {
        const formStr = `
            <form action="fileupload" method="post" enctype="multipart/form-data">
                <input type="file" name="filetoupload">
                <input type="submit" value="Upload">
            </form>
            `

        ctx.status = 200
        ctx.body = formStr
        ctx.set('Content-Type', 'text/html; charset=UTF-8')
    } else if (ctx.url === '/fileupload') {
        const form = formidable({});

        await new Promise((resolve, reject) => {
            form.parse(ctx.req, (err, fields, files) => {
                if (err) {
                    reject(err);
                    return;
                }

                ctx.set('Content-Type', 'application/json');
                ctx.status = 200;
                ctx.state = { fields, files };
                ctx.body = JSON.stringify(ctx.state, null, 2);
                resolve();
            });
        });

        await next();
    }
})

app.use((ctx) => {
    console.log('The next middleware is called');
    console.log('Results:', ctx.state);
});

app.listen(3000, () => {
    console.log('Server listening on http://localhost:3000 ...');
});

上傳文件時對文件進行操作

有時候我們在上傳文件時,需要對文件進行一些操作,等待這些操作執行成功後,才能返回結果。比如上傳一個包含多個UTF-8文本文件的zip壓縮包,在上傳後對文件進行解壓,然後讀取解壓後文件的內容。

const fs = require('fs')
const util = require('util')
const stream = require('stream')
const pipeline = util.promisify(stream.pipeline)
const compressing = require('compressing')
const fsExtra = require('fs-extra')

// 基於koa.js
app.use(async (ctx) {
  if(ctx.url === '/fileupload') {
      const file = ctx.request.files.uploadFile
    const reader = fs.createReadStream(file.filepath, { encoding: 'binary'})        

    // 寫入二進制文件(zip)
    const filePath = process.cwd() + '/public/upload'
    if(!fs.existsSync(filePath)) {
      fs.mkdirSync(filePath)
    }

    const fileName = decodeURIComponent(file.originalFilename)
    const writer = fs.createWriteStream(filePath + '/' + fileName, { encoding: 'binary'})
    await pipeline(reader, writer)

    // 解壓zip文件
    const unzipPath = filePath + '/' + fileName.substring(0, fileName.length - 4)
    if(!fs.existsSync(unzipPath)) {
      fs.mkdirSync(unzipPath)
    }

    await compressing.zip.uncompress(filePath + '/' + fileName, unzipPath)


    // 讀取解壓後的文件的文本內容
    const files = fs.readdirSync(unzipPath)
    for(let i = 0; i < files.length; i++) {
      const unzipFile = files[i]
      const fileStat = fs.statSync(unzipPath + '/' + unzipFile)
      if(fileStat.isFile()) {
        const content = fs.readFileSync(unzipPath + '/' + unzipFile, { encoding: 'utf-8' })
        console.log(content)
      }
    }

    // 刪除zip文件和解壓後的文件
    fs.unlink(filePath + '/' + fileName, (err) => {
      if(err) {
        console.log(err)
      }
    })

    fsExtra.remove(unzipPath, (err) => {
      if(err) {
        console.log(err)
      }
    })

    ctx.status = 200
    }
})

不使用FormData的方式來上傳文件

直接上傳File實例

From表單是一種規範,我就不遵守規範可以嗎?當然可以,不遵守規範即代表你用了新的規範,或者説不叫規範,而是一種前後端都認可的方式,只要你的後端支持就好。

文件上傳的實質是上傳文件的內容以及文件的格式,當我們使用HTML提供的Input上傳文件的時候,它將文件的內容讀進內存裏,那我們直接將內存裏的數據當成普通的數據提交到服務端可以麼?看下面的例子。

<!-- 前端代碼:-->
<div>
  <input id="file" type="file" />
  <input type="button" value="文件上傳" onclick="uploadFile()" />
</div>
<script>
  function uploadFile() {
    const file = document.getElementById('file').files[0];
    const xhr = new XMLHttpRequest();
    xhr.open('POST', `http://127.0.0.1:8000/upload?name=${file.name}`, true);
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
      }
      }
    xhr.send(file);
  }
</script>

// Nodejs服務端代碼
...
if(reqUrl.pathname ==='/upload' && method === 'POST') {
  const fileName = qs.parse(reqUrl.query).name;
  req.pipe(fileStream(fileName));
  req.on('end', () => {
    res.writeHead(200, {  'Content-Type': 'text/html; charset=utf-8' });
    res.end('上傳完成');
  })
}
...

File

賦值給file變量的是一個File對象構造的實例,File對象繼承自Blob,並擴展了與文件系統相關的功能

FileReader

FileReader 是一個對象,其唯一目的是從 Blob(因此也從 File)對象中讀取數據。

讀取數據的方法有以下四種:

  1. readAsArrayBuffer()
  2. readAsBinaryString()
  3. readAsDataURL()
  4. readAsText()

讀取文件示例:

<input type="file" onchange="readFile(this)">

<script>
function readFile(input) {
  let file = input.files[0];

  let reader = new FileReader();

  reader.readAsText(file);

  reader.onload = function() {
    console.log(reader.result);
  };

  reader.onerror = function() {
    console.log(reader.error);
  };

}
</script>

使用FileReader的readAsDataURL方法讀取文件內容轉換為Base64字符串後上傳

參考:

Node.js 文件上傳

揭秘前端文件上傳原理(一)

揭秘前端文件上傳原理(二)

File 和 FileReader

通過異步迭代簡化 Node.js 流

Node.js stream

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.