关于在通过值数组进行映射时如何生成 JS 倒计时的指南

Guidance on how to generate JS Countdowns when mapping through an array of values

在将值数组映射到 HTML 时,我正在努力寻找为每个项目显示倒数计时器的最佳方法。

我正在做一个项目,我从 API 访问值并在我的网站上以卡片格式显示它们。所有信息都按预期显示,但我不知道如何为每张卡片显示唯一的倒数计时器。

目前,我正尝试在每次映射迭代时 运行 一个函数,该函数将生成倒计时并显示在 'countdownId' 的 ID 上。这不起作用,因为我收到 'Uncaught TypeError: Cannot set 属性 'innerHTML' of null' 的错误。我知道这是因为 id 不是在函数 运行ning 时生成的,但我想不出显示实际倒计时的替代方法。我已经尝试研究,但没有任何进展。

提前感谢任何帮助。

Index.html - 将此限制为与显示所有卡片关联的相关 ID。

<ul id="appTest"></ul>

card.js - 这里是 API 被解析和单独卡片形成的地方

import { initializeClock } from './timer';

const rocketCard = (array) => {

    const cardReturn = array.map(indiv => {
    
  
        let rocketCard= `<li class='rocket-card'>
                        <img src="${indiv.rocketPhoto}" />
                        <h2>${indiv.rocketName}</h2>
                        <div class='card-body'>
                            <p class='card-subtitle'> Launch Date:</p>
                            <p>${indiv.launchDate}</p>
                            </br>
                            <p class='card-subtitle'>Launch Location: </p>
                            <p> ${indiv.location} </p>
                            <div id='countdownId'></div>
                            </br>
                            <p class='card-subtitle'>Upcoming Mission Description:</p>
                            <p class='card-desc'>${indiv.description}</p>
                            </div>
                            </li>`
        initializeClock('countdownId', indiv.launchDate)
        return rocketCard;
    })
    document
        .getElementById('appTest')
        .innerHTML = cardReturn.join('')
}

const rocketCollection = () => {
  
    let rocketArr = [];
 
    // fetch('https://ll.thespacedevs.com/2.0.0/launch/upcoming/?limit=12') //Live Data subject to limits
    fetch('https://lldev.thespacedevs.com/2.0.0/launch/upcoming/?limit=12') //Fetch stale data for development
    .then(response => response.json())
    .then(response => response.results.forEach(indiv => (
        rocketArr.push({
        rocketName: indiv.name,
        launchDate: indiv.window_start,
        rocketPhoto: indiv.image, 
        location: indiv.pad.location.name,
        description: indiv.mission ? indiv.mission.description : 'No description available'
    }))))
  .then(() => rocketCard(rocketArr))
};

timer.js - 我用来创建倒数计时器的当前函数。

const myTimer = (deadline) => {
    let theDeadline = new Date(deadline).getTime();
    let now = new Date().getTime();
    let timeleft = theDeadline - now;
    let days = Math.floor(timeleft / (1000 * 60 * 60 * 24));
    let hours = Math.floor((timeleft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    let minutes = Math.floor((timeleft % (1000 * 60 * 60)) / (1000 * 60));
    let seconds = Math.floor((timeleft % (1000 * 60)) / 1000);
   
    return {days, hours, minutes, seconds};
};

export const initializeClock = (id, endtime) => {
    const clock = document.getElementById(id);
    const timeinterval = setInterval(() => {
        const t = myTimer(endtime);
        clock.innerHTML =   'days: ' + t.days + '<br>' +
                            'hours: '+ t.hours + '<br>' +
                            'minutes: ' + t.minutes + '<br>' +
                            'seconds: ' + t.seconds;
        if (t.total <= 0) {
            clearInterval(timeinterval);
        }
    },1000);
}

让我们把它分成两部分。

首先是在 DOM 中打印出内容。您正确地确定 Cannot set property 'innerHTML' of null' 错误是因为该元素尚未插入到 DOM 中。除此之外,我还发现了两个问题:

  1. 您在名为 rocketCard 的函数中有一个名为 rocketCard 的变量。你在那里自找麻烦,因为任何时候你试图访问保存你的标记的变量,你都可能最终意外调用该函数。因此,为了使调试更容易,我已将函数重命名为更符合实际功能的名称:initDom.
  2. 您在每次倒计时时使用相同的 ID – countdownId。因此,即使您确实将标记正确地插入到 DOM 中,事情也不会奏效。我们需要为每个倒计时生成一个唯一的 ID。

下面我将 map 更改为 forEach,其中 首先 将元素插入 DOM(使用 innerHTML += 而不是在结束时立即将所有内容倾倒),然后 初始化倒计时。

const myTimer = (deadline) => {
  let theDeadline = new Date(deadline).getTime();
  let now = new Date().getTime();
  let timeleft = theDeadline - now;
  let days = Math.floor(timeleft / (1000 * 60 * 60 * 24));
  let hours = Math.floor((timeleft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  let minutes = Math.floor((timeleft % (1000 * 60 * 60)) / (1000 * 60));
  let seconds = Math.floor((timeleft % (1000 * 60)) / 1000);

  return {
    days,
    hours,
    minutes,
    seconds
  };
};

const initializeClock = (id, endtime) => {
  const clock = document.getElementById(id);
  const timeinterval = setInterval(() => {
    const t = myTimer(endtime);
    clock.innerHTML = 'days: ' + t.days + '<br>' +
      'hours: ' + t.hours + '<br>' +
      'minutes: ' + t.minutes + '<br>' +
      'seconds: ' + t.seconds;
    if (t.total <= 0) {
      clearInterval(timeinterval);
    }
  }, 1000);
};

const initDom = (array) => {
  const app = document.getElementById('appTest');
  array.forEach((indiv, index) => {
    const countdownId = `countdownId-${index}`;
    const rocketCard = `<li class='rocket-card'>
      <img src="${indiv.rocketPhoto}" />
      <h2>${indiv.rocketName}</h2>
      <div class='card-body'>
        <p class='card-subtitle'> Launch Date:</p>
          <p>${indiv.launchDate}</p>
        </br>
        <p class='card-subtitle'>Launch Location: </p>
          <p>${indiv.location} </p>
          <div id="${countdownId}"></div>
        </br>
        <p class='card-subtitle'>Upcoming Mission Description:</p>
        <p class='card-desc'>${indiv.description}</p>
      </div>
    </li>
    `;
    app.innerHTML += rocketCard;
    initializeClock(countdownId, indiv.launchDate);
  });
};

const createRocketCollection = () => {
  let rocketArr = [];

  //fetch('https://ll.thespacedevs.com/2.0.0/launch/upcoming/?limit=12') //Live Data subject to limits
  fetch('https://lldev.thespacedevs.com/2.0.0/launch/upcoming/?limit=12') //Fetch stale data for development
    .then(response => response.json())
    .then(response => {
      response.results.forEach(indiv => {
        rocketArr.push({
          rocketName: indiv.name,
          launchDate: indiv.window_start,
          rocketPhoto: indiv.image,
          location: indiv.pad.location.name,
          description: indiv.mission ? indiv.mission.description : 'No description available'
        });
      });
    })
    .then(() => initDom(rocketArr));
};

createRocketCollection();
<ul id="appTest"></ul>

所以现在我们已经显示了一些东西,但我们的倒计时实际上并不存在。给出了什么?

首先,您将时间间隔存储在一个名为 timeinterval 的变量中,这很好,因为您可以稍后清除它,但您永远不会真正调用 timeinterval().

如果我们解决这个问题,我们会看到我们的循环运行一次然后抛出一个错误。但我们确实得到了一个工作倒计时,哇!

下一个要解决的问题基本上是您需要在最初创建它们的循环之外保持对每个计时器的引用。可能有很多方法可以做到这一点,但作为一个例子,我所做的是制作一个数组,其中数组中的每个项目对应于其中一枚火箭并包含一个看起来像 { startInterval: () => ... }.[= 的对象26=]

初始化 DOM 后,我们循环返回这个新数组,通过调用那些 startInterval 函数来初始化火箭计时器。

let countdowns = [];

const myTimer = (deadline) => {
  let theDeadline = new Date(deadline).getTime();
  let now = new Date().getTime();
  let timeleft = theDeadline - now;
  let days = Math.floor(timeleft / (1000 * 60 * 60 * 24));
  let hours = Math.floor((timeleft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  let minutes = Math.floor((timeleft % (1000 * 60 * 60)) / (1000 * 60));
  let seconds = Math.floor((timeleft % (1000 * 60)) / 1000);

  return {
    days,
    hours,
    minutes,
    seconds
  };
};

const initializeClock = (id, endtime, index) => {
  return {
    startInterval: setInterval(() => {
      const clock = document.getElementById(id);
      const t = myTimer(endtime);
      clock.innerHTML = 'days: ' + t.days + '<br>' +
        'hours: ' + t.hours + '<br>' +
        'minutes: ' + t.minutes + '<br>' +
        'seconds: ' + t.seconds;
      if (t.total <= 0) {
        clearInterval(countdowns[index].startInterval);
      }
    }, 1000),
  };
};

const initDom = (array) => {
  const app = document.getElementById('appTest');
  array.forEach((indiv, index) => {
    const countdownId = `countdownId-${index}`;
    const rocketCard = `<li class='rocket-card'>
      <img src="${indiv.rocketPhoto}" />
      <h2>${indiv.rocketName}</h2>
      <div class='card-body'>
        <p class='card-subtitle'> Launch Date:</p>
          <p>${indiv.launchDate}</p>
        </br>
        <p class='card-subtitle'>Launch Location: </p>
          <p>${indiv.location} </p>
          <div id="${countdownId}"></div>
        </br>
        <p class='card-subtitle'>Upcoming Mission Description:</p>
        <p class='card-desc'>${indiv.description}</p>
      </div>
    </li>
    `;
    app.innerHTML += rocketCard;
    const rocketClock = initializeClock(countdownId, indiv.launchDate, index);
    countdowns.push(rocketClock);
  });
};

const createRocketCollection = () => {
  let rocketArr = [];

  //fetch('https://ll.thespacedevs.com/2.0.0/launch/upcoming/?limit=12') //Live Data subject to limits
  fetch('https://lldev.thespacedevs.com/2.0.0/launch/upcoming/?limit=12') //Fetch stale data for development
    .then(response => response.json())
    .then(response => {
      response.results.forEach(indiv => {
        rocketArr.push({
          rocketName: indiv.name,
          launchDate: indiv.window_start,
          rocketPhoto: indiv.image,
          location: indiv.pad.location.name,
          description: indiv.mission ? indiv.mission.description : 'No description available'
        });
      });
    })
    .then(() => initDom(rocketArr));
};

createRocketCollection();
countdowns.forEach(countdown => countdown.startInterval());
<ul id="appTest"></ul>

所以你有一个 DOM 充满了 10 个不同计时器的东西 运行。

公平警告,您可能需要稍微修改一下。我没有对它或任何东西进行全面测试,很难说一切正常(特别是清除计时器归零时的间隔),因为开发人员 API 的发射日期对于每枚火箭都是相同的,但这应该会让你朝着正确的方向前进。