调用后无法清除 AbortController.abort()

Unable to clear AbortController.abort() after called

大家下午好, 这是一个目前让我感到困惑的问题,我似乎无法只见树木不见森林。

我有一个带有内置文件上传器的 React Native 应用程序,这个上传器的一部分是使用 abortcontroller,它允许代码向获取请求发送信号以停止正在进行的调用 这完美地工作,正如人们所期望的问题是,如果用户然后选择另一个文件或尝试上传之前取消的文件,我的承诺 returns 立即取消,并且中止错误仍然存​​在,阻止任何进一步的上传,无论是出于爱还是金钱我能不能找到一种方法来阻止这种情况的发生。

这是我屏幕的缩小版(删除了网址,删除了一些数据点等)以保护我系统的隐私

import React from 'react';
import {StyleSheet,View,ScrollView,Alert,} from 'react-native';
import AppSettings from '../../constants/AppSettings'
import Colours from '../../constants/Colours';
import CustomHeader from '../../components/CustomHeader';
import CustomButton from '../../components/CustomButton';
import TextContent from '../../components/TextContent';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Constants from 'expo-constants';
import * as DocumentPicker from 'expo-document-picker';
import * as Permissions from 'expo-permissions';
import CustomInput from '../../components/CustomInput';
import Progressbar from '../../components/ProgressBar';
import * as Network from 'expo-network';

export default class UploadScreen extends React.Component {

  state = {
    file: null,
    filetype: null,
    fileExtention:null,
    uploading: false,
    pickResult: null,
    mainDataLoaded: false,
    docDesc:null,
    enteredPassword:'',
    currentUploadPercent:0,
    uploadTime: 0,
    startPick: false,
  };
render() {


    return (
      <View style={styles.screenContainer}>
      <LinearGradient start={[0, 1]} end={[0,0.9]} colors={['rgba(163, 163, 163,1)', 'rgba(163, 163, 163,0)']} style={{flex:1}} >
        <ScrollView style={styles.scrollContainer} contentContainerStyle={styles.defaultContainer}>
          <View style={{flex:1, alignItems: 'center', justifyContent: 'center', width:'100%' }}>
            <TextContent>Upload file containing: {this.state.docDesc}</TextContent>
            {this._maybeRenderFile()}
            {this._maybeRenderControls()}
            {this._maybeRenderUploadingIndicator()}
          </View>
        </ScrollView>
      </LinearGradient>
      </View>
    );
  }

  _maybeRenderUploadingIndicator = () => {

      if (this.state.uploading) {
      return (
        <View style={{width:'80%',alignItems:'center'}}>
          <Progressbar progress={this.state.currentUploadPercent}/>
          <CustomButton style={{width:'100%'}} onPress={()=>{AbortUpload(this)}} title='Cancel Upload'></CustomButton>          
        </View>


      );
    }
  };
  _maybeRenderControls = () => {
    if (!this.state.uploading) {
      return (
        <View style={{width:'100%',alignItems:'center'}}>
          <CustomButton style={{width:'80%'}} onPress={this._pickImage} title='Select file to upload'><MaterialCommunityIcons style={{color:Colours.PrimaryButtonText}} name="folder-open" size={30}/></CustomButton>
        </View>
      );
    }
  };
  _maybeRenderFile = () => {
    if (this.state.file) {
      switch (this.state.filetype) {
        case 'application/pdf':
          const passwordHandler = enteredText => {
            this.setState({enteredPassword: enteredText});
          };
          return (
            <View style={{alignItems:'center'}}>
              <MaterialCommunityIcons style={{color:Colours.PrimaryText}} name="file-pdf" size={100}/>
              <TextContent style={{textAlign:'center'}}>File to upload: {this.state.file}</TextContent>
              <TextContent>If this file requires a password to access please type it below or leave blank if not required.</TextContent>
              {!this.state.uploading && (
                <View>
                  <CustomInput placeholder='PDF Password (if applicable)' autoCapitalize='characters' autoCompleteType='off' autoCorrect={false} textContentType='none' onChangeText={passwordHandler} value={this.state.enteredPassword}/>
                  <CustomButton style={{width:'100%'}} onPress={()=>{this._handleImagePicked(this.state.pickResult)}} title='Upload this file'><MaterialCommunityIcons style={{color:Colours.PrimaryButtonText}} name="file-upload-outline" size={30}/></CustomButton>
                  <TextContent style={{textAlign:'center'}}>Or</TextContent>
                </View>
              )}

            </View>
          );    
          break;
        case 'image/jpg':
        case 'image/png':
        case 'image/gif':
          return (
            <View style={{alignItems:'center'}}>
              <MaterialCommunityIcons style={{color:Colours.PrimaryText}} name="file-image" size={100}/>
              <TextContent style={{textAlign:'center'}}>File to upload: {this.state.file}</TextContent>
              {!this.state.uploading && (
                <View>
                  <CustomButton style={{minWidth:'80%'}} onPress={()=>{this._handleImagePicked(this.state.pickResult)}} title='Upload this file'><MaterialCommunityIcons style={{color:Colours.PrimaryButtonText}} name="file-upload-outline" size={30}/></CustomButton>
                  <TextContent style={{textAlign:'center'}}>Or</TextContent>
                </View>
              )}
            </View>
          );
          break;
        default:
          break;
      }

    }
  };

  _askPermission = async (type, failureMessage) => {
    const { status, permissions } = await Permissions.askAsync(type);

    if (status === 'denied') {
      alert(failureMessage);
    }
  };


  _pickImage = async () => {
    await this._askPermission(
      Permissions.CAMERA_ROLL,
      'We need the file permission to access files from your phone...'
    );
    if(!this.state.startPick){
      this.setState({startPick: true})
      let pickerResult = await DocumentPicker.getDocumentAsync({});

      if(pickerResult.type == 'success'){
        this.setState({startPick: false})
        //Get file extention
        var splitAt = pickerResult.name.lastIndexOf(".")
        var fileExt = pickerResult.name.slice(splitAt,pickerResult.name.length).toLowerCase()
        switch (fileExt) {
          case '.pdf':
            this.setState({file: pickerResult.name, filetype: 'application/pdf', pickResult: pickerResult, fileExtention: fileExt})
            break;
          case '.jpg':
            this.setState({file: pickerResult.name, filetype: 'image/jpg', pickResult: pickerResult, fileExtention: fileExt})
            break;
          case '.jpeg':
            this.setState({file: pickerResult.name, filetype: 'image/jpg', pickResult: pickerResult, fileExtention: fileExt})
            break;
          case '.png':
            this.setState({file: pickerResult.name, filetype: 'image/png', pickResult: pickerResult, fileExtention: fileExt})
            break;
          case '.gif':
            this.setState({file: pickerResult.name, filetype: 'image/gif', pickResult: pickerResult, fileExtention: fileExt})
            break;
          default:
            this.setState({file: null, filetype: null, pickResult: null})
            Alert.alert('Unsupported filetype','For security reasons you may only select images or PDF files to upload.')
            break;
        }
      }else{
        //No file selected
        this.setState({file: null, filetype: null, pickResult: null})
        this.setState({startPick: false})
      }
      if(__DEV__){console.log('Result:', pickerResult)}
    }else{
      if(__DEV__){console.log('Pick already started')}
    }

  };
_StatusCheck = async() =>{
  return fetch('Url for server side upload status response')
  .then((response) => response.json())
  .then((responseJson) => {
    return responseJson
  })
  .catch((error) =>{
    console.error(error);
  });
}


  _handleImagePicked = async pickerResult => {
    try {

      if (!pickerResult.cancelled) {
        var thisTime = Date.now()
        this.setState({uploadTime: thisTime})
        var myPromise = new Promise(function(){})
        myPromise = MakeQuerablePromise(new uploadFileAsync(
          pickerResult.uri,
          this.state.docType + '-' + Date.now() + this.state.fileExtention,
          this.state.filetype,
          this.state.docType,
          this.state.docDesc,
          this.state.enteredPassword,
          this.state.quoteID,
          this.state.uploading,
          thisTime,
          controller.signal
        ));
        this.setState({ uploading: true });
        if(__DEV__){
          console.log("Initial fulfilled:", myPromise.isFulfilled());//false
          console.log("Initial rejected:", myPromise.isRejected());//false
          console.log("Initial pending:", myPromise.isPending());//true
        }
        for (let index = 0; index < 1;) {

          var currentStatus = await this._StatusCheck()
          var curTime = new Date()
          if(__DEV__){
            console.log('Time:',curTime.getHours(),':',curTime.getMinutes(),':',curTime.getSeconds())
            console.log('Status:',currentStatus)
            console.log("Promise status- fulfilled:", myPromise.isFulfilled(), "rejected:", myPromise.isRejected(),"pending:", myPromise.isPending());//false
            console.log('Promise Content:',myPromise)
          }
          if(!myPromise.isRejected()){
            if(currentStatus.percent != undefined){
              if(currentStatus.percent < 100){
                this.setState({currentUploadPercent:currentStatus.percent})
                if(__DEV__){
                console.log('Upload progess ' + currentStatus.percent)
                console.log("Promise status: fulfilled:", myPromise.isFulfilled(), "rejected:", myPromise.isRejected(),"pending:", myPromise.isPending());//false
                }
              }else{
                this.setState({currentUploadPercent:currentStatus.percent})
                if(__DEV__){
                console.log('Upload progess 100%')
                console.log("Promise status: fulfilled:", myPromise.isFulfilled(), "rejected:", myPromise.isRejected(),"pending:", myPromise.isPending());//false
                }
              }
            }
          }

          if(myPromise.isFulfilled() == true){
            if(__DEV__){
            console.log("Entered Fulfilled State - Promise status: fulfilled:", myPromise.isFulfilled(), "rejected:", myPromise.isRejected(),"pending:", myPromise.isPending());//false
            }
            index++
          }
          if(myPromise.isRejected() == true){
            if(__DEV__){
            console.log("Entered Rejected State - Promise status: fulfilled:", myPromise.isFulfilled(), "rejected:", myPromise.isRejected(),"pending:", myPromise.isPending());//false
            }
            index++
          }

        }
        if(myPromise.isRejected() == false){
          myPromise.then(response => response.json()).then((responseJson)=>{
            if(__DEV__){
            console.log('Promise Json:',responseJson)
            console.log("Final fulfilled:", myPromise.isFulfilled());//true
            console.log("Final rejected:", myPromise.isRejected());//false
            console.log("Final pending:", myPromise.isPending());//false
          }
            if(responseJson.datapage.result.gitUploadStatus.successful == true){
              //Successful upload
              this.props.navigation.navigate('CaptureThanks')
            }else{
              //Upload had a issue
              Alert.alert('Upload error','There was an issue with the upload, this maybe a temporary issue with your connection or with the server please ensure you have a good steady signal and try again, if the problem persists please contact us. Error Code: '+responseJson.datapage.gitUploadStatus.errorMessage)
            }
          })
        }else{
          //Rejected promise handle failure
          if(myPromise.rejectReason() == 'AbortError'){
            myPromise = MakeQuerablePromise(new Promise(function(resolve, reject){
              resolve('AbortError')
            }))
          }else{
            Alert.alert('Upload error','There was an issue with the upload, this maybe a temporary issue with your connection or with the server please ensure you have a good steady signal and try again, if the problem persists please contact us. Error Code: '+responseJson.datapage.gitUploadStatus.errorMessage)
          }
        }
      }
    } catch (e) {
      if(__DEV__){
        console.log('Error Name:',e.name)
        console.log('Catch Error:',{ e });
      }
        return;
    } finally {
      if(__DEV__){
        console.log('Reached Final')
      }
      myPromise = MakeQuerablePromise(new Promise(function(resolve, reject){
        resolve('Finished')
      }))
      console.log(myPromise)
      this.setState({ uploading: false, currentUploadPercent:0 });
    }
  };
}
function MakeQuerablePromise(promise) {
  // Don't modify any promise that has been already modified.
  if (promise.isResolved){
    return promise
  };
  // Set initial state
  var isPending = true;
  var isRejected = false;
  var isFulfilled = false;
  var rejectReason = '';

  // Observe the promise, saving the fulfillment in a closure scope.
  var result = promise.then(
      function(v) {
          isFulfilled = true;
          isPending = false;
          rejectReason = '';
          return v; 
      }, 
      function(e) {
          isRejected = true;
          isPending = false;
          rejectReason = e.name;
          return e; 
      }
  );
  result.isFulfilled = function() { return isFulfilled; };
  result.isPending = function() { return isPending; };
  result.isRejected = function() { return isRejected; };
  result.rejectReason = function() {return rejectReason; };
  return result;
}
const controller = new AbortController;
async function uploadFileAsync(uri,name,type,docType,docDesc,password,quoteid,isUploading,uploadTime,abortSignal) {
  if(!isUploading){

    if(__DEV__){console.log('Making upload request for ',docType,' Document description:', docDesc)}
    let apiUrl = 'Url to push the upload to';

    let formData = new FormData();
    //(method) FormData.append(name: string, value: string | Blob, fileName?: string): void
    formData.append('filename', {
      uri,
      name: name,
      type: type,
      documentType:docType,
      description:docDesc,
      password:password,
      quoteid:quoteid,
    });
    let options = {
      method: 'POST',
      body: formData,
      headers: {
        Accept: 'application/json',
        'Content-Type': 'multipart/form-data',
      },
      signal: abortSignal,

    };
    if(__DEV__){console.log('Options:', options)}
    return fetch(apiUrl, options);
  }else{
    return null
  }
}
async function AbortUpload(stateObject){
  controller.abort()
  stateObject.setState({isUploading: false})
}

UploadScreen.navigationOptions = {
  header: () => <CustomHeader goback={true} title='Document Upload'/>,
  title: AppSettings.AppName + ' ',
  headerTitleStyle:{
    fontFamily: AppSettings.HeaderFont,
  },
  headerStyle: {
      backgroundColor: Colours.HeaderBackground
    },
    headerTintColor: Colours.HeaderText
};
const styles = StyleSheet.create({
  screenContainer:{
      flex:1,
      backgroundColor: Colours.PrimaryBackgroud,
  },
  scrollContainer:{
      flex: 1,
      height:'100%'
  },
  defaultContainer: {
    alignItems: 'center',
  },
});

请原谅我的代码的性质,因为我对 React Native 还是很陌生,现在只做了几个月,所以我仍然在思考很多功能和系统,只是更新到昨天 expo (36) 的最新版本,所以我可以启用获取中止。

但是任何人都知道为什么在信号被调用一次以中止每个未来请求之后似乎得到相同的信号,无论用户是否再次单击它并且我没有存储它处于状态,所以我似乎无法理解为什么它坚持到我什至在开始和结束时重建承诺以确保每次上传时都被清理的极端程度。

您分享的代码太多了,但我会尝试一下。

首先,令我惊讶的是它竟然在一开始就起作用了。您正在像这样创建中止控制器:

const controller = new AbortController;

虽然应该是:

const controller = new AbortController();

现在,当涉及到逻辑时,我认为它显示了一个错误,因为您使用的是同一个控制器,它已经被取消了。

诀窍是将它变成一个状态并在它中止后立即用新的 AbortController 更新它(可能在你的 AbortUpload 方法中,在调用 controller.abort()).