简体   繁体   English

收益调用(…)未调用功能

[英]Yield call(…) not invoking function

I am running into issues with the redux-saga api call method and whether it is behaving as intended. redux-saga了关于redux-saga api call方法以及它是否按预期方式运行的问题。 The crux of the issue I believe is that the call isn't calling the function passed to it. 我认为,问题的症结在于call没有调用传递给它的函数。

Here is the main generator function sendEmail that starts the api call: 这是启动api调用的主要生成器函数sendEmail

/**
 * A POST api call that will send a sendGrid email with csv data as an attachment
 *
 * @param  {object} action object containing the csv data, security key string, fields of CSV, and CSV file name
 *
 */

export function* sendEmail(action) {
  const { payload, security_key, CSVFields, CSVFileName } = action;
  //  API url
  const requestURL = `/api/email?security_key=${security_key}`;
  //  JSON2csvParser options, with the CSV fields
  const opts = { fields: CSVFields };
  //  The CSV data, which is a string
  const CSVData = payload;

  try {
    const parser = new Json2csvParser(opts);
    const csv = parser.parse(CSVData);
    //  create blob with the csv string
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });

    //  instantiate File Reader Web API
    const reader = new FileReader();

    //  when reader finishes reader.readAsDataURL(blob), stepthrough the emailAPI generator
    reader.addEventListener(
      'load',
      () => {
        const gen = emailAPI(requestURL, CSVFileName, reader);
        console.log("gen.next(): ", gen.next());
        console.log("gen.next(): ", gen.next());
        console.log("gen.next(): ", gen.next());
      },
      false,
    );

    //  reader reads the contents of the blob
    reader.readAsDataURL(blob);

    //  error handling of the reader
    reader.onerror = function (error) {
      console.log('Error: ', error);
    };

  } catch (err) {
    console.error(
      'sendEmail: Error occurred while parsing JSON to CSV ',
      err,
    );
  }
}

This generator function sendEmail will call another generator function emailAPI . 这个生成器函数sendEmail将调用另一个生成器函数emailAPI

This is the code for that generator function emailAPI : 这是该生成器函数emailAPI的代码:

function* emailAPI(url, filename, reader) {
  let readerResult = null
  yield readerResult = reader.result.split(',')[1]
  const requestBody = {
    filename,
    content: readerResult,
  };

  try {
    const response = yield call(request, url, {
      method: 'POST',
      body: JSON.stringify(requestBody),
      headers: new Headers({
        'Content-Type': 'application/json',
        Accept: 'application/json',
      }),
    });
    console.log("response: ", response);
  } catch (err) {
    console.log("err: ", err);
    yield err
  }
}

Stepping through with gen.next() , this what I get in the logs: gen.next() ,这是我在日志中得到的:

<code> gen.next()</ code>的控制台日志

As you'll see in the image, the value of the first iterator returns readerResult successfully. 正如您将在图像中看到的,第一个迭代器的值成功返回了readerResult The value of the second iterator returns the redux saga call . 第二个迭代器的值返回redux saga call Before the last iterator (which is when the generator is done) I log response , which returns undefined as does the last iterator. 在最后一个迭代器之前(即生成器完成时),我记录了response与最后一个迭代器一样返回undefined

The function passed to call , request , does work with other sagas. 传递给callrequest的函数与其他sagas一起使用。 I am logging within the request function to check if it is being called. 我在request函数中记录日志,以检查是否正在调用它。 It is not . 不是 This is behaviour I am not expecting, does anyone know why request isn't being called? 这是我所不期望的行为,有人知道为什么没有调用request吗?

EDIT Here is the request function. 编辑这是request功能。 Just a fetch being passed the url and options. 只是fetch传递的网址和选项。

import 'whatwg-fetch';

/**
 * Parses the JSON returned by a network request
 *
 * @param  {object} response A response from a network request
 *
 * @return {object}          The parsed JSON from the request
 */
function parseJSON(response) {
  if (response.status === 204 || response.status === 205) {
    return null;
  }
  return response.json();
}

/**
 * Checks if a network request came back fine, and throws an error if not
 *
 * @param  {object} response   A response from a network request
 *
 * @return {object|undefined} Returns either the response, or throws an error
 */
function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }

  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}

/**
 * Requests a URL, returning a promise
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 *
 * @return {object}           The response data
 */
export default function request(url, options) {
  console.log("request    url: ", url);
  return fetch(url, options)
    .then(checkStatus)
    .then(parseJSON);
}

The Problem 问题

The problem is in these lines: 问题出在以下几行:

const gen = emailAPI(requestURL, CSVFileName, reader);
console.log("gen.next(): ", gen.next());
console.log("gen.next(): ", gen.next());
console.log("gen.next(): ", gen.next());

I suppose you tried to iterate the generator function manually since you couldn't use yield inside the event handler. 我想您尝试手动迭代生成器函数,因为您无法在事件处理程序中使用yield redux-saga offers eventChannel for situations like this. redux-sagaeventChannel情况提供了eventChannel

First, let me explain why you got undefined and why request was never called. 首先,让我解释一下为什么您undefined以及为什么从未调用request That is because, yield call(request) will only return an effect descriptor which needs to be handled by the saga middleware. 这是因为yield call(request)将仅返回需要由saga中间件处理的效果描述符。 What you're doing here only iterates through the generator, and does nothing to handle the call effect. 您在此处执行的操作只会遍历生成器,并且不会处理调用效果。 Also in the third gen.next() , you're passing nothing (equivalent to undefined ) as the yield 's return value. 同样在第三个gen.next() ,您没有传递任何内容(等于undefined )作为yield的返回值。 That's the undefined you got in response: undefined console line. 那就是您得到的undefined response: undefined控制台行。

Solution

This is not a complete solution. 这不是一个完整的解决方案。 I hope this will guide you in the right direction. 我希望这会引导您朝正确的方向发展。 Please ask for clarification in comments if you need. 如果需要,请在评论中进行澄清。

I have slightly modified your saga code to use event channels. 我已经稍微修改了您的传奇代码以使用事件通道。 You will need to improve it to exactly match your requirement. 您将需要对其进行改进以使其完全符合您的要求。

...
import { eventChannel } from 'redux-saga';
...

let fileReaderEventChannel;

export function getFileReaderEventChannel(blob) {
  if (fileReaderEventChannel) {
    fileReaderEventChannel = eventChannel((emit) => {

      //  instantiate File Reader Web API
      const reader = new FileReader();

      //  when reader finishes reader.readAsDataURL(blob), stepthrough the emailAPI generator
      reader.addEventListener(
        'load',
        () => {
          emit({ error: null })
        },
        false,
      );

      //  reader reads the contents of the blob
      reader.readAsDataURL(blob);

      //  error handling of the reader
      reader.onerror = function (error) {
        emit({ error });
      };

      return () => { 
        // Properly close or abort FileReader here.
      };

    });
  }
  return fileReaderEventChannel;
};

/**
 * A POST api call that will send a sendGrid email with csv data as an attachment
 *
 * @param  {object} action object containing the csv data, security key string, fields of CSV, and CSV file name
 *
 */

export function* sendEmail(action) {
  const { payload, security_key, CSVFields, CSVFileName } = action;
  //  API url
  const requestURL = `/api/email?security_key=${security_key}`;
  //  JSON2csvParser options, with the CSV fields
  const opts = { fields: CSVFields };
  //  The CSV data, which is a string
  const CSVData = payload;

  try {
    const parser = new Json2csvParser(opts);
    const csv = parser.parse(CSVData);
    //  create blob with the csv string
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });

    // CHECK HERE - START
    const channel = yield call(getFileReaderEventChannel, blob);
    const fileReaderEvent = yield take(channel);
    if (fileReaderEvent.error === null) {
      yield call(emailAPI, requestURL, CSVFileName, reader);
    } else {
      console.log('error', error);
    }
    // CHECK HERE - END

  } catch (err) {
    console.error(
      'sendEmail: Error occurred while parsing JSON to CSV ',
      err,
    );
  }
}

Have a look at the code between CHECK HERE comments and the new function getFileReaderEventChannel . 看一下CHECK HERE注释和新函数getFileReaderEventChannel之间的代码。

References: 参考文献:

Basically the problem is unfortunately you can not use yield call(fn,arguments) within a callback in a generator function ( sendEmail in this case) as the callback is expected to be a regular function, not a generator one. 基本上,问题是不幸的是,您不能在生成器函数(在本例中为sendEmail yield call(fn,arguments)的回调中使用yield call(fn,arguments) ,因为该回调应该是常规函数,而不是生成器函数。

Instead you create a eventChannel within the closure of the generator function ( sendEmail ) and pass eventChannel.emit as the callback. 相反,您可以在生成器函数( sendEmail )的闭包内创建一个eventChannel ,并将eventChannel.emit作为回调传递。 Then you listen into that eventChannel where you can use yield call(fn,arguments) 然后,侦听该eventChannel ,在其中可以使用yield call(fn,arguments)

A simpler use case would be clearer https://redux-saga.js.org/docs/advanced/Channels.html 一个更简单的用例将更加清晰https://redux-saga.js.org/docs/advanced/Channels.html

<!-- language: lang-js -->
import { eventChannel, delay } from "redux-saga";

function* sendEmail(action) {
  const { payload, security_key, CSVFields, CSVFileName } = action;

  //  JSON2csvParser options, with the CSV fields
  const opts = { fields: CSVFields };
  //  The CSV data, which is a string
  const CSVData = payload;

  try {
    const parser = new Json2csvParser(opts);
    const csv = parser.parse(CSVData);
    //  create blob with the csv string
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
    //  instantiate File Reader Web API
    const reader = new FileReader();

    const loadChannel = eventChannel(emit => {
      reader.addEventListener("load", emit, false);
      const unsubscribeFn = () => reader.removeEventListener("load");
      return unsubscribeFn;
    });
    yield spawn(onReaderLoad, loadChannel);
    //  reader reads the contents of the blob
    reader.readAsDataURL(blob);
    //  error handling of the reader
    reader.onerror = console.log;
  } catch (err) {
    console.error("sendEmail: Error occurred while parsing JSON to CSV ", err);
  }
}

function* onReaderLoad(channel) {
  while (true) {
    const event = yield take(channel);
    const content = event.target.result.split(",")[1];
    const requestURL = `/api/email?security_key=${security_key}`;
    const requestBody = {
      CSVFileName,
      content
    };

    try {
      const response = yield call(request, requestURL, {
        method: "POST",
        body: JSON.stringify(requestBody),
        headers: new Headers({
          "Content-Type": "application/json",
          Accept: "application/json"
        })
      });
      console.log("response: ", response);
    } catch (err) {
      console.log("err: ", err);
      yield err;
    }
  }
}

I am aware that you still need to pass CSVFileName to the onReaderLoad generator but I think it would be a nice exercise for you. 我知道您仍然需要将CSVFileName传递给onReaderLoad生成器,但是我认为这对您来说是一个不错的练习。

I have passed emit as the callback reader.addEventListener("load", emit, false); 我已经通过emit作为回调reader.addEventListener("load", emit, false); but instead we could well have passed an anonymous fn and pass any extra arguments reader.addEventListener("load", (event)=> emit {CSVFileName, event}, false); 但相反,我们可以传递一个匿名fn并传递任何额外的参数reader.addEventListener("load", (event)=> emit {CSVFileName, event}, false);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM