Node.js обещание периодически не выполняется, даже если оно выполнено - PullRequest
0 голосов
/ 08 июля 2020

Я учусь использовать MongoDB, создав простое приложение для блога. Однако часть моего кода, которая сохраняет данное сообщение, кажется, иногда дает проблемы с обещаниями, но не всегда, и успешность кода просто кажется удачей.

Каждый пост в моей базе данных хранится с следующая схема:

{
    title: String,
    author: String,
    body: String,
    slug: String,
    baseSlug: String,
    published: { type: Boolean, default: false }
}

slug определяет ссылку, используемую для доступа к сообщению в блоге, и автоматически создается на основе заголовка сообщения в блоге. Однако, если заголовки статей дублируются, к slug будет добавлен номер в конце, чтобы отличить его от похожих статей, а baseSlug останется прежним. Например:

  • Я создаю сообщение "My first post", и ему присваивается baseSlug из "my-first-post". Поскольку никакие другие сообщения не имеют такого же baseSlug, для slug также установлено значение "my-first-post".
  • Я создаю еще одно сообщение с именем "My first post", и ему присваивается baseSlug из "my-first-post". Однако, поскольку другой пост имеет такой же baseSlug, ему назначается slug "my-first-post-1".

Чтобы создать такое поведение, я написал следующий маршрут addpost в Express:

app.post("/addpost", (req, res) => {
    let postInfo = req.body;

    for (key of Object.keys(postInfo)) {
        if (postInfo[key] == "true") postInfo[key] = true;
    }

    let slug = postInfo.title
        .toLowerCase()
        .split(" ")
        .filter(hasNumber) // return /\d/.test(str);
        .slice(0, 5)
        .join("-");
    postInfo.slug = slug;

    var postData;

    Post.find({ baseSlug: postInfo.slug }, (error, documents) => {
        if (documents.length > 0) {
            let largestSlugSuffix = 0;

            for (let document of documents) {
                var fullSlug = document.slug.split("-");
                var suffix = fullSlug[fullSlug.length - 1];
                if (!isNaN(suffix)) {
                    if (parseInt(suffix) > largestSlugSuffix) {
                        largestSlugSuffix = suffix;
                    }
                }
            }

            largestSlugSuffix++;
            postInfo.baseSlug = postInfo.slug;
            postInfo.slug += "-" + largestSlugSuffix;
        } else {
            postInfo.baseSlug = postInfo.slug;
        }

        postData = new Post(postInfo);
    })
        .then(() => {
            postData
                .save()
                .then(result => {
                    res.redirect("/");
                })
                .catch(err => {
                    console.log(err);
                    res.status(400).send("Unable to save data");
                });
        })
        .catch(err => {
            console.log(err);
            res.status(400).send("Unable to save data");
        });
});

Этот код, кажется, работает большую часть времени, но иногда он дает сбой и выводит следующее:

TypeError: Cannot read property 'save' of undefined
    at C:\Users\User\BlogTest\app.js:94:18
    at processTicksAndRejections (internal/process/task_queues.js:94:5)

(Для справки, строка 94 в моем файле - postData.save())

Я подозреваю, что это связано с тем, что выполнение основной части функции занимает больше времени, чем должно, а переменная postData еще не определена. Однако postData.save() не следует выполнять до завершения обещания из-за функции обратного вызова .then().

Почему мой код ведет себя так? Есть ли способ исправить?

Ответы [ 2 ]

0 голосов
/ 08 июля 2020

Проблема в том, что вы смешиваете обещания с обратными вызовами и закрытием. Это не то, как это должно работать.

Когда вы объединяете обещания, все, что вы возвращаете в первом обработчике обещаний, будет добавлено в качестве входных данных для следующего. И если вы вернете обещание, это обещание будет обработано первым перед отправкой в ​​следующий объект.

Итак, вам нужно вернуть обещания из своих обещаний, например:

app.post("/addpost", (req, res) => {
  let postInfo = req.body;

  for (key of Object.keys(postInfo)) {
    if (postInfo[key] == "true") postInfo[key] = true;
  }

  let slug = postInfo.title
    .toLowerCase()
    .split(" ")
    .filter(hasNumber) // return /\d/.test(str);
    .slice(0, 5)
    .join("-");
  postInfo.slug = slug;

  // var postData; <-- Don't do that

  Post.find({ baseSlug: postInfo.slug })
    .then((documents) => {
      if (documents.length > 0) {
        let largestSlugSuffix = 0;

        for (let document of documents) {
          var fullSlug = document.slug.split("-");
          var suffix = fullSlug[fullSlug.length - 1];
          if (!isNaN(suffix)) {
            if (parseInt(suffix) > largestSlugSuffix) {
              largestSlugSuffix = suffix;
            }
          }
        }

        largestSlugSuffix++;
        postInfo.baseSlug = postInfo.slug;
        postInfo.slug += "-" + largestSlugSuffix;
      } else {
        postInfo.baseSlug = postInfo.slug;
      }
      return new Post(postInfo);
      // We could actually have called postData.save() in this method,
      // but I wanted to return it to exemplify what I'm talking about
    })
    // It is important to return the promise generated by postData.save().
    // This way it will be resolved first, before invoking the next .then method
    .then( (postData) => { return postData.save(); })
    // This method will wait postData.save() to complete
    .then( () => { res.redirect("/"); })
    .catch( (err) => {
      console.log(err);
      res.status(400).send("Unable to save data");
    });
});

Это можно значительно упростить с помощью async / await:

app.post("/addpost", async (req, res) => {
  try {
    let postInfo = req.body;

    for (key of Object.keys(postInfo)) {
      if (postInfo[key] == "true") postInfo[key] = true;
    }

    let slug = postInfo.title
      .toLowerCase()
      .split(" ")
      .filter(hasNumber)
      .slice(0, 5)
      .join("-");
    postInfo.slug = slug;

    let documents = await Post.find({ baseSlug: postInfo.slug });
    if (documents.length > 0) {
      let largestSlugSuffix = 0;

      for (let document of documents) {
        var fullSlug = document.slug.split("-");
        var suffix = fullSlug[fullSlug.length - 1];
        if (!isNaN(suffix)) {
          if (parseInt(suffix) > largestSlugSuffix) {
            largestSlugSuffix = suffix;
          }
        }
      }
      largestSlugSuffix++;
      postInfo.baseSlug = postInfo.slug;
      postInfo.slug += "-" + largestSlugSuffix;
    } else {
      postInfo.baseSlug = postInfo.slug;
    }
    let postData = new Post(postInfo);
    await postData.save();
    res.redirect("/");
  } catch (err) {
    console.log(err);
    res.status(400).send("Unable to save data");
  };
});
0 голосов
/ 08 июля 2020

Вы смешиваете обратные вызовы и обещания, и хотя он может что-то делать, я не уверен, что именно он будет делать. Вы должны выбрать одно или другое и не смешивать их как можно больше. Я бы рекомендовал выбирать обещания, если вы используете язык, поддерживающий async / await, в противном случае - обратные вызовы.

Так, например, ваш внешний обработчик может быть asyn c function

app.post("/addpost", async (req, res) => {
  //...
})

Your реальная ошибка заключается в обработке Post.find, вы обрабатываете ее отчасти с помощью обратного вызова и отчасти с обещанием, и, вероятно, происходит то, что его случайное значение будет вызвано первым обратным вызовом или разрешением обещания. Вместо обоих, вы должны просто сделать это сейчас, когда у вас есть функция asyn c:

try {
  const posts = await Post.find({ baseSlug: postInfo.slug });

  // stuff you were doing in the callback
  const post = new Post(postInfo)
  
  // Now the promise code
  await post.save()

  // success!
  res.redirect("/");

} catch (err) {
  // With an async function you can just catch errors like normal
  console.log(err);
  res.status(400).send("Unable to save data");
}  

Если вы не используете webpack или typescript и не можете настроить таргетинг на es7, тогда и, следовательно, не можете использовать async / await, тогда Я бы порекомендовал просто использовать обратные вызовы, не используйте .then или .catch, и это будет больше похоже:

function error(err) {
  console.log(err)
  res.status(400).send("Unable to save data")
}

Post.find({ baseSlug: postInfo.slug }, (err, documents) => {
  if (err) return error(err)

  // stuff you're doing in the callback now
  const post = new Post(postInfo)

  post.save((err) => {
    if (err) return error(err)
    
    // success!
    res.redirect("/");
  })
})
...