react-native 使用react-native-image-crop-picker上传图片、视频到服务端

    技术2022-07-10  161

    博主主要卡在了上传数据这一步

    情景是这样的:

    每一次只允许选择一张图片,每次从相册中选择一图片点击右上角确定后,立即发送请求,上传该图片,并且下次再点击时,重复这个动作。

    (1)点击下图的上传资料

    (2)点击红框内的按钮

    (3)选择图片

    (4)选择完毕的同时,上传图片到服务器(这边展示的图片是本地的,不是服务器那请求回来的)

     

    上传图片的回调返回的Image信息:

    { creationDate: "1344408930" cropRect: null data: "/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAA" exif: null filename: "IMG_0005.JPG" height: 2002 localIdentifier: "ED7AC36B-A150-4C38-BB8C-B6D696F4F2ED/L0/001" mime: "image/jpeg" modificationDate: "1552363036" path: "/Users/ng/Library/Developer/CoreSimulator/Devices/CC28FB0A-09AA-4DEB-9633-F570FD1EDDE5/data/Containers/Data/Application/03FA20A9-374E-44E0-BACB-14FE9833296F/tmp/react-native-image-crop-picker/B0CD309A-4004-4B06-ADA6-92521584328F.jpg" size: 4752033 sourceURL: "file:///Users/ng/Library/Developer/CoreSimulator/Devices/CC28FB0A-09AA-4DEB-9633-F570FD1EDDE5/data/Media/DCIM/100APPLE/IMG_0005.JPG" width: 3000 }

    我们可以看到,提供给我们的是本地的图片路径,还有base64,这边我们需要的自然是path的属性值啦,不过,IOS是不需要file:///的,android才需要,因此,这边需要做个代码适配

     请求头上传类型Content-Type:multipart/form-data

    我们可以看到如下的请求结构(Request Payload):

    这是multipart/form-data类型的请求体数据,Content-Disposition是用来备注,提示我们的,而底下的[object Object]则是form-data数据啦,也就是我们真正要上传的图片、视频数据

    上面的就是我们要上传的formData数据了,那我们打印出来会是什么样的呢?

    成功发送请求后,返回一个fileId给我们:

     

    完整代码 

    /** * @flow * @author * @description 上传图片 */ import React, { PureComponent } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Image, TextInput, ActivityIndicator, } from 'react-native'; import ImagePicker from 'react-native-image-crop-picker'; import CommonModalView from '../../widget/CommonModalView'; import OASize, { Ratio } from '../../constants/OASize'; import OAColor from '../../theme/OAColor'; import OAStyles from '../../theme/OAStyles'; import { ButtonBase } from '../../components'; import API from '../../api'; import NetworkHandler from '../../utils/NetworkHandler'; import OAConstants from '../../constants/OAConstants'; import { system } from '../../utils'; import MOALog from '../../utils/MOALog'; import Toast from '../../widget/Toast'; type ITProps = { contentId: number, }; export default class ImageMaterialUploadContainer extends PureComponent<ITProps> { constructor(props: Props) { super(props); this.state = { imgList: [], inputText: '', isLoading: false, }; } componentDidMount() { // const fetchHandler = new NetworkHandler( // { api: '/partyAppDev/task/fileDownload' }, // { fileId: '06b258e1d802471c85a53e14c6fa7e3a' } // ); // fetchHandler.get((res: any, error) => { // MOALog.info('文件下载res', res, 'error', error); // }); } _handleUpload = () => { // global.FilePicker.pick((files, type, other) => { // console.log('FilePicker files:', files); // this._getImgList([files[0]]); // // [{}] 数组对象 // // 视频 // // { // // height: 2232 // // mime: "video/mp4" // // modificationDate: "1592807013000" // // path: "file:///data/user/0/com.cmschina.partydevelopdev/cache/react-native-image-crop-picker/S00620-15080390.mp4" // // size: 37277904 // // width: 1080 // // } // // 相片 // // { // // height: 2232 // // mime: "image/png" // // modificationDate: "1592807163000" // // path: "file:///data/user/0/com.cmschina.partydevelopdev/cache/react-native-image-crop-picker/S00621-14103571.png" // // size: 373418 // // width: 1080 // // } // }); const imgPickProps = { loadingLabelText: '正在处理中...', multipleChooseText: '完成', multipleCancelText: '取消', includeBase64: true, }; CommonModalView.showActionsTextModal( '', [ { id: 'photo', name: '从相册选择', }, { id: 'material', name: '从素材库选择', }, { id: 'camera', name: '拍照', }, { id: 'video', name: '选择视频', }, ], (item, index) => { switch (index) { case 0: setTimeout(() => { ImagePicker.openPicker({ multiple: false, mediaType: 'photo', //选择的类型 ...imgPickProps, }) .then((image) => { console.log('image:', image); this._handlePictureRes(image); }) .catch((error) => { console.log('error:', error); // Toast.info(`您的图片无权限读取`); }); }, 350); break; case 1: mb.getNavigator().push('MaterialLibraryScene'); break; case 2: setTimeout(() => { ImagePicker.openCamera({ cropping: false, mediaType: 'photo', //选择的类型 ...imgPickProps, }).then((image) => { // this._getImgList([image]); this._handlePictureRes(image); }); }, 350); break; case 3: setTimeout(() => { ImagePicker.openPicker({ mediaType: 'video', //选择的类型 // multiple: true, // ...imgPickProps, }) .then((image) => { // this._getImgList([image]); this._handlePictureRes(image); }) .catch((error) => { Toast.info(`您的视频无权限读取`, error); }); }, 350); break; default: } } ); }; _handlePictureRes = (image) => { MOALog.info('_handlePictureRes image:', image); this.setState({ isLoading: true }); let _fileName = image.filename || image.name; let _path = image.path || image.sourceURL; // [修复] android上传文件file路径需要`file://` if (system.isIOS && /^file:\/\//i.test(_path)) { _path = _path.replace('file://', ''); } else if (system.isAndroid && !/^file:\/\//i.test(_path)) { _path = 'file://' + _path; } if (!_fileName) { _fileName = _path.match(/[^\/]+$/)[0]; } if (image.size > OAConstants.MAX_ATTACHMENT_SIZE) { Toast.info( `附件“${image.filename}”无法添加:\n单个附件的大小不能超过10M` ); this.setState({ isLoading: false }); return false; } // 上传附件 console.log('fetchHandler url:', _path); const fetchHandler = new NetworkHandler({ api: API.home.uploadFile, // api: 'https://oams.newone.com.cn/api/email/attachment/upload', }); fetchHandler.upload({ uri: _path, name: _fileName }, (res: any, error) => { MOALog.info('res, error===>', res, error); if (error) { this.setState({ isLoading: false }); Toast.info(error); return; } this._getImgList([image], res); this.setState({ isLoading: false }); }); }; _getImgList = (image, res) => { console.log('FilePicker image:', image); let { imgList } = this.state; if (image.length > 6) { return Toast.info('添加的图片不超过6张'); } const list = image.map((item) => { let list = { path: item.path, creationDate: item.creationDate, data: item.data ? `data:${item.mime};base64,${item.data}` : item.path, height: item.height, width: item.width, imgData: item.data, imgName: item.mime, fileIds: res, // 关键,成功上传后获取到的图片的唯一标志 }; return list; }); imgList = [...imgList, ...list]; this.setState({ imgList, }); }; _uploadImg = () => { const { imgList, inputText } = this.state; const { contentId } = this.props; const arr = []; imgList && imgList.length && imgList.forEach((img) => { arr.push(img.fileIds); }); MOALog.info( 'replyTask imgList', imgList, 'replyTask files', arr, 'contentId', contentId ); // 附件上传后,一次性提交 const fetchHandler = new NetworkHandler( { api: API.home.replyTask }, { replyExplain: inputText, files: arr, contentId } ); fetchHandler.post((res: any, error) => { MOALog.info('item, index', res, 'replyTask error', error); if (error) { return; } mb.getNavigator().pop(); }); }; _removeImgItem = (item, index) => { this.setState({ isLoading: true }); MOALog.info('_removeImgItem item, index', item, index); // 附件删除 const fetchHandler = new NetworkHandler( { api: API.home.delFile }, { fileId: item.fileIds.fileId } ); fetchHandler.get((res: any, error) => { // 真正删除成功时,才删除对应数组元素 this.state.imgList.splice(index, 1); this.setState({ list: this.state.imgList, isLoading: false, }); MOALog.info('res', res, 'error', error); }); }; _onChangeText = (v) => { this.setState({ inputText: v }); }; render() { const { imgList, isLoading } = this.state; return ( <View style={styles.imgPick}> <Text style={{ ...OAStyles.font, marginBottom: OASize(5), fontSize: OASize(16), fontWeight: 'bold', }} > 说明: </Text> <TextInput onChangeText={this._onChangeText} multiline // autoFocus maxLength={100} numberOfLines={3} placeholder="请输入说明内容..." style={{ height: OASize(80), marginBottom: OASize(30), backgroundColor: 'rgba(0, 0, 0, 0.05)', }} /> <View style={{ flexDirection: 'row' }}> <Text style={styles.text_label}>资料上传</Text> <Text style={{ color: '#666', fontSize: 15 * Ratio }}>(选填)</Text> </View> <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}> <TouchableOpacity onPress={this._handleUpload}> <Image resizeMode="contain" source={require('../../assets/app/pic_add.png')} style={styles.itemImg} /> </TouchableOpacity> {imgList.length ? imgList.map((item, index) => { return ( <View key={index}> <TouchableOpacity style={{ position: 'absolute', right: 4, top: 0, zIndex: 100, }} activeOpacity={0.88} onPress={() => this._removeImgItem(item, index)} > <Image resizeMode="contain" source={require('../../assets/app/pic_del.png')} style={{ width: 16, height: 16, }} /> </TouchableOpacity> <Image // resizeMode="contain" source={{ uri: item.data }} style={styles.itemImg} // style={{ // width: 100, // height: 100 // }} /> </View> ); }) : null} {isLoading ? ( <View style={[ { justifyContent: 'center', alignItems: 'center', }, styles.itemImg, ]} > <ActivityIndicator /> </View> ) : null} </View> <ButtonBase textStyle={{ color: OAColor.white }} outline={OAColor.primary} style={{ minWidth: OASize(80), marginTop: OASize(15), backgroundColor: '#499ad0', borderWidth: 0, }} onPress={(_) => { // mb.getNavigator().push('ImageMaterialUploadScene', { // listTitle: '三会一课(第一部分)', // }); this._uploadImg(); }} > 提交 </ButtonBase> </View> ); } } const styles = StyleSheet.create({ imgPick: { paddingVertical: 10 * Ratio, paddingHorizontal: OASize(15), }, text_label: { color: '#333', fontSize: 15 * Ratio }, itemImg: { width: 70 * Ratio, height: 70 * Ratio, marginVertical: 10 * Ratio, marginRight: 12 * Ratio, }, });

     

    Processed: 0.061, SQL: 9