Reactjs 和 redux - 如何防止来自实时搜索组件的过多 api 调用?

Reactjs and redux - How to prevent excessive api calls from a live-search component?

我创建了这个实时搜索组件:

class SearchEngine extends Component {
  constructor (props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.handleSearch = this.handleSearch.bind(this);
  }
  handleChange (e) {
      this.props.handleInput(e.target.value); //Redux
  }
  handleSearch (input, token) {
     this.props.handleSearch(input, token) //Redux
  };
  componentWillUpdate(nextProps) {
      if(this.props.input !== nextProps.input){
          this.handleSearch(nextProps.input,  this.props.loginToken);
      }
  }
  render () {
      let data= this.props.result;
      let searchResults = data.map(item=> {
                  return (
                      <div key={item.id}>
                              <h3>{item.title}</h3>
                              <hr />
                              <h4>by {item.artist}</h4>
                              <img alt={item.id} src={item.front_picture} />
                      </div>
                  )
              });
      }
      return (
          <div>
              <input name='input'
                     type='text'
                     placeholder="Search..."
                     value={this.props.input}
                     onChange={this.handleChange} />
              <button onClick={() => this.handleSearch(this.props.input, this.props.loginToken)}>
                  Go
              </button>
              <div className='search_results'>
                  {searchResults}
              </div>
          </div>
      )
}

它是我正在开发的 React & Redux 应用程序的一部分,并且连接到 Redux 商店。 问题是,当用户输入搜索查询时,它会为输入中的每个字符触发 API 调用,并创建过多的 API 调用,从而导致错误,例如显示先前的结果查询,而不是跟进当前的搜索输入。

我的 api 通话 (this.props.handleSearch):

export const handleSearch = (input, loginToken) => {
  const API= `https://.../api/search/vector?query=${input}`;
  }
  return dispatch => {
      fetch(API, {
          headers: {
              'Content-Type': 'application/json',
              'Authorization': loginToken
          }
      }).then(res => {
          if (!res.ok) {
              throw Error(res.statusText);
          }
          return res;
      }).then(res => res.json()).then(data => {
          if(data.length === 0){
              dispatch(handleResult('No items found.'));
          }else{
              dispatch(handleResult(data));
          }
      }).catch((error) => {
          console.log(error);
         });
      }
};

我的意图是它会是一个实时搜索,并根据用户输入进行自我更新。但我正在尝试找到一种方法来等待用户完成他的输入,然后应用更改以防止过度 API 调用和错误。

建议?

编辑:

以下是对我有用的方法。 感谢 我设法创建了自己的 class QueueHandler。

export default class QueueHandler {

constructor () { // not passing any "queryFunction" parameter
    this.requesting = false;
    this.stack = [];
}

//instead of an "options" object I pass the api and the token for the "add" function. 
//Using the options object caused errors.

add (api, token) { 
    if (this.stack.length < 2) {
        return new Promise ((resolve, reject) => {
            this.stack.push({
                api,
                token,
                resolve,
                reject
            });
            this.makeQuery()
        })
    }
    return new Promise ((resolve, reject) => {
        this.stack[1] = {
            api,
            token,
            resolve,
            reject
        };
        this.makeQuery()
    })

}

makeQuery () {
    if (! this.stack.length || this.requesting) {
        return null
    }

    this.requesting = true;
// here I call fetch as a default with my api and token
    fetch(this.stack[0].api, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': this.stack[0].token
        }
    }).then(response => {
        this.stack[0].resolve(response);
        this.requesting = false;
        this.stack.splice(0, 1);
        this.makeQuery()
    }).catch(error => {
        this.stack[0].reject(error);
        this.requesting = false;
        this.stack.splice(0, 1);
        this.makeQuery()
    })
}
}

我做了一些更改以使其对我有用(请参阅评论)。

我导入了它并赋值了一个变量:

//searchActions.js file which contains my search related Redux actions

import QueueHandler from '../utils/QueueHandler';

let queue = new QueueHandler();

然后在我原来的handleSearch函数中:

export const handleSearch = (input, loginToken) => {
  const API= `https://.../api/search/vector?query=${input}`;
  }
  return dispatch => {
    queue.add(API, loginToken).then...  //queue.add instead of fetch.

希望这对任何人都有帮助!

我认为它们是处理问题的几种策略。我将在这里讨论 3 种方法。

前两种方式是 "throttling" 和 "debouncing" 您的输入。这里有一篇很好的文章解释了不同的技术:https://css-tricks.com/debouncing-throttling-explained-examples/

Debounce 会等待给定的时间来实际执行您要执行的功能。如果在这个给定时间内你打了同样的电话,它会再次等待这个给定时间看你是否再次打给它。如果不这样做,它将执行该功能。这张图片对此进行了说明(取自上述文章):

Throttle 直接执行该函数,等待给定时间等待新调用并执行在此给定时间内进行的最后一次调用。以下架构对此进行了解释(摘自本文 http://artemdemo.me/blog/throttling-vs-debouncing/):

我最初使用的是那些最初的技术,但我发现了它的一些缺点。最主要的是我无法真正控制组件的渲染。

让我们想象一下下面的函数:

function makeApiCall () {
  api.request({
    url: '/api/foo',
    method: 'get'
  }).then(response => {
    // Assign response data to some vars here
  })
}

如您所见,该请求使用了一个异步过程,稍后将分配响应数据。现在让我们想象两个请求,我们总是想使用已经完成的最后一个请求的结果。 (这就是您在搜索输入中想要的)。但是第二个请求的结果先于第一个请求的结果。这将导致您的数据包含错误的响应:

1. 0ms -> makeApiCall() -> 100ms -> assigns response to data
2. 10ms -> makeApiCall() -> 50ms -> assigns response to data

我的解决方案是创建某种 "queue"。这个队列的行为是:

1 - 如果我们将任务添加到队列中,则该任务排在队列前面。 2 - 如果我们向队列中添加第二个任务,则该任务位于第二个位置。 3 - 如果我们将第三个任务添加到队列中,该任务将替换第二个。

因此队列中最多有两个任务。一旦第一个任务结束,就会执行第二个任务,依此类推...

所以你总是得到相同的结果,并且你限制了你的 api 调用许多参数的功能。如果用户的互联网连接速度较慢,第一个请求将需要一些时间来执行,因此不会有很多请求。

这是我用于此队列的代码:

export default class HttpQueue {

  constructor (queryFunction) {
    this.requesting = false
    this.stack = []
    this.queryFunction = queryFunction
  }

  add (options) {
    if (this.stack.length < 2) {
      return new Promise ((resolve, reject) => {
        this.stack.push({
          options,
          resolve,
          reject
        })
        this.makeQuery()
      })
    }
    return new Promise ((resolve, reject) => {
      this.stack[1] = {
        options,
        resolve,
        reject
      }
      this.makeQuery()
    })

  }

  makeQuery () {
    if (! this.stack.length || this.requesting) {
      return null
    }

    this.requesting = true

    this.queryFunction(this.stack[0].options).then(response => {
      this.stack[0].resolve(response)
      this.requesting = false
      this.stack.splice(0, 1)
      this.makeQuery()
    }).catch(error => {
      this.stack[0].reject(error)
      this.requesting = false
      this.stack.splice(0, 1)
      this.makeQuery()
    })
  }
}

你可以这样使用:

// First, you create a new HttpQueue and tell it what to use to make your api calls. In your case, that would be your "fetch()" function:

let queue = new HttpQueue(fetch)

// Then, you can add calls to the queue, and handle the response as you would have done it before:

queue.add(API, {
    headers: {
        'Content-Type': 'application/json',
        'Authorization': loginToken
    }
}).then(res => {
    if (!res.ok) {
        throw Error(res.statusText);
    }
    return res;
}).then(res => res.json()).then(data => {
    if(data.length === 0){
        dispatch(handleResult('No vinyls found.'));
    }else{
        dispatch(handleResult(data));
    }
}).catch((error) => {
    console.log(error);
   });
}