日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

移动端H5实现图片上传

發布時間:2024/3/7 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 移动端H5实现图片上传 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

效果圖

基礎知識

FormData

通過FormData對象可以組裝一組用 XMLHttpRequest發送請求的鍵/值對。它可以更靈活方便的發送表單數據,因為可以獨立于表單使用。如果你把表單的編碼類型設置為multipart/form-data ,則通過FormData傳輸的數據格式和表單通過submit() 方法傳輸的數據格式相同。

這是一種常見的移動端上傳方式,FormData也是H5新增的 兼容性如下:

base64

Base64是一種基于64個可打印字符來表示二進制數據的表示方法。 由于2的6次方等于64,所以每6個位元為一個單元,對應某個可打印字符。 三個字節有24個位元,對應于4個Base64單元,即3個字節可表示4個可打印字符。

base64可以說是很出名了,就是用一段字符串來描述一個二進制數據,所以很多時候也可以使用base64方式上傳。兼容性如下:

Blob對象

一個 Blob對象表示一個不可變的, 原始數據的類似文件對象。Blob表示的數據不一定是一個JavaScript原生格式。 File 接口基于Blob,繼承 blob功能并將其擴展為支持用戶系統上的文件。

簡單說Blob就是一個二進制對象,是原生支持的,兼容性如下:

FileReader對象

FileReader 對象允許Web應用程序異步讀取存儲在用戶計算機上的文件(或原始數據緩沖區)的內容,使用 File 或 Blob 對象指定要讀取的文件或數據。

FileReader也就是將本地文件轉換成base64格式的dataUrl。

圖片上傳思路

準備工作都做完了,那怎樣用這些材料完成一件事情呢。
這里要強調的是,考慮到移動端流量很貴,所以有必要對大圖片進行下壓縮再上傳。
圖片壓縮很簡單,將圖片用canvas畫出來,再使用canvas.toDataUrl方法將圖片轉成base64格式。

所以圖片上傳思路大致是:

1.監聽一個input(type=‘file’)的onchange事件,這樣獲取到文件file;
2.將file轉成dataUrl;
3.然后根據dataUrl利用canvas繪制圖片壓縮,然后再轉成新的dataUrl;
4.再把dataUrl轉成Blob;
5.把Blob append進FormData中;
6.xhr實現上傳。

手機兼容性問題

理想很豐滿,現實很骨感。
實際上由于手機平臺兼容性問題,上面這套流程并不能全都支持。
所以需要根據兼容性判斷。

經過試驗發現:

1.部分安卓微信瀏覽器無法觸發onchange事件(第一步就特么遇到問題)
這其實安卓微信的一個遺留問題。 查看討論 解決辦法也很簡單:input標簽 <input type=“file" name=“image” accept="image/gif, image/jpeg, image/png”>要寫成就沒問題了。
2.部分安卓微信不支持Blob對象
3.部分Blob對象append進FormData中出現問題
4.iOS 8不支持new File Constructor,但是支持input里的file對象。
5.iOS 上經過壓縮后的圖片可以上傳成功 但是size是0 無法打開。
6.部分手機出現圖片上傳轉換問題。
7.安卓手機不支持多選,原因在于multiple屬性根本就不支持。
8.多張圖片轉base64時候卡頓,因為調用了cpu進行了計算。
9.上傳圖片可以使用base64上傳或者formData上傳

上傳思路修改方案

經過考慮,我們決定做兼容性處理:

這里邊兩條路,最后都是File對象append進FormData中實現上傳。

代碼實現

首先有個html

<input type="file" name="image" accept=“image/*” onchange='handleInputChange'>

然后js如下:

// 全局對象,不同function使用傳遞數據 const imgFile = {};function handleInputChange (event) {// 獲取當前選中的文件const file = event.target.files[0];const imgMasSize = 1024 * 1024 * 10; // 10MB// 檢查文件類型if(['jpeg', 'png', 'gif', 'jpg'].indexOf(file.type.split("/")[1]) < 0){// 自定義報錯方式// Toast.error("文件類型僅支持 jpeg/png/gif!", 2000, undefined, false);return;}// 文件大小限制if(file.size > imgMasSize ) {// 文件大小自定義限制// Toast.error("文件大小不能超過10MB!", 2000, undefined, false);return;}// 判斷是否是iosif(!!window.navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)){// iOStransformFileToFormData(file);return;}// 圖片壓縮之旅transformFileToDataUrl(file); } // 將File append進 FormData function transformFileToFormData (file) {const formData = new FormData();// 自定義formData中的內容// typeformData.append('type', file.type);// sizeformData.append('size', file.size || "image/jpeg");// nameformData.append('name', file.name);// lastModifiedDateformData.append('lastModifiedDate', file.lastModifiedDate);// append 文件formData.append('file', file);// 上傳圖片uploadImg(formData); } // 將file轉成dataUrl function transformFileToDataUrl (file) {const imgCompassMaxSize = 200 * 1024; // 超過 200k 就壓縮// 存儲文件相關信息imgFile.type = file.type || 'image/jpeg'; // 部分安卓出現獲取不到type的情況imgFile.size = file.size;imgFile.name = file.name;imgFile.lastModifiedDate = file.lastModifiedDate;// 封裝好的函數const reader = new FileReader();// file轉dataUrl是個異步函數,要將代碼寫在回調里reader.onload = function(e) {const result = e.target.result;if(result.length < imgCompassMaxSize) {compress(result, processData, false ); // 圖片不壓縮} else {compress(result, processData); // 圖片壓縮}};reader.readAsDataURL(file); } // 使用canvas繪制圖片并壓縮 function compress (dataURL, callback, shouldCompress = true) {const img = new window.Image();img.src = dataURL;img.onload = function () {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0, canvas.width, canvas.height);let compressedDataUrl;if(shouldCompress){compressedDataUrl = canvas.toDataURL(imgFile.type, 0.2);} else {compressedDataUrl = canvas.toDataURL(imgFile.type, 1);}callback(compressedDataUrl);} }function processData (dataURL) {// 這里使用二進制方式處理dataUrlconst binaryString = window.atob(dataUrl.split(',')[1]);const arrayBuffer = new ArrayBuffer(binaryString.length);const intArray = new Uint8Array(arrayBuffer);const imgFile = this.imgFile;for (let i = 0, j = binaryString.length; i < j; i++) {intArray[i] = binaryString.charCodeAt(i);}const data = [intArray];let blob;try {blob = new Blob(data, { type: imgFile.type });} catch (error) {window.BlobBuilder = window.BlobBuilder ||window.WebKitBlobBuilder ||window.MozBlobBuilder ||window.MSBlobBuilder;if (error.name === 'TypeError' && window.BlobBuilder){const builder = new BlobBuilder();builder.append(arrayBuffer);blob = builder.getBlob(imgFile.type);} else {// Toast.error("版本過低,不支持上傳圖片", 2000, undefined, false);throw new Error('版本過低,不支持上傳圖片');}}// blob 轉fileconst fileOfBlob = new File([blob], imgFile.name);const formData = new FormData();// typeformData.append('type', imgFile.type);// sizeformData.append('size', fileOfBlob.size);// nameformData.append('name', imgFile.name);// lastModifiedDateformData.append('lastModifiedDate', imgFile.lastModifiedDate);// append 文件formData.append('file', fileOfBlob);uploadImg(formData); }// 上傳圖片 uploadImg (formData) {const xhr = new XMLHttpRequest();// 進度監聽xhr.upload.addEventListener('progress', (e)=>{console.log(e.loaded / e.total)}, false);// 加載監聽// xhr.addEventListener('load', ()=>{console.log("加載中");}, false);// 錯誤監聽xhr.addEventListener('error', ()=>{Toast.error("上傳失敗!", 2000, undefined, false);}, false);xhr.onreadystatechange = function () {if (xhr.readyState === 4) {const result = JSON.parse(xhr.responseText);if (xhr.status === 200) {// 上傳成功} else {// 上傳失敗}}};xhr.open('POST', '/uploadUrl' , true);xhr.send(formData); }

多圖并發上傳

多張圖片上傳方式有三種:

圖片隊列一張一張上傳
圖片隊列并發全部上傳
圖片隊列并發上傳X個,其中一個返回了結果直接觸發下一個上傳,保證最多有X個請求。
這個一張一張上傳好解決,但是問題是上傳事件太長了,體驗不佳;多張圖片全部上傳事件變短了,但是并發量太大了,很可能出現問題;最后這個并發上傳X個,體驗最佳,只是需要仔細想想如何實現。

并發上傳實現

最后我們確定X = 3或者4。比如說上傳9張圖片,第一次上傳個3個,其中一個請求回來了,立即去上傳第四個,下一個回來上傳第5個,以此類推。
這里我使用es6的generator函數來實現的,定義一個函數,返回需要上傳的數組:

*uploadGenerator (uploadQueue) {/*** 多張圖片并發上傳控制規則* 上傳1-max數量的圖片* 設置一個最大上傳數量* 保證最大只有這個數量的上傳請求**/// 最多只有三個請求在上傳const maxUploadSize = 3;if(uploadQueue.length > maxUploadSize){const result = [];for(let i = 0; i < uploadQueue.length; i++){// 第一次return maxUploadSize數量的圖片if(i < maxUploadSize){result.push(uploadQueue[i]);if(i === maxUploadSize - 1){yield result;}} else {yield [uploadQueue[i]];}}} else {yield uploadQueue.map((item)=>(item));}}

調用的時候:

// 通過該函數獲取每次要上傳的數組this.uploadGen = this.uploadGenerator(uploadQueue);// 第一次要上傳的數量const firstUpload = this.uploadGen.next();// 真正開始上傳流程firstUpload.value.map((item)=>{/*** 圖片上傳分成5步* 圖片轉dataUrl* 壓縮* 處理數據格式* 準備數據上傳* 上傳** 前兩步是回調的形式 后面是同步的形式*/this.transformFileToDataUrl(item, this.compress, this.processData);});

這樣將每次上傳幾張圖片的邏輯分離出來。

單個圖片上傳函數改進

然后遇到了下一個問題,圖片上傳分成5步,

1.圖片轉dataUrl
2.壓縮
3.處理數據格式
4.準備數據上傳
5.上傳

這里面前兩個是回調的形式,最后一個是異步形式。無法寫成正常函數一個調用一個;而且各個function之間需要共享一些數據,之前把這個數據掛載到this.imgFile上了,但是這次是并發,一個對象沒法滿足需求了,改成數組也有很多問題。

所以這次方案是:第一步創建一個要上傳的對象,每次都通過參數交給下一個方法,直到最后一個方法上傳。并且通過回調的方式,將各個步驟串聯起來。Upload完整的代碼如下:

/*** Created by Aus on 2017/7/4.*/ import React from 'react' import classNames from 'classnames' import Touchable from 'rc-touchable' import Figure from './Figure' import Toast from '../../../Feedback/Toast/components/Toast' import '../style/index.scss'// 統計img總數 防止重復 let imgNumber = 0;// 生成唯一的id const getUuid = () => {return "img-" + new Date().getTime() + "-" + imgNumber++; };class Uploader extends React.Component{constructor (props) {super(props);this.state = {imgArray: [] // 圖片已上傳 顯示的數組};this.handleInputChange = this.handleInputChange.bind(this);this.compress = this.compress.bind(this);this.processData = this.processData.bind(this);}componentDidMount () {// 判斷是否有初始化的數據傳入const {data} = this.props;if(data && data.length > 0){this.setState({imgArray: data});}}handleDelete(id) {this.setState((previousState)=>{previousState.imgArray = previousState.imgArray.filter((item)=>(item.id !== id));return previousState;});}handleProgress (id, e) {// 監聽上傳進度 操作DOM 顯示進度const number = Number.parseInt((e.loaded / e.total) * 100) + "%";const text = document.querySelector('#text-'+id);const progress = document.querySelector('#progress-'+id);text.innerHTML = number;progress.style.width = number;}handleUploadEnd (data, status) {// 準備一條標準數據const _this = this;const obj = {id: data.uuid, imgKey: '', imgUrl: '', name: data.file.name, dataUrl: data.dataUrl, status: status};// 更改狀態this.setState((previousState)=>{previousState.imgArray = previousState.imgArray.map((item)=>{if(item.id === data.uuid){item = obj;}return item;});return previousState;});// 上傳下一個const nextUpload = this.uploadGen.next();if(!nextUpload.done){nextUpload.value.map((item)=>{_this.transformFileToDataUrl(item, _this.compress, _this.processData);});}}handleInputChange (event) {const {typeArray, max, maxSize} = this.props;const {imgArray} = this.state;const uploadedImgArray = []; // 真正在頁面顯示的圖片數組const uploadQueue = []; // 圖片上傳隊列 這個隊列是在圖片選中到上傳之間使用的 上傳完成則清除// event.target.files是個類數組對象 需要轉成數組方便處理const selectedFiles = Array.prototype.slice.call(event.target.files).map((item)=>(item));// 檢查文件個數 頁面顯示的圖片個數不能超過限制if(imgArray.length + selectedFiles.length > max){Toast.error('文件數量超出最大值', 2000, undefined, false);return;}let imgPass = {typeError: false, sizeError: false};// 循環遍歷檢查圖片 類型、尺寸檢查selectedFiles.map((item)=>{// 圖片類型檢查if(typeArray.indexOf(item.type.split('/')[1]) === -1){imgPass.typeError = true;}// 圖片尺寸檢查if(item.size > maxSize * 1024){imgPass.sizeError = true;}// 為圖片加上位移idconst uuid = getUuid();// 上傳隊列加入該數據uploadQueue.push({uuid: uuid, file: item});// 頁面顯示加入數據uploadedImgArray.push({ // 顯示在頁面的數據的標準格式id: uuid, // 圖片唯一iddataUrl: '', // 圖片的base64編碼imgKey: '', // 圖片的key 后端上傳保存使用imgUrl: '', // 圖片真實路徑 后端返回的name: item.name, // 圖片的名字status: 1 // status表示這張圖片的狀態 1:上傳中,2上傳成功,3:上傳失敗});});// 有錯誤跳出if(imgPass.typeError){Toast.error('不支持文件類型', 2000, undefined, false);return;}if(imgPass.sizeError){Toast.error('文件大小超過限制', 2000, undefined, false);return;}// 沒錯誤準備上傳// 頁面先顯示一共上傳圖片個數this.setState({imgArray: imgArray.concat(uploadedImgArray)});// 通過該函數獲取每次要上傳的數組this.uploadGen = this.uploadGenerator(uploadQueue);// 第一次要上傳的數量const firstUpload = this.uploadGen.next();// 真正開始上傳流程firstUpload.value.map((item)=>{/*** 圖片上傳分成5步* 圖片轉dataUrl* 壓縮* 處理數據格式* 準備數據上傳* 上傳** 前兩步是回調的形式 后面是同步的形式*/this.transformFileToDataUrl(item, this.compress, this.processData);});}*uploadGenerator (uploadQueue) {/*** 多張圖片并發上傳控制規則* 上傳1-max數量的圖片* 設置一個最大上傳數量* 保證最大只有這個數量的上傳請求**/// 最多只有三個請求在上傳const maxUploadSize = 3;if(uploadQueue.length > maxUploadSize){const result = [];for(let i = 0; i < uploadQueue.length; i++){// 第一次return maxUploadSize數量的圖片if(i < maxUploadSize){result.push(uploadQueue[i]);if(i === maxUploadSize - 1){yield result;}} else {yield [uploadQueue[i]];}}} else {yield uploadQueue.map((item)=>(item));}}transformFileToDataUrl (data, callback, compressCallback) {/*** 圖片上傳流程的第一步* @param data file文件 該數據會一直向下傳遞* @param callback 下一步回調* @param compressCallback 回調的回調*/const {compress} = this.props;const imgCompassMaxSize = 200 * 1024; // 超過 200k 就壓縮// 封裝好的函數const reader = new FileReader();// ?? 這是個回調過程 不是同步的reader.onload = function(e) {const result = e.target.result;data.dataUrl = result;if(compress && result.length > imgCompassMaxSize){data.compress = true;callback(data, compressCallback); // 圖片壓縮} else {data.compress = false;callback(data, compressCallback); // 圖片不壓縮}};reader.readAsDataURL(data.file);}compress (data, callback) {/*** 壓縮圖片* @param data file文件 數據會一直向下傳遞* @param callback 下一步回調*/const {compressionRatio} = this.props;const imgFile = data.file;const img = new window.Image();img.src = data.dataUrl;img.onload = function () {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0, canvas.width, canvas.height);let compressedDataUrl;if(data.compress){compressedDataUrl = canvas.toDataURL(imgFile.type, (compressionRatio / 100));} else {compressedDataUrl = canvas.toDataURL(imgFile.type, 1);}data.compressedDataUrl = compressedDataUrl;callback(data);}}processData (data) {// 為了兼容性 處理數據const dataURL = data.compressedDataUrl;const imgFile = data.file;const binaryString = window.atob(dataURL.split(',')[1]);const arrayBuffer = new ArrayBuffer(binaryString.length);const intArray = new Uint8Array(arrayBuffer);for (let i = 0, j = binaryString.length; i < j; i++) {intArray[i] = binaryString.charCodeAt(i);}const fileData = [intArray];let blob;try {blob = new Blob(fileData, { type: imgFile.type });} catch (error) {window.BlobBuilder = window.BlobBuilder ||window.WebKitBlobBuilder ||window.MozBlobBuilder ||window.MSBlobBuilder;if (error.name === 'TypeError' && window.BlobBuilder){const builder = new BlobBuilder();builder.append(arrayBuffer);blob = builder.getBlob(imgFile.type);} else {throw new Error('版本過低,不支持上傳圖片');}}data.blob = blob;this.processFormData(data);}processFormData (data) {// 準備上傳數據const formData = new FormData();const imgFile = data.file;const blob = data.blob;// typeformData.append('type', blob.type);// sizeformData.append('size', blob.size);// append 文件formData.append('file', blob, imgFile.name);this.uploadImg(data, formData);}uploadImg (data, formData) {// 開始發送請求上傳const _this = this;const xhr = new XMLHttpRequest();const {uploadUrl} = this.props;// 進度監聽xhr.upload.addEventListener('progress', _this.handleProgress.bind(_this, data.uuid), false);xhr.onreadystatechange = function () {if (xhr.readyState === 4) {if (xhr.status === 200 || xhr.status === 201) {// 上傳成功_this.handleUploadEnd(data, 2);} else {// 上傳失敗_this.handleUploadEnd(data, 3);}}};xhr.open('POST', uploadUrl , true);xhr.send(formData);}getImagesListDOM () {// 處理顯示圖片的DOMconst {max} = this.props;const _this = this;const result = [];const uploadingArray = [];const imgArray = this.state.imgArray;imgArray.map((item)=>{result.push(<Figure key={item.id} {...item} onDelete={_this.handleDelete.bind(_this)} />);// 正在上傳的圖片if(item.status === 1){uploadingArray.push(item);}});// 圖片數量達到最大值if(result.length >= max ) return result;let onPress = ()=>{_this.refs.input.click();};// 或者有正在上傳的圖片的時候 不可再上傳圖片if(uploadingArray.length > 0) {onPress = undefined;}// 簡單的顯示文案邏輯判斷let text = '上傳圖片';if(uploadingArray.length > 0){text = (imgArray.length - uploadingArray.length) + '/' + imgArray.length;}result.push(<Touchablekey="add"activeClassName={'zby-upload-img-active'}onPress={onPress}><div className="zby-upload-img"><span key="icon" className="fa fa-camera" /><p className="text">{text}</p></div></Touchable>);return result;}render () {const imagesList = this.getImagesListDOM();return (<div className="zby-uploader-box">{imagesList}<input ref="input" type="file" className="file-input" name="image" accept="image/*" multiple="multiple" onChange={this.handleInputChange} /></div>)} }Uploader.propTypes = {uploadUrl: React.PropTypes.string.isRequired, // 圖上傳路徑compress: React.PropTypes.bool, // 是否進行圖片壓縮compressionRatio: React.PropTypes.number, // 圖片壓縮比例 單位:%data: React.PropTypes.array, // 初始化數據 其中的每個元素必須是標準化數據格式max: React.PropTypes.number, // 最大上傳圖片數maxSize: React.PropTypes.number, // 圖片最大體積 單位:KBtypeArray: React.PropTypes.array, // 支持圖片類型數組 };Uploader.defaultProps = {compress: true,compressionRatio: 20,data: [],max: 9,maxSize: 5 * 1024, // 5MBtypeArray: ['jpeg', 'jpg', 'png', 'gif'], };export default Uploader

原文地址: https://segmentfault.com/a/1190000010034177?utm_source=tag-newest

總結

以上是生活随笔為你收集整理的移动端H5实现图片上传的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。