基於Node的React圖片上傳組件實現

寫在前面

紅旗不倒,誓把JavaScript進行到底!今天介紹個人開源項目 Royal 裏的圖片上傳組件的先後端實現原理(React + Node),花了一些時間,但願對你有所幫助。javascript

這裏寫圖片描述

前端實現

遵循React 組件化的思想,我把圖片上傳作成了一個獨立的組件(沒有其餘依賴),直接import便可。html

import React, { Component } from 'react'
import Upload from '../../components/FormControls/Upload/'

//......

render() {
    return (
        <div><Upload uri={'http://jafeney.com:9999/upload'} /></div>
    )
}

uri 參數是必須傳的,是圖片上傳的後端接口地址,接口怎麼寫下面會講到。前端

渲染頁面

組件render部分須要體現三個功能:html5

  • 圖片選取(dialog窗口)java

  • 可拖拽功能(拖拽容器)node

  • 可預覽(預覽列表)react

  • 上傳按鈕 (button)git

  • 上傳完成圖片地址和連接 (信息列表)github

render函數

render() {
    return (
        <form action={this.state.uri} method="post" encType="multipart/form-data">
            <div className="ry-upload-box">
                <div className="upload-main">
                    <div className="upload-choose">
                        <input
                            onChange={(v)=>this.handleChange(v)}
                            type="file"
                            size={this.state.size}
                            name="fileSelect"
                            accept="image/*"
                            multiple={this.state.multiple} />
                        <span ref="dragBox"
                            onDragOver={(e)=>this.handleDragHover(e)}
                            onDragLeave={(e)=>this.handleDragHover(e)}
                            onDrop={(e)=>this.handleDrop(e)}
                            className="upload-drag-area">
                            或者將圖片拖到此處
                        </span>
                    </div>
                    <div className={this.state.files.length?
                         "upload-preview":"upload-preview ry-hidden"}>
                        { this._renderPreview();   // 渲染圖片預覽列表 }
                    </div>
                </div>
                <div className={this.state.files.length?
                     "upload-submit":"upload-submit ry-hidden"}>
                     <button type="button"
                         onClick={()=>this.handleUpload()}
                         class="upload-submit-btn">
                         確認上傳圖片
                     </button>
                </div>
                <div className="upload-info">
                    { this._renderUploadInfos();   // 渲染圖片上傳信息 }
                </div>
            </div>
        </form>
    )
}

渲染圖片預覽列表

_renderPreview() {
    if (this.state.files) {
        return this.state.files.map((item, idx) => {
            return (
                <div className="upload-append-list">
                    <p>
                        <strong>{item.name}</strong>
                        <a href="javascript:void(0)"
                           className="upload-delete"
                           title="刪除" index={idx}></a>
                        <br/>
                        <img src={item.thumb} className="upload-image" />
                     </p>
                     <span className={this.state.progress[idx]?
                         "upload-progress":
                         "upload-progress ry-hidden"}>
                         {this.state.progress[idx]}
                     </span>
                 </div>
            )
        })
    } else {
        return null
    }
}

渲染圖片上傳信息列表

_renderUploadInfos() {
    if (this.state.uploadHistory) {
        return this.state.uploadHistory.map((item, idx) => {
            return (
                <p>
                    <span>上傳成功,圖片地址是:</span>
                    <input type="text" class="upload-url" value={item.relPath}/>
                    <a href={item.relPath} target="_blank">查看</a>
                 </p>
            );
        })
    } else {
        return null;
    }
}

文件上傳

前端要實現圖片上傳的原理就是經過構建FormData對象,把文件對象append()到該對象,而後掛載在XMLHttpRequest對象上 send() 到服務端。web

獲取文件對象

獲取文件對象須要藉助 input 輸入框的 change 事件來獲取 句柄參數 e

onChange={(e)=>this.handleChange(e)}

而後作如下處理:

e.preventDefault()
let target = event.target
let files = target.files
let count = this.state.multiple ? files.length : 1
for (let i = 0; i < count; i++) {
    files[i].thumb = URL.createObjectURL(files[i])
}
// 轉換爲真正的數組
files = Array.prototype.slice.call(files, 0)
// 過濾非圖片類型的文件
files = files.filter(function (file) {
    return /image/i.test(file.type)
})

這時 files 就是 咱們須要的文件對象組成的數組,把它 concat 到原有的 files裏。

this.setState({files: this.state.files.concat(files)})

如此,接下來的操做 就能夠 經過 this.state.files 取到當前已選中的 圖片文件。

利用Promise處理異步上傳

文件上傳對於瀏覽器來講是異步的,爲了處理 接下來的多圖上傳,這裏引入了 Promise來處理異步操做:

upload(file, idx) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest()
        if (xhr.upload) {
            // 上傳中
            xhr.upload.addEventListener("progress", (e) => {
                // 處理上傳進度
                this.handleProgress(file, e.loaded, e.total, idx);
            }, false)
            // 文件上傳成功或是失敗
            xhr.onreadystatechange = (e) => {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                    // 上傳成功操做
                    this.handleSuccess(file, xhr.responseText)
                    // 把該文件從上傳隊列中刪除
                    this.handleDeleteFile(file)
                    resolve(xhr.responseText);
                 } else {
                    // 上傳出錯處理 
                    this.handleFailure(file, xhr.responseText)
                    reject(xhr.responseText);
                 }
            }
        }
        // 開始上傳
        xhr.open("POST", this.state.uri, true)
        let form = new FormData()
        form.append("filedata", file)
        xhr.send(form)
    })
}

上傳進度計算

利用XMLHttpRequest對象發異步請求的好處是能夠 計算請求處理的進度,這是fetch所不具有的。
咱們能夠爲 xhr.upload 對象的 progress 事件添加事件監聽:

xhr.upload.addEventListener("progress", (e) => {
    // 處理上傳進度
    this.handleProgress(file, e.loaded, e.total, i);
}, false)

說明:idx參數是紀錄多圖上傳隊列的索引

handleProgress(file, loaded, total, idx) {
    let percent = (loaded / total * 100).toFixed(2) + '%';
    let _progress = this.state.progress;
    _progress[idx] = percent;
    this.setState({ progress: _progress })  // 反饋到DOM裏顯示
}

拖拽上傳

拖拽文件對於HTML5來講其實很是簡單,由於它自帶的幾個事件監聽機制能夠直接作這類處理。主要用到的是下面三個:

onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}

取消拖拽時的瀏覽器行爲:

handleDragHover(e) {
    e.stopPropagation()
    e.preventDefault()
}

處理拖拽進來的文件:

handleDrop(e) {
    this.setState({progress:[]})
    this.handleDragHover(e)
    // 獲取文件列表對象
    let files = e.target.files || e.dataTransfer.files
    let count = this.state.multiple ? files.length : 1
    for (let i = 0; i < count; i++) {
        files[i].thumb = URL.createObjectURL(files[i])
    }
    // 轉換爲真正的數組 
    files = Array.prototype.slice.call(files, 0)
    // 過濾非圖片類型的文件
    files = files.filter(function (file) {
        return /image/i.test(file.type)
    })
    this.setState({files: this.state.files.concat(files)})
}

多圖同時上傳

支持多圖上傳咱們須要在組件調用處設置屬性:

multiple = { true }  // 開啓多圖上傳 
size = { 50 }        // 一次最大上傳數量(雖沒有上限,爲保證服務端正常,建議50如下)

而後咱們可使用 Promise.all() 處理異步操做隊列:

handleUpload() {
    let _promises = this.state.files.map((file, idx) => this.upload(file, idx))
    Promise.all(_promises).then( (res) => {
        // 所有上傳完成 
        this.handleComplete()
    }).catch( (err) => { console.log(err) })
}

好了,前端工做已經完成,接下來就是Node的工做了。

後端實現

爲了方便,後端採用的是express框架來快速搭建Http服務和路由。具體項目見個人github node-image-upload。邏輯雖然簡單,但仍是有幾個可圈可點的地方:

跨域調用

本項目後端採用的是express,咱們能夠經過 res.header() 設置 請求的 "容許源" 來容許跨域調用:

res.header('Access-Control-Allow-Origin', '*');

設置爲 * 說明容許任何 訪問源,不太安全。建議設置成 你須要的 二級域名,如 jafeney.com

除了 "容許源" ,其餘還有 "容許頭" 、"容許域"、 "容許方法"、"文本類型" 等。經常使用的設置以下:

function allowCross(res) {
    res.header('Access-Control-Allow-Origin', '*');   
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    res.header("X-Powered-By",' 3.2.1')
    res.header("Content-Type", "application/json;charset=utf-8");
}

ES6下的Ajax請求

ES6風格下的Ajax請求和ES5不太同樣,在正式的請求發出以前都會先發一個 類型爲 OPTIONS的請求 做爲試探,只有當該請求經過之後,正式的請求才能發向服務端。

因此服務端路由 咱們還須要 處理這樣一個 請求:

router.options('*', function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    res.header("X-Powered-By",' 3.2.1')
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
});

注意:該請求一樣須要設置跨域。

處理上傳

處理上傳的圖片引人了multiparty模塊,用法很簡單:

/*使用multiparty處理上傳的圖片*/
router.post('/upload', function(req, res, next) { 
    // 生成multiparty對象,並配置上傳目標路徑
    var form = new multiparty.Form({uploadDir: './public/file/'});
    // 上傳完成後處理
    form.parse(req, function(err, fields, files) {
        var filesTmp = JSON.stringify(files, null, 2);
        var relPath = '';
        if (err) {
            // 保存失敗 
            console.log('Parse error: ' + err);
        } else {
            // 圖片保存成功!
            console.log('Parse Files: ' + filesTmp);
            // 圖片處理
            processImg(files);
        }
    });
});

圖片處理

Node處理圖片須要引入 gm 模塊,它須要用 npm 來安裝:

npm install gm --save

BUG說明

注意:node的圖形操做gm模塊前使用必須 先安裝 imagemagickgraphicsmagickLinux (ubuntu)上使用apt-get 安裝:

sudo apt-get install imagemagick
    sudo apt-get install graphicsmagick  --with-webp  // 支持webp格式的圖片

MacOS上能夠用 Homebrew 直接安裝:

brew install imagemagick
    brew install graphicsmagick --with-webp   // 支持webp格式的圖片

預設尺寸

有些時候除了原圖,咱們可能須要把原圖等比例縮小做爲預覽圖或者縮略圖。這個異步操做仍是用Promise來實現:

function reSizeImage(paths, dstPath, size) {
    return new Promise(function(resolve, reject) {
        gm(dstPath)
        .noProfile()
        .resizeExact(size)
        .write('.' + paths[1] + '@' + size + '00.' + paths[2], function (err) {
            if (!err) {
                console.log('resize as ' + size + ' ok!')
                resolve()
            }
            else {
                reject(err)
            };
        });
    });
}

重命名圖片

爲了方便排序和管理圖片,咱們按照 "年月日 + 時間戳 + 尺寸" 來命名圖片:

var _dateSymbol = new Date().toLocaleDateString().split('-').join('');
var _timeSymbol = new Date().getTime().toString();

至於圖片尺寸 使用 gmsize() 方法來獲取,處理方式以下:

gm(uploadedPath).size(function(err, size) {
    var dstPath = './public/file/' + _dateSymbol + _timeSymbol 
        + '_' + size.width + 'x' + size.height + '.' 
        + _img.originalFilename.split('.')[1];
    var _port = process.env.PORT || '9999';
        relPath = 'http://' + req.hostname + ( _port!==80 ? ':' + _port : '' ) 
        + '/file/' + _dateSymbol + _timeSymbol + '_' + size.width + 'x' 
        +  size.height + '.' + _img.originalFilename.split('.')[1];
    // 重命名
    fs.rename(uploadedPath, dstPath, function(err) {
        if (err) {
            reject(err)
        } else {
            console.log('rename ok!');
        }
    });
});

總結

對於大前端的工做,理解圖片上傳的先後端原理僅僅是淺層的。咱們的口號是 "把JavaScript進行到底!",如今不管是 ReactNative的移動端開發,仍是NodeJS的後端開發,前端工程師能夠作的工做早已不只僅是侷限於web頁面,它已經滲透到了互聯網應用層面的方方面面,或許,叫 全棧工程師 更爲貼切吧。

固然,全棧 兩個字的份量很重,不積跬步,無以致千里,功力低下的我還須要不斷修煉和實踐!

參考

張鑫旭 《基於HTML5的可預覽多圖片Ajax上傳》


@歡迎關注個人 github我的博客 -Jafeney

相關文章
相關標籤/搜索