摘要:對于圖片上傳,大家一定不陌生。項目地址一環境搭建本項目使用目前最新的和進行開發,所以環境的搭建必不可少。在目錄下新建代碼內容只有行,其輸入為一個圖片文件,輸出為一串編碼。同樣的方式,我們可以為也設置一個代理數組,以實現向外拋出數組的目的。
對于圖片上傳,大家一定不陌生。最近工作中遇到了關于圖片上傳的內容,借此機會認真研究了一番,遂一發不可收拾,最后琢磨了一個東西出來。在開發的過程中有不少的體會,于是打算寫一篇文章分享一下心得體會。
本文將會以這個名為Dolu的項目為例子,一步步介紹我是如何進行環境搭建、代碼設計以及實際開發的。內容較多,還請耐心讀完。
項目地址:https://github.com/jrainlau/dolu
一、環境搭建本項目使用目前最新的webpack 2和es7進行開發,所以環境的搭建必不可少。但是由于這個項目比較簡單,所以環境的搭建也是非常簡單的,只有一個webpack.config.js文件:
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模式,而是手動注釋對應的內容進行切換。
定義好入口文件和輸出路徑后,我使用了babel-loader和eslint-loader。這兩個loader的作用就不多作介紹了,值得注意的是養成使用eslint的習慣是極好的,能夠有效減少代碼的錯誤,并且能夠改掉很多壞習慣。同時在編輯器里(我用VSCODE)中也能夠實時進行代碼檢查,非常方便。
為了使用最新的es7,我們也需要在根目錄下配置一份.babelrc文件:
{ "presets": [ ["latest", { "es2015": { "modules": false } }] ], "plugins": [ ["transform-runtime"] ] }
配置好了webpack.config.js和.babelrc以后,我們打開package.json,來看看需要安裝的依賴都有哪些:
"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服務器需要用的,其他都是運行所需。
然后在"scripts"里面寫上我們要用到的幾條命令:
"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" },
分別對應開發模式,生產模式,啟動本地后臺服務器。
然后我們在根目錄下新建一個src目錄,一個index.html,一個/src/main.js。這時候整個項目的目錄結構如下:
├── index.html ├── package.json ├── src │?? └── main.js ├── webpack.config.js └── .babelrc
至此,我們的開發環境已經搭建完畢。
二、功能設計基本的流程及功能如上圖所示,其中的每一步我們都將以模塊的方式進行開發。
當然,我們不能滿足于這么一點點的功能,我們需要考慮更多的情況更多的可能,擴展一下,也許我們可以這么做:
比如我們在獲取圖片之后先不進行上傳,也許我們還要對轉出來的base64進行處理或使用,也許我們能夠直接上傳一堆由第三方提供的base64甚至formdata。另外我們還需要對上傳的方法進行自定義,又或者可以選擇多張圖片什么的……除此之外,可能還有許許多多的場景,為了開發一個通用的組件,我們需要思考的地方實在有很多很多。
當然,這一次我們的任務比較簡單,上面這么多功能已經夠我們玩的了,下面我們進入實際的開發。
三、開始coding!在/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實例中所定義的上傳方法,這個稍后再述。
到這里,我們已經完成了上一節第一張圖片的幾個“基本功能”了,和外面一撈一大把的教程相差無幾。別急,我們馬上進入對擴展功能的開發。
四、實現向外輸出完整的base64字符串數組我們重新把目光投向上一節的_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來實現一個數據綁定實例》,然后我們一起來步入正題吧!
在/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進行壓縮等等,這些坑以后有空再填。比較完善的代碼可以直接查看我的倉庫。
感謝您的閱讀,歡迎對文章內容提出批評指導建議!
參考資料:
移動前端—圖片壓縮上傳實踐
HTML5實現圖片上傳
How to detect input type=file “change” for the same file?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/82536.html
摘要:責編現代化的方式開發一個圖片上傳工具前端掘金對于圖片上傳,大家一定不陌生。之深入事件機制前端掘金事件綁定的方式原生的事件綁定方式有幾種想必有很多朋友說種目前,在本人目前的研究中,只有兩種半兩種半還有半種的且聽我道來。 Ajax 與數據傳輸 - 前端 - 掘金背景 在沒有ajax之前,前端與后臺傳數據都是靠表單傳輸,使用表單的方法傳輸數據有一個比較大的問題就是每次提交數據都會刷新頁面,用...
摘要:也是一款優秀的響應式框架站點所使用的一套框架為微信服務量身設計的一套框架一組很小的,響應式的組件,你可以在網頁的項目上到處使用一個可定制的文件,使瀏覽器呈現的所有元素,更一致和符合現代標準。 GitHub 值得收藏的前端項目 整理與收集的一些比較優秀github項目,方便自己閱讀,順便分享出來,大家一起學習,本篇文章會持續更新,版權歸原作者所有。歡迎github star與fork 預...
閱讀 793·2021-11-11 16:54
閱讀 1531·2021-08-24 10:01
閱讀 1916·2019-08-30 15:54
閱讀 3302·2019-08-29 14:02
閱讀 3137·2019-08-28 18:22
閱讀 2251·2019-08-28 18:09
閱讀 3712·2019-08-26 10:26
閱讀 2674·2019-08-23 18:23