紅旗不倒,誓把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
請求和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
注意: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格式的圖片
有些時候除了原圖,咱們可能須要把原圖等比例縮小做爲預覽圖或者縮略圖。這個異步操做仍是用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();
至於圖片尺寸 使用 gm
的 size()
方法來獲取,處理方式以下:
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頁面,它已經滲透到了互聯網應用層面的方方面面,或許,叫 全棧工程師
更爲貼切吧。
固然,全棧
兩個字的份量很重,不積跬步,無以致千里
,功力低下的我還須要不斷修煉和實踐!
@歡迎關注個人 github 和 我的博客 -Jafeney