Express+multer 實(shí)現(xiàn)文件上傳,并在 router 中指定文件存放路徑

內(nèi)容簡(jiǎn)單說(shuō)明

??文件上傳是 web 開(kāi)發(fā)中比較常見(jiàn)的一個(gè)功能雖然說(shuō)起來(lái)是文件上傳,實(shí)際上,可以看做是對(duì) multipart/form-data 數(shù)據(jù)的處理卑雁。在 npm 中,有很多處理類(lèi)似數(shù)據(jù)的庫(kù),包括周下載量近 2kw 的 form-data,周下載量近 3mw 的 formidable镶奉。

??不過(guò),如果 nodejs 后端使用的 express 框架轿衔,其官方也有一個(gè)自己的文件上傳中間件,用它自己的話來(lái)說(shuō)就是:“Multer 是一個(gè) node.js 中間件睦疫,用于處理 multipart/form-data 類(lèi)型的表單數(shù)據(jù)害驹,它主要用于上傳文件「蛴”

??使用 multer 比較簡(jiǎn)單宛官,一般就是
????1、導(dǎo)入 multer瓦糕,
????2底洗、指定文件上傳地址(如果有必要的話,不指定只是寫(xiě)到內(nèi)存中)咕娄,
????3亥揖、在 router 的路徑后,回調(diào)函數(shù)前圣勒,寫(xiě)一個(gè)upload.single(photo)(單文件)或者upload.array('photos', 12)(多文件)费变,在 router 的回調(diào)中,就可以使用req.file 或者 req.files獲取文件了圣贸。

??在這里挚歧,因?yàn)橹付ǖ纳蟼鞯刂肥窃?multer(opts)中的 opts 配置,所以 opts 配置號(hào)一個(gè)地址之后吁峻,后續(xù)修改就不是那么方便滑负。如果需要對(duì)不同文件不同路由路徑指定不同的文件上傳地址,那應(yīng)該如何處理用含?

??multer 的簡(jiǎn)單使用后文會(huì)給個(gè)示例矮慕,但是最終的目的,是想要在 express 的 router 回調(diào)函數(shù)中啄骇,可以指定文件上傳的路徑痴鳄,而不是所有的文件都上傳到唯一指定的路徑。例如肠缔,路由是“testUpload”夏跷,我在 router 處理時(shí)指定存放到測(cè)試使用的上傳路徑。路由是“formalUpload”明未,我在處理時(shí)可以指定存放到正式的上傳路徑槽华。

express+multer 基本文件上傳示例

??因?yàn)橹饕菧y(cè)試 multer 內(nèi)容,所以一切從簡(jiǎn)趟妥,就在一個(gè)簡(jiǎn)單的 express 項(xiàng)目中測(cè)試就好

1猫态、創(chuàng)建一個(gè) express 項(xiàng)目(前提:已安裝 express-generator),并安裝 multer

express --view=ejs express-mutler-demo
// 進(jìn)入項(xiàng)目根目錄
npm i multer

2、上傳頁(yè)面編寫(xiě)

??修改 views/index.ejs 的<body>標(biāo)簽內(nèi)容如下:

  <div>
    <h3>Express + multer 簡(jiǎn)陋上傳文件</h3>
    <form method="post" action="/upload" id="upload-form" encType="multipart/form-data">
      <input id='upload' type="file" name="file" />
      <input type="submit" value="上傳">
    </form>

    <!-- 進(jìn)度條 -->
    <progress id="uploadprogress" min="0" max="100" value="0">0</progress>
    <p id='msg'></p>
  </div>

  <!-- 引入jquery.js -->
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"
    integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>

  <script>
    let form = $("#upload-form");
    form.on('submit', function (event) {

      // 清除提交結(jié)果顯示信息
      $("#msg").html("");

      // 在原頁(yè)面處理亲雪,不跳轉(zhuǎn)
      event.preventDefault();

      // 檢查是否支持FormData
      if (window.FormData) {
        let formData = new FormData();

        // 建立一個(gè)file表單項(xiàng)勇凭,值為上傳的文件
        formData.append('file', $('#upload').get(0).files[0]);
        let xhr = new XMLHttpRequest();
        xhr.open('POST', $(this).attr('action'));

        // 進(jìn)度條占比計(jì)算
        xhr.upload.onprogress = function (event) {
          if (event.lengthComputable) {
            let complete = (event.loaded / event.total * 100 | 0);
            $("#uploadprogress").val(complete);
            $("#uploadprogress").innerHTML = complete;
          }
        };
        // 定義上傳完成后的回調(diào)函數(shù)
        xhr.onload = function (e) {
          if (xhr.status === 200) {
            $("#msg").html("上傳成功!");
            // alert('上傳成功!');
          } else {
            // alert('文件上傳出錯(cuò)了义辕!')
            $("#msg").html("上傳失敗!");
          }
        };
        // 發(fā)送表單數(shù)據(jù)
        xhr.send(formData);
      }
    });
  </script>

??代碼內(nèi)容很簡(jiǎn)單虾标,就是一個(gè) form 用來(lái)模擬文件上傳,為了最簡(jiǎn)單灌砖,直接使用的 XMLHttpRequest 實(shí)現(xiàn)上傳璧函,還沒(méi)事整了個(gè)進(jìn)度條。
??本來(lái)想用原始的方法基显,還是引入了 jquery蘸吓。更簡(jiǎn)略類(lèi)似下面也 ok。

<script>
    function PostData() {
        $.ajax({
            type: "POST",
            url: "XXX",
            data : "",
            success: function(msg) {
            }
        });
        return false;
    }
</script>
<form onsubmit="return PostData()">
    <input type="text" value="">
    <input type="submit">
</form>

??依舊以第一個(gè)為準(zhǔn)撩幽,頁(yè)面大概是這個(gè)樣子(運(yùn)行 express 項(xiàng)目库继,在 localhost:3000 看到):

input畫(huà)面.png

3、multer 的簡(jiǎn)單配置

??新建一個(gè) util/Upload.js窜醉,編寫(xiě) multer 配置并導(dǎo)出:

const multer = require('multer');

// 文件上傳配置
const fileStorage = multer.diskStorage({
    destination: function (req, file, callback) {
        callback(null, "/defaultUploadDir");
    },
    filename: function (req, file, callback) {
        callback(null, file.originalname);
    }
});
// 導(dǎo)出配置
module.exports = {
    fileUpdate: multer({ 'storage': fileStorage }),
}

注意:上傳地址 "/defaultUploadDir"要先手動(dòng)創(chuàng)建宪萄,否則報(bào)錯(cuò)。

4酱虎、在對(duì)應(yīng) router 中使用 multer

??在 routes/index.js 中雨膨,添加以下 router 代碼:

router.post('/upload', upload.fileUpdate.single('file'), function (req, res, next) {

  const file = req.file;
  console.log(file);

  //如果得到了文件擂涛,就返回上傳成功
  if (file) {
    return res.status(200).json({ success: true });
  } else {
    return res.status(500).json({ success: false });
  }
});

??記得在最上面引入 multer 配置:

const upload = require('../util/Upload');

??幾個(gè)簡(jiǎn)單注意點(diǎn):
????1读串、這個(gè)路由路徑和路由方法,要和前臺(tái)頁(yè)面中的 action 和 method 一致撒妈;
????2恢暖、多文件就要 upload.array(),單文件就用 upload.single()(后續(xù)都是單文件示例中說(shuō)明);
????3、第二點(diǎn)()里面的標(biāo)志字符串要和前臺(tái)頁(yè)面中的<input id='upload' type="file" name="file" />name 屬性一致狰右。

??如果步驟都正確杰捂,成功上傳,應(yīng)該可以看到前臺(tái)頁(yè)面如下:

上傳成功.png

??router 的回調(diào)中取得上傳文件的信息棋蚌,如下:


上傳成功router取得文件信息.png

??文件上傳的位置:

第一次上傳成功后臺(tái)地址.png

關(guān)于使用 multer 文本域數(shù)據(jù)

??multer 的 readme 所說(shuō):”Multer 會(huì)添加一個(gè) body 對(duì)象 以及 file 或 files 對(duì)象 到 express 的 request 對(duì)象中嫁佳。 body 對(duì)象包含表單的文本域信息,file 或 files 對(duì)象包含對(duì)象表單上傳的文件信息谷暮≥锿“
??實(shí)際測(cè)試,在前臺(tái)頁(yè)面 index.ejs 創(chuàng)建 formData 后湿弦,append 一個(gè)文本數(shù)據(jù):

let formData = new FormData();

// 補(bǔ)入此句
formData.append('dest', 'file_upload');

??刷新頁(yè)面之后瓤漏,重新上傳,可以在 multer 配置中,在 diskStorage 的 destination 的 callback 中蔬充,可以得到 req.body 包含了 dest 屬性蝶俱。如下圖:

req.body獲取文本域數(shù)據(jù).png

??這是好事,很好的饥漫,這樣榨呆,在前臺(tái)上傳文件時(shí),就可以把需要上傳的地址放到這里庸队,那么不同的文件上傳就可以存放的不同的地址了愕提。
??那么會(huì)有哪些問(wèn)題呢?
????1皿哨、前端需要知道后臺(tái)的上傳路徑浅侨,不合理。
????2证膨、并不是所有使用 formData.append()添加的屬性都能在文件上傳 destination 生成前如输,在 req.body 中獲取到。

????這是一個(gè)實(shí)際遇到的問(wèn)題央勒,我在使用 angular 時(shí)不见,使用 HttpClient 實(shí)現(xiàn)文件上傳操作,類(lèi)似:

  upload(file: any) {
    // 文件使用FormData發(fā)送
    const formData: FormData = new FormData();
    formData.append('file', file, file.name);
    formData.append('file_name', file, file.name);
    return this.http.post(this.URL + '/upload', formData );
  }

??后臺(tái)的 req.body 在獲取到上傳的文件前并不會(huì)有 file_name 屬性的值崔步,即在 multer 配置在 diskStorage 的 destination 的 callback 中稳吮,可以得到 req.body 是空,在對(duì)應(yīng) upload 的 router 回調(diào)中井濒,才取得 req.body 的 file_name 屬性灶似。

在 router 的回調(diào)中,指定文件上傳的路徑瑞你。

??在”關(guān)于使用 multer 文本域數(shù)據(jù)“這部分有講到酪惭,前臺(tái)直接傳入文件上傳的路徑不合理,在接受到上傳的文件前得到指定的上傳路徑也不一定成功者甲,而直接使用配置好的 multer春感,其文件上傳目的地 destination 又只有固定一個(gè)。該如何實(shí)現(xiàn)虏缸?

??把 multer 的配置鲫懒,封裝到一個(gè)返回 promise 的函數(shù),指定傳入一個(gè)文件路徑參數(shù)刽辙,并在 router 的回調(diào)中使用該函數(shù)窥岩,傳入上傳路徑。

??修改 utils/Upload.js 文件扫倡,補(bǔ)入以下內(nèi)容:

// multer文件上傳,可指定上傳路徑,不在router參數(shù)里直接用
let uploadFunction = (req, res, dest) => {

    let storage = multer.diskStorage({
        destination: function (req, file, cb) {
            let newDestination = dest;
            let stat = null;
            try {
                // 檢查傳入的路徑是否存在谦秧,不存在則創(chuàng)件
                stat = fs.statSync(newDestination);
            } catch (err) {
                fs.mkdirSync(newDestination);
            }
            if (stat && !stat.isDirectory()) {
                throw new Error('文件目錄: "' + dest + '已存在竟纳!"');
            }
            cb(null, newDestination);
        },
        filename: function (req, file, callback) {
            callback(null, file.originalname);
        }
    });

    let upload = multer({
        storage: storage
    }).single('file');

    return new Promise((resolve, reject) => {
        upload(req, res, (err) => {
            if (err) {
                return reject(err);
            }
            resolve();
        })
    })
};

??記得導(dǎo)出:

module.exports = {
    fileUpdate: multer({ 'storage': fileStorage }),
    uploadFunction,
}

??在 router 中使用,修改原 routes/index.js 的 upload 路由如下:

router.post('/upload', /*upload.fileUpdate.single('file'), */ async function (req, res, next) {

  // 指定文件上傳路徑
  let uploadPath = 'test_upload';
  // 等到文件上傳完成
  await upload.uploadFunction(req, res, uploadPath);

  const file = req.file;
  console.log(req.file);
  //如果得到了文件疚鲤,就返回上傳成功
  if (file) {
    return res.status(200).json({ success: true });
  } else {
    return res.status(500).json({ success: false });
  }
});

??當(dāng)然锥累,await 需要在 async 函數(shù)中使用,也最好放到 trycatch 中集歇。

??如果步驟正確桶略,結(jié)果應(yīng)該和第一步中的一樣,文件上傳成功诲宇。在后臺(tái)的項(xiàng)目中會(huì)新建一個(gè) test_upload 文件夾际歼,并有上傳的文件。

第二次文件上傳成功位置.png

??代碼已放到 github姑蓝,有需求可查閱鹅心。

??以上內(nèi)容,全部親測(cè)有效纺荧,如果有問(wèn)題旭愧,請(qǐng)?zhí)岢鼋涣鳎x謝宙暇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末输枯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子占贫,更是在濱河造成了極大的恐慌桃熄,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件型奥,死亡現(xiàn)場(chǎng)離奇詭異瞳收,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)桩引,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)缎讼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人坑匠,你說(shuō)我怎么就攤上這事∥韵В” “怎么了厘灼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)咽瓷。 經(jīng)常有香客問(wèn)我设凹,道長(zhǎng),這世上最難降的妖魔是什么茅姜? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任闪朱,我火速辦了婚禮月匣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘奋姿。我一直安慰自己锄开,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布称诗。 她就那樣靜靜地躺著萍悴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寓免。 梳的紋絲不亂的頭發(fā)上癣诱,一...
    開(kāi)封第一講書(shū)人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音袜香,去河邊找鬼撕予。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蜈首,可吹牛的內(nèi)容都是我干的嗅蔬。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼疾就,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼澜术!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起猬腰,我...
    開(kāi)封第一講書(shū)人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸟废,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后姑荷,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體盒延,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年鼠冕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了添寺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡懈费,死狀恐怖计露,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情憎乙,我是刑警寧澤票罐,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站泞边,受9級(jí)特大地震影響该押,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜阵谚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一蚕礼、第九天 我趴在偏房一處隱蔽的房頂上張望烟具。 院中可真熱鬧,春花似錦奠蹬、人聲如沸朝聋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)玖翅。三九已至,卻和暖如春割以,著一層夾襖步出監(jiān)牢的瞬間金度,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工严沥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留猜极,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓消玄,卻偏偏與公主長(zhǎng)得像跟伏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子翩瓜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容