對於圖片上傳,你們必定不陌生。最近工做中遇到了關於圖片上傳的內容,藉此機會認真研究了一番,遂一發不可收拾,最後琢磨了一個東西出來。在開發的過程當中有很多的體會,因而打算寫一篇文章分享一下心得體會。
本文將會以這個名爲Dolu
的項目爲例子,一步步介紹我是如何進行環境搭建、代碼設計以及實際開發的。內容較多,還請耐心讀完。html
項目地址:https://github.com/jrainlau/dolu前端
本項目使用目前最新的webpack 2
和es7
進行開發,因此環境的搭建必不可少。可是因爲這個項目比較簡單,因此環境的搭建也是很是簡單的,只有一個webpack.config.js
文件:html5
var path = require('path') var webpack = require('webpack') module.exports = { entry: './src/main.js', // 開發模式用 // entry: './src/dolu.js', // 生產模式用 output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js', // 開發模式用 // filename: 'index.js', // 生產模式用 libraryTarget: 'umd' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules|dist/, use: [ 'babel-loader', 'eslint-loader' ] } ] }, devServer: { historyApiFallback: true, noInfo: true, host: '0.0.0.0' }, performance: { hints: false }, devtool: '#eval-source-map' } if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), new webpack.LoaderOptionsPlugin({ minimize: true }) ]) }
考慮到「生產模式」使用的次數很少,因此並無區分dev
和prod
模式,而是手動註釋對應的內容進行切換。node
定義好入口文件和輸出路徑後,我使用了babel-loader
和eslint-loader
。這兩個loader的做用就很少做介紹了,值得注意的是養成使用eslint
的習慣是極好的,可以有效減小代碼的錯誤,而且可以改掉不少壞習慣。同時在編輯器裏(我用VSCODE)中也可以實時進行代碼檢查,很是方便。webpack
爲了使用最新的es7
,咱們也須要在根目錄下配置一份.babelrc
文件:ios
{ "presets": [ ["latest", { "es2015": { "modules": false } }] ], "plugins": [ ["transform-runtime"] ] }
配置好了webpack.config.js
和.babelrc
之後,咱們打開package.json
,來看看須要安裝的依賴都有哪些:git
"devDependencies": { "babel-core": "^6.24.0", "babel-loader": "^6.4.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-latest": "^6.24.0", "cors": "^2.8.3", "cross-env": "^3.2.4", "eslint": "^3.19.0", "eslint-config-standard": "^10.2.1", "eslint-loader": "^1.7.1", "eslint-plugin-import": "^2.2.0", "eslint-plugin-node": "^4.2.2", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^3.0.1", "multer": "^1.3.0", "webpack": "^2.3.1", "webpack-dev-server": "^2.4.2" }
當中的cors
模塊和multer
模塊爲咱們以後搭建node服務器須要用的,其餘都是運行所需。es6
而後在"scripts"裏面寫上咱們要用到的幾條命令:github
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", "server": "node ./server/index.js" },
分別對應開發模式
,生產模式
,啓動本地後臺服務器
。web
而後咱們在根目錄下新建一個src
目錄,一個index.html
,一個/src/main.js
。這時候整個項目的目錄結構以下:
├── index.html ├── package.json ├── src │ └── main.js ├── webpack.config.js └── .babelrc
至此,咱們的開發環境已經搭建完畢。
基本的流程及功能如上圖所示,其中的每一步咱們都將以模塊的方式進行開發。
固然,咱們不能知足於這麼一點點的功能,咱們須要考慮更多的狀況更多的可能,擴展一下,也許咱們能夠這麼作:
好比咱們在獲取圖片以後先不進行上傳,也許咱們還要對轉出來的base64
進行處理或使用,也許咱們可以直接上傳一堆由第三方提供的base64
甚至formdata
。另外咱們還須要對上傳的方法進行自定義,又或者能夠選擇多張圖片什麼的……除此以外,可能還有許許多多的場景,爲了開發一個通用的組件,咱們須要思考的地方實在有不少不少。
固然,這一次咱們的任務比較簡單,上面這麼多功能已經夠咱們玩的了,下面咱們進入實際的開發。
在/src
目錄下新建一個dolu.js
文件,這將會是咱們整個項目的核心。
首先定義一個類:
class Dolulu { constructor (config = {}) {} }
而後咱們按照上一節腦圖的思路,先完成「圖片選取」相關的功能。
在這個類裏面咱們定義一個名爲_pickFile()
的私有方法,這個方法咱們不但願被外部調用,只是做爲Dolu
內置的方法。
_pickFile () { const picker = document.querySelector(this.config.picker) picker.addEventListener('change', () => { if (!picker.files.length) { return } const files = [...picker.files] if (files.length > this.config.quantity) { throw new Error('Out of file quantity limit!') } /* * 這時候咱們已經拿到了文件數組files,能夠立刻進行轉碼 * _transformer()函數是另外一個私有方法,用於格式轉碼 */ this._transformer(files) /* * 加入這一行以實現重複選中同一張圖片 */ picker.value = null }) }
而後寫一個初始化的方法,讓Dolu
實例可以自動開啓文件選取功能:
_init () { if (this.config.picker) { return this._pickFile() } }
只要在constructor
裏面調用這個方法就能夠了。
選擇完圖片,咱們就要對它進行轉碼了。爲了更好地組織咱們的代碼,咱們把這個「圖片轉成base64」的函數封裝成一個模塊。在/src
目錄下新建fileToBase64.js
:
const fileToBase64 = (file) => { const reader = new FileReader() reader.readAsDataURL(file) return new Promise((resolve) => { reader.addEventListener('load', () => { const result = reader.result resolve(result) }) }) } export default fileToBase64
代碼內容只有15行,其輸入爲一個圖片文件,輸出爲一串base64編碼。返回一個Promise方便接下來咱們使用async/await
語法。
一樣的道理,咱們新建一個base64ToBlob.js
文件,以實現輸入爲base64,輸出爲formdata的功能:
const base64ToBlob = (base64) => { const byteString = atob(base64.split(',')[1]) const mimeString = base64.split(',')[0].split(':')[1].split(';')[0] const ab = new ArrayBuffer(byteString.length) const ia = new Uint8Array(ab) for (let i = 0, len = byteString.length; i < len; i += 1) { ia[i] = byteString.charCodeAt(i) } let Builder = window.WebKitBlobBuilder || window.MozBlobBuilder let blobUrl if (Builder) { const builder = new Builder() builder.append(ab) blobUrl = builder.getBlob(mimeString) } else { blobUrl = new window.Blob([ab], { type: mimeString }) } const fd = new FormData() fd.append('file', blobUrl) return fd } export default base64ToBlob
接下來咱們利用這兩個模塊,構建咱們的_transformer()
方法:
_transformer (files, manually = false) { files.forEach(async (file, index) => { if (isObject(file)) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) { return } const dataUrl = await fileToBase64(file) const formData = await base64ToBlob(dataUrl) if (this.config.autoSend || manually) { this._uploader(formData, index) } } })
能夠看到,這個方法會遍歷整個files數組,經過篩選保證其文件類型爲圖片,而後連續轉碼生成formdata格式數據,做爲參數傳入_uploader()
方法中。另外爲了方便擴展和使用,同時傳入了圖片的下標。圖片的下標可以方便在上傳函數中讓用戶知道「如今是第幾張圖片被處理」。
_upload()
函數將會直接調用Dolu
實例中所定義的上傳方法,這個稍後再述。
到這裏,咱們已經完成了上一節第一張圖片的幾個「基本功能」了,和外面一撈一大把的教程相差無幾。別急,咱們立刻進入對擴展功能的開發。
咱們從新把目光投向上一節的_transformer()
函數。這個函數接受一個數組,在內部使用.forEach()
方法遍歷每個文件,對它進行轉碼處理。爲了向外輸出完整的轉碼後的數組,關鍵的步驟在於如何肯定轉碼已經完成了。從最簡單的想法開始,在forEach
循環體的外部直接把數組拋出去行不行?好比這樣:
_transformer (files, manually = false) { files.forEach(async (file, index) => { if (isObject(file)) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) { return } const dataUrl = await fileToBase64(file) const formData = await base64ToBlob(dataUrl) this.dataUrlArr.push(dataUrl) if (this.config.autoSend || manually) { this._uploader(formData, index) } } }) this.config.getDataUrls(this.dataUrlArr) return this }
看起來沒有問題,可是在實際的測試中,傳入this.config.getDataUrls
中的dataUrlArr
首先會是一個空數組,過一下子纔會有數據。爲了驗證這個結論,咱們在/src
名錄下新建一個文件main.js
,寫入以下內容:
import Dolu from './dolu' const dolu = new Dolu({ picker: '#picker', getDataUrls (arr) { console.info(arr) arr.forEach((dataUrl) => { console.log(dataUrl) }) } })
運行一下,發現輸出結果以下:
只有一個空數組,並且forEach()
循環並無打印出任何東西。這個例子不直觀,咱們如今把開發者工具關掉,而後從新打開,看看會發生什麼:
僅僅是從新打開開發者工具,就發現剛纔的空數組變成了一個有內容的數組,特別奇怪。
其實緣由也很簡單,由於_transformer()
內部的forEach()
循環,並不能保證圖片已經轉碼完畢,這涉及到瀏覽器任務隊列的知識(此處理解可能有誤,歡迎指出),在這裏就不展開討論了。
那麼咱們只能等待圖片轉碼完畢,才調用this.config.getDataUrls()
方法。要實現這個目的,咱們有許多種方法,最簡單粗暴的就是利用setInterval()
進行輪詢,當dataUrlArr.length === files.length
,則當即調用,可是這種作法一點兒也不優雅。咱們能不能讓函數發送一個通知,當.push()
方法執行併成功的時候就判斷dataUrlArr.length =?= files.length
,若條件符合則進行相應的處理。
這時候咱們能夠考慮使用es6新增語法Proxy
來解決。關於Proxy
的使用能夠查閱個人另一篇文章
《使用ES6的新特性Proxy來實現一個數據綁定實例》,而後咱們一塊兒來步入正題吧!
Proxy
實現數據綁定在/src
目錄下的utils.js
裏,咱們加入一個新的工具方法:
function proxier (props, callback) { const waitProxy = new Proxy(props, { set (target, property, value) { target[property] = value callback(target, property, value) return true } }) return waitProxy }
回到dolu.js
文件,改寫一下_transformer()
方法:
_transformer (files, manually = false) { const dataUrlArrProxy = proxier(this.dataUrlArr, (target, property, value) => { if (property === 'length') { if (target.length === files.length) { this.config.getDataUrls(this.dataUrlArr) } } }) files.forEach(async (file, index) => { if (isObject(file)) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) { return } const dataUrl = await fileToBase64(file) const formData = await base64ToBlob(dataUrl) dataUrlArrProxy.push(dataUrl) if (this.config.autoSend || manually) { this._uploader(formData, index) } } }) return this }
這樣,咱們每一次轉碼事後,都會調用代理數組dataUrlArrProxy
中的.push()
方法,這時候代理數組就會自動判斷target.length =?= files.length
而後調用相應的方法。
嘗試運行一下,發現結果符合預期。一樣的方式,咱們能夠爲formDataArr
也設置一個代理數組,以實現向外拋出formdata
數組的目的。
把前端這邊的圖片選取、圖片轉碼都已經作完了,那麼咱們是時候搭建一個後臺服務器,去測試以formdata
格式上傳圖片是否有效了。
進入根目錄下的/server
文件夾,咱們新建一個/imgs
目錄以及一個index.js
文件,內容以下:
const express = require('express') const multer = require('multer') const cors = require('cors') const app = express() app.use(express.static('./public')) app.use(cors()) app.listen(process.env.PORT || 8888) console.log('Node.js Ajax Upload File running at: http://0.0.0.0:8888') app.post('/upload', (req, res) => { const store = multer.diskStorage({ destination: './server/imgs' }) const upload = multer({ storage: store }).any() upload(req, res, function (err) { if (err) { console.log(err) return res.end('Error') } else { console.log(req.body) req.files.forEach(function (item) { console.log(item) }) res.end('File uploaded') } }) })
該服務器將會運行於本地8888
端口,經過post
方法發送到localhost:8888/upload
,而後圖片會保存到server/imgs
目錄下。
回到dolu.js
,咱們寫一個_uploader()
方法,該方法會調用config
裏面的自定義設置,調用設置中具體的上傳方法:
_uploader (formData, index) { this.config.uploader(formData, index) }
在main.js
中,咱們使用axios
做爲上傳的工具:
const dolu = new Dolu({ picker: '#picker', autoSend: true, uploader (data, index) { axios({ method: 'post', url: 'http://0.0.0.0:8888/upload', data: data, onUploadProgress: (e) => { const percent = Math.round((e.loaded * 100) / e.total) console.log(percent, index) } }).then((res) => { console.log(res) }).catch((err) => { console.log(err) }) } })
激動人心的時刻來了,咱們來測試一下吧!
打開開發者工具當中的Network
,隨便選幾張圖片進行上傳,看看效果如何:
點擊去看看發送的是什麼東西:
如上圖所示,是一個formdata數據。打開./server/imgs
目錄,咱們應該就能看到三個文件了:
上傳成功!並且符合咱們以「formdata上傳的二進制格式」的需求。
至此已經基本完成了咱們整個圖片上傳組件,還有幾個細節須要注意,好比所發送圖片的命名、對圖片經過canvas進行壓縮等等,這些坑之後有空再填。比較完善的代碼能夠直接查看個人倉庫。
感謝您的閱讀,歡迎對文章內容提出批評指導建議!
參考資料: