React Native七牛上传+本地图片选择

参考:

react-native-image-crop-picker图片选择并裁减 //这个看需求使用
react-native-image-picker图片选择
react-native-qiniu
我只要一个多图片上传功能,所以就写简单一点

效果

已上传状态

上传中状态

步骤

1、手机图片、视频选择功能

react-native-image-picker插件
yarn add react-native-image-picker;ios需要pod install

import {launchCamera, launchImageLibrary, ImageLibraryOptions, PhotoQuality} from 'react-native-image-picker';
/**
 * 从相册选择图片;
 * sourceType: 'camera'  打开相机拍摄图片
**/
export async function chooseImage(options: {
  count?: number,
  quality?: PhotoQuality
  sourceType?: 'camera',  //默认'album'
} = {}) {
  return new Promise<any>(async(resolve, reject) => {
    const Opts: ImageLibraryOptions = {
      mediaType: 'photo',
      quality: options.quality || 1,
      selectionLimit: options.count || 1
    };
    const result = options.sourceType == 'camera'? 
      await launchCamera(Opts) : 
      await launchImageLibrary(Opts);
    resolve(result)
  })
}
/**
 * 从相册选择视频;
 * sourceType: 'camera'  打开相机拍摄视频
**/
export async function chooseVideo(options: {
  count?: number,
  quality?: 'low' | 'high'
  sourceType?: 'camera',  //默认'album'
} = {}) {
  return new Promise<any>(async(resolve, reject) => {
    const Opts: ImageLibraryOptions = {
      mediaType: 'video',
      videoQuality: options.quality,
      selectionLimit: options.count || 1
    };
    const result = options.sourceType == 'camera'? 
      await launchCamera(Opts) : 
      await launchImageLibrary(Opts);
    resolve(result)
  })
}
2、七牛上传文件功能
class qiniuUpload {
  private UP_HOST = 'http://upload.qiniu.com';
  // private RS_HOST = 'http://rs.qbox.me';
  // private RSF_HOST = 'http://rsf.qbox.me';
  // private API_HOST = 'http://api.qiniu.com';

  public upload = async(uri:string, key:string, token:string) => {
    return new Promise<any>((resolve, reject) => {
      let formData = new FormData();
      formData.append('file', {uri: uri, type: 'application/octet-stream', name: key});
      formData.append('key', key);
      formData.append('token', token);
    
      let options:any = {
        body: formData,
        method: 'post',
      };
      fetch(this.UP_HOST, options).then((response) => {
        resolve(response)
      }).catch(error => {
        console.error(error)
        resolve(null)
      });  
    })
  }
   //...后面再加别的功能
}

const qiniu = new qiniuUpload();
export default qiniu;
import qiniu from '@/modules/qiniu/index'
...
  /**
   * 上传视频图片
   */
  uploadFile: async (filePath: string) => {
    const res = await createBaseClient('GET', '/v1/file')();  //这是接口请求方法,用来拿后端的七牛token、key
    
    if( !res ) {
      return res;
    }

    const { key, token } = res;
    const fileSegments = filePath.split('.');
    const fileKey = key + '.' + fileSegments[fileSegments.length - 1];

    try {
      const result = await qiniu.upload(filePath, fileKey, token)
      if(result && result.ok) {
        return {
          url: ASSET_HOST + '/' + fileKey,  //ASSET_HOST是资源服务器域名前缀
        };
      }else {
        return null
      }
    } catch (error) {
      return null;
    }

  },
...
3、多图上传组件封装

(这里Base、Image、ActionSheet都是封装过的,需看情况调整)

import React from 'react'
import {
  ViewStyle,
  StyleProp,
  ImageURISource,
  ActivityIndicator
} from 'react-native'
import Base from '@/components/Base';
import { Image, View, Text } from '@/components';   //Image封装过的,所以有些属性不一样
import ActionSheet from "@/components/Feedback/ActionSheet";  //自己封装
import styles from './styleCss';  //样式就不放上来了


interface Props {
  type?: 'video'
  src?: string[]
  count?: number
  btnPath?: ImageURISource
  style?: StyleProp<ViewStyle>
  itemStyle?: StyleProp<ViewStyle>
  itemWidth?: number
  itemHeight?: number  //默认正方形
  onChange?: (e) => void
}

interface State {
  imageUploading: boolean
  images: string[]
}

/**
 * 多图上传组件
 * * type?: 'video'
 * * src?: string[]   //图片数据,可用于初始数据
 * * count?: number    //数量
 * * btnPath?: ImageURISource   //占位图
 * * itemStyle?: item样式,width, height单独设
 * * itemWidth?: number
 * * itemHeight?: number  //默认正方形
 * * onChange?: (e:string[]) => void
**/
export default class Uploader extends Base<Props, State> {
  public state: State = {
    imageUploading: false,
    images: []
  };

  public didMount() {
    this.initSrc(this.props.src)
  }

  public componentWillReceiveProps(nextProps){
    if(nextProps.hasOwnProperty('src') && !!nextProps.src){
      this.initSrc(nextProps.src)
    }
  }

  /**
   *初始化以及改动图片
  **/
  private initSrc = (srcProp:any) => {
    if(!this.isEqual(srcProp, this.state.images)) {
      this.setState({
        images: srcProp
      })
    }
  }
  
  public render() {
    const { style, btnPath, count, itemStyle, itemWidth, itemHeight, type } = this.props;
    const { imageUploading, images } = this.state;
    let countNumber = count? count: 1

    return (
      <React.Fragment>
        <View style={[styles.uploaderBox, style]}>
          {images.length > 0 && images.map((res, ind) => (
            <View style={[styles.item, itemStyle]} key={res}>
              <View style={styles.imgItem}>
                <Image
                  source={{uri: res}}
                  width={this.itemW}
                  height={this.itemH}
                  onPress={() => {
                    this.singleEditInd = ind;
                    this.handleShowActionSheet()
                  }}
                />
                <Text style={styles.del} onPress={this.handleDelete.bind(null, ind)}>删除</Text>
              </View>
            </View>
          ))}
          {images.length < countNumber  &&
            <View style={[styles.item, itemStyle]}> 
              {imageUploading? (
                <View style={[{
                  width: this.itemW,
                  height: this.itemH,
                }, styles.loading]}>
                  <ActivityIndicator size={this.itemW*0.4}></Loading>
                  <Text style={{
                    fontSize: 14,
                    color: '#888',
                    marginTop: 5
                  }}>
                    上传中...
                  </Text>
                </View>
              ): (
                <View style={styles.btn}>
                  <Image
                    source={btnPath || this.assets.uploadIcon}
                    width={this.itemW}
                    height={this.itemH}
                    onPress={() => {
                      this.singleEditInd = undefined;
                      this.handleShowActionSheet()
                    }}
                  />
                </View>
              )}
              
            </View>
          }
          
        </View>
        <ActionSheet
          name="uploaderActionSheet"
          options={[{
            name: type == 'video'? '拍摄': '拍照',
            onClick: () => {
              if(type == 'video') {
                this.handleChooseVideo('camera')
              }else if(this.singleEditInd !== undefined) {
                this.handleChooseSingle('camera')
              }else {
                this.handleChooseImage('camera')
              }
            }
          }, {
            name: '相册',
            onClick: () => {
              if(type == 'video') {
                this.handleChooseVideo()
              }else if(this.singleEditInd !== undefined) {
                this.handleChooseSingle()
              }else {
                this.handleChooseImage()
              }
            }
          }]}
        ></ActionSheet>
      </React.Fragment>
    );
  }

  private get itemW() {
    return this.props.itemWidth || 92
  }
  private get itemH() {
    return this.props.itemHeight || this.itemW;
  }

  
  private isEqual = (firstValue, secondValue) => {
    /** 判断两个值(数组)是否相等 **/
    if (Array.isArray(firstValue)) {
      if (!Array.isArray(secondValue)) {
        return false;
      }

      if(firstValue.length != secondValue.length) {
        return false;
      }

      return firstValue.every((item, index) => {
        return item === secondValue[index];
      });
    }

    return firstValue === secondValue;
  }

  private handleShowActionSheet = () => {
    this.feedback.showFeedback('uploaderActionSheet');  //这是显示ActionSheet选择弹窗。。。
  }

  private handleChooseImage = async (sourceType?: 'camera') => {
    const { imageUploading, images } = this.state;
    const { count } = this.props
    if (imageUploading) {
      return;
    }

    let countNumber = count? count: 1

    const { assets } = await this.interface.chooseImage({  //上面封装的选择图片方法
      count: countNumber,
      sourceType: sourceType || undefined,
    });
    
    if(!assets) {
      return;
    }

    this.setState({
      imageUploading: true,
    });

    
    let request:any = []
    assets.map(res => {
      let req = this.apiClient.uploadFile(res.uri)   //上面封装的七牛上传方法
      request.push(req)
    })
    Promise.all(request).then(res => {
      let imgs:any = []
      res.map((e:any) => {
        if(e && e.url){
          imgs.push(e.url)
        }
      })
      imgs = [...images, ...imgs];
      this.setState({
        images: imgs.splice(0,countNumber),
        imageUploading: false,
      },
        this.handleChange
      );
    })
    
  }

  private singleEditInd?: number;  //修改单个时的索引值
  private handleChooseSingle = async(sourceType?: 'camera') => {
    let { imageUploading, images } = this.state;
    if (imageUploading) {
      return;
    }
    
    const { assets } = await this.interface.chooseImage({   //上面封装的选择图片方法
      count: 1,
      sourceType: sourceType || undefined,
    });
    if(!assets) {
      return;
    }

    this.setState({
      imageUploading: true,
    });

    const res = await this.apiClient.uploadFile(assets[0].uri)   //上面封装的七牛上传方法
    if(res && res.url && this.singleEditInd){
      images[this.singleEditInd] = res.url
    }
    this.setState({
      images: [...images],
      imageUploading: false,
    },
      this.handleChange
    );
    
  }

  private handleChooseVideo = async(sourceType?: 'camera') => {
    const { onChange } = this.props
    let { imageUploading } = this.state;
    if (imageUploading) {
      return;
    }
    
    const { assets } = await this.interface.chooseVideo({
      sourceType: sourceType
    });
    if(!assets) {
      return;
    }

    this.setState({
      imageUploading: true,
    });

    const res = await this.apiClient.uploadFile(assets[0].uri)   //上面封装的七牛上传方法
    if(res && res.url){
      //视频就不在组件中展示了,父组件处理
      if(onChange) {
        onChange(res.url)
      }
    }
    this.setState({
      imageUploading: false,
    });
    
  }

  private handleDelete = (ind:number) => {
    let { images } = this.state
    images.splice(ind,1)
    this.setState({
      images: [...images]
    },
      this.handleChange
    )
  }

  private handleChange = () => {
    const { onChange } = this.props
    const { images } = this.state
    if(onChange) {
      onChange(images)
    }
  }
}
4、最后调用
import Uploader from "@/components/Uploader";
...
          <Uploader
            count={6}
            onChange={urls => {
              this.setState({
                uploadImgs: urls
              })
            }}
            src={uploadImgs}
          />
...
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容