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

时间:2017-11-11 13:48:08

标签: javascript reactjs redux es6-promise livesearch

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

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&amp; amp;的一部分。 Redux app我正在工作并连接到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调用和错误。

建议?

修改

这对我有用。 感谢Hammerbot's amazing answer我设法创建了自己的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.

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

1 个答案:

答案 0 :(得分:2)

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

两种第一种方式是&#34;限制&#34;和&#34;去抖&#34;你的意见。这里有一篇非常好的文章解释了不同的技巧:https://css-tricks.com/debouncing-throttling-explained-examples/

Debounce等待给定时间实际执行您要执行的函数。如果在这个给定的时间内你拨打同一个电话,它会在这个给定时间再次等待,看你是否再次拨打电话。如果你不这样做,它将执行该功能。这是用这张图片说明的(取自上面提到的文章):

enter image description here

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

enter image description here

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

让我们想象以下功能:

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

我的解决方案是创建某种&#34;队列&#34;。此队列的行为是:

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);
   });
}