基于Node的React圖片上傳組件實(shí)現(xiàn)實(shí)例代碼
寫在前面
紅旗不倒,誓把JavaScript進(jìn)行到底!今天介紹我的開源項(xiàng)目 Royal 里的圖片上傳組件的前后端實(shí)現(xiàn)原理(React + Node),花了一些時(shí)間,希望對(duì)你有所幫助。

前端實(shí)現(xiàn)
遵循React 組件化的思想,我把圖片上傳做成了一個(gè)獨(dú)立的組件(沒有其他依賴),直接import即可。
import React, { Component } from 'react'
import Upload from '../../components/FormControls/Upload/'
//......
render() {
return (
<div><Upload uri={'http://jafeney.com:9999/upload'} /></div>
)
}
uri 參數(shù)是必須傳的,是圖片上傳的后端接口地址,接口怎么寫下面會(huì)講到。
渲染頁(yè)面
組件render部分需要體現(xiàn)三個(gè)功能:
- 圖片選取(dialog窗口)
- 可拖拽功能(拖拽容器)
- 可預(yù)覽(預(yù)覽列表)
- 上傳按鈕 (button)
- 上傳完成圖片地址和鏈接 (信息列表)
主render函數(shù)
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(); // 渲染圖片預(yù)覽列表 }
</div>
</div>
<div className={this.state.files.length?
"upload-submit":"upload-submit ry-hidden"}>
<button type="button"
onClick={()=>this.handleUpload()}
class="upload-submit-btn">
確認(rèn)上傳圖片
</button>
</div>
<div className="upload-info">
{ this._renderUploadInfos(); // 渲染圖片上傳信息 }
</div>
</div>
</form>
)
}
渲染圖片預(yù)覽列表
_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)" rel="external nofollow"
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;
}
}
文件上傳
前端要實(shí)現(xiàn)圖片上傳的原理就是通過構(gòu)建FormData對(duì)象,把文件對(duì)象append()到該對(duì)象,然后掛載在XMLHttpRequest對(duì)象上 send() 到服務(wù)端。
獲取文件對(duì)象
獲取文件對(duì)象需要借助 input 輸入框的 change 事件來(lái)獲取 句柄參數(shù) 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])
}
// 轉(zhuǎn)換為真正的數(shù)組
files = Array.prototype.slice.call(files, 0)
// 過濾非圖片類型的文件
files = files.filter(function (file) {
return /image/i.test(file.type)
})
這時(shí) files 就是 我們需要的文件對(duì)象組成的數(shù)組,把它 concat 到原有的 files里。
this.setState({files: this.state.files.concat(files)})
如此,接下來(lái)的操作 就可以 通過 this.state.files 取到當(dāng)前已選中的 圖片文件。
利用Promise處理異步上傳
文件上傳對(duì)于瀏覽器來(lái)說(shuō)是異步的,為了處理 接下來(lái)的多圖上傳,這里引入了 Promise來(lái)處理異步操作:
upload(file, idx) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
if (xhr.upload) {
// 上傳中
xhr.upload.addEventListener("progress", (e) => {
// 處理上傳進(jìn)度
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)
// 把該文件從上傳隊(duì)列中刪除
this.handleDeleteFile(file)
resolve(xhr.responseText);
} else {
// 上傳出錯(cuò)處理
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)
})
}
上傳進(jìn)度計(jì)算
利用XMLHttpRequest對(duì)象發(fā)異步請(qǐng)求的好處是可以 計(jì)算請(qǐng)求處理的進(jìn)度,這是fetch所不具備的。
我們可以為 xhr.upload 對(duì)象的 progress 事件添加事件監(jiān)聽:
xhr.upload.addEventListener("progress", (e) => {
// 處理上傳進(jìn)度
this.handleProgress(file, e.loaded, e.total, i);
}, false)
說(shuō)明:idx參數(shù)是紀(jì)錄多圖上傳隊(duì)列的索引
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里顯示
}
拖拽上傳
拖拽文件對(duì)于HTML5來(lái)說(shuō)其實(shí)非常簡(jiǎn)單,因?yàn)樗詭У膸讉€(gè)事件監(jiān)聽機(jī)制可以直接做這類處理。主要用到的是下面三個(gè):
onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}
取消拖拽時(shí)的瀏覽器行為:
handleDragHover(e) {
e.stopPropagation()
e.preventDefault()
}
處理拖拽進(jìn)來(lái)的文件:
handleDrop(e) {
this.setState({progress:[]})
this.handleDragHover(e)
// 獲取文件列表對(duì)象
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])
}
// 轉(zhuǎn)換為真正的數(shù)組
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)})
}
多圖同時(shí)上傳
支持多圖上傳我們需要在組件調(diào)用處設(shè)置屬性:
multiple = { true } // 開啟多圖上傳
size = { 50 } // 一次最大上傳數(shù)量(雖沒有上限,為保證服務(wù)端正常,建議50以下)
然后我們可以使用 Promise.all() 處理異步操作隊(duì)列:
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) })
}
好了,前端工作已經(jīng)完成,接下來(lái)就是Node的工作了。
后端實(shí)現(xiàn)
為了方便,后端采用的是express框架來(lái)快速搭建Http服務(wù)和路由。具體項(xiàng)目見我的github node-image-upload。邏輯雖然簡(jiǎn)單,但還是有幾個(gè)可圈可點(diǎn)的地方:
跨域調(diào)用
本項(xiàng)目后端采用的是express,我們可以通過 res.header() 設(shè)置 請(qǐng)求的 “允許源” 來(lái)允許跨域調(diào)用:
res.header('Access-Control-Allow-Origin', '*');
設(shè)置為 * 說(shuō)明允許任何 訪問源,不太安全。建議設(shè)置成 你需要的 二級(jí)域名,如 jafeney.com。
除了 “允許源” ,其他還有 “允許頭” 、”允許域”、 “允許方法”、”文本類型” 等。常用的設(shè)置如下:
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請(qǐng)求
ES6風(fēng)格下的Ajax請(qǐng)求和ES5不太一樣,在正式的請(qǐng)求發(fā)出之前都會(huì)先發(fā)一個(gè) 類型為 OPTIONS的請(qǐng)求 作為試探,只有當(dāng)該請(qǐng)求通過以后,正式的請(qǐng)求才能發(fā)向服務(wù)端。
所以服務(wù)端路由 我們還需要 處理這樣一個(gè) 請(qǐng)求:
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();
});
注意:該請(qǐng)求同樣需要設(shè)置跨域。
處理上傳
處理上傳的圖片引人了multiparty模塊,用法很簡(jiǎn)單:
/*使用multiparty處理上傳的圖片*/
router.post('/upload', function(req, res, next) {
// 生成multiparty對(duì)象,并配置上傳目標(biāo)路徑
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 來(lái)安裝:
npm install gm --save
BUG說(shuō)明
注意:node的圖形操作gm模塊前使用必須 先安裝 imagemagick 和 graphicsmagick,Linux (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格式的圖片
預(yù)設(shè)尺寸
有些時(shí)候除了原圖,我們可能需要把原圖等比例縮小作為預(yù)覽圖或者縮略圖。這個(gè)異步操作還是用Promise來(lái)實(shí)現(xiàn):
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)
};
});
});
}
重命名圖片
為了方便排序和管理圖片,我們按照 “年月日 + 時(shí)間戳 + 尺寸” 來(lái)命名圖片:
var _dateSymbol = new Date().toLocaleDateString().split('-').join('');
var _timeSymbol = new Date().getTime().toString();
至于圖片尺寸 使用 gm的 size() 方法來(lái)獲取,處理方式如下:
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!');
}
});
});
總結(jié)
對(duì)于大前端的工作,理解圖片上傳的前后端原理僅僅是淺層的。我們的口號(hào)是 “把JavaScript進(jìn)行到底!”,現(xiàn)在無(wú)論是 ReactNative的移動(dòng)端開發(fā),還是NodeJS的后端開發(fā),前端工程師可以做的工作早已不僅僅是局限于web頁(yè)面,它已經(jīng)滲透到了互聯(lián)網(wǎng)應(yīng)用層面的方方面面,或許,叫 全棧工程師 更為貼切吧。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- ReactNative實(shí)現(xiàn)圖片上傳功能的示例代碼
- react native實(shí)現(xiàn)往服務(wù)器上傳網(wǎng)絡(luò)圖片的實(shí)例
- React+ajax+java實(shí)現(xiàn)上傳圖片并預(yù)覽功能
- React中上傳圖片到七牛的示例代碼
- React實(shí)現(xiàn)阿里云OSS上傳文件的示例
- React Native使用fetch實(shí)現(xiàn)圖片上傳的示例代碼
- react quill中圖片上傳由默認(rèn)轉(zhuǎn)成base64改成上傳到服務(wù)器的方法
- React獲取Java后臺(tái)文件流并下載Excel文件流程解析
- react-native-fs實(shí)現(xiàn)文件下載、文本存儲(chǔ)的示例代碼
- react使用axios實(shí)現(xiàn)上傳下載功能
相關(guān)文章
React中style的使用及注意事項(xiàng)(推薦)
React中style的使用和直接在HTML中使用有些不同,第一,React中必須是style="opacity:{this.state.opacity};"這種寫法,第二如果設(shè)置多個(gè)style格式如下,多個(gè)style中間使用逗號(hào)分割,這篇文章主要介紹了React中style的使用注意事項(xiàng),需要的朋友可以參考下2023-02-02
React使用highlight.js Clipboard.js實(shí)現(xiàn)代碼高亮復(fù)制
這篇文章主要為大家介紹了React使用highlight.js Clipboard.js實(shí)現(xiàn)代碼高亮復(fù)制功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
react項(xiàng)目中express動(dòng)態(tài)路由未能匹配造成的404問題解決
本文主要介紹了react項(xiàng)目中express動(dòng)態(tài)路由未能匹配造成的404問題解決,解決了白屏的問題,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09
解決React報(bào)錯(cuò)No duplicate props allowed
這篇文章主要為大家介紹了React報(bào)錯(cuò)No duplicate props allowed解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

