【问题标题】:How to start a timer without page refresh (Rails/JavaScript)?如何在不刷新页面的情况下启动计时器(Rails/JavaScript)?
【发布时间】:2022-07-23 12:12:57
【问题描述】:

我有一个名为“Deal”的模型,它具有start_atend_at 属性。我已经使用hotwire/stimulus JS 实现了一个倒计时。

  1. 当交易开始时(开始日期为过去,结束日期为未来),将显示显示剩余交易时间的倒数计时器。例如剩余交易时间:2 小时、4 分钟、30 秒等。它将递减 1 秒。
  2. 如果交易尚未开始(开始日期在未来),页面将显示“交易将于 #{datetime} 开始”。

但是,如果交易在此期间开始(即从“交易将于 #{datetime} 开始”转换为倒计时),用户需要刷新他们当前所在的页面以查看计时器。我想知道在不刷新页面的情况下启动计时器的最佳方法是什么。谢谢。

【问题讨论】:

  • 添加您正在使用的最少控制器代码会很棒。这样会更容易回答问题。

标签: javascript ruby-on-rails timer stimulusjs


【解决方案1】:

管理每 X 毫秒运行一些功能的“计时器”的方法是通过浏览器的setInterval function

这个函数可以这样使用——const intervalID = setInterval(myCallback, 500);——其中myCallback是每500ms尝试运行的函数。

可以通过调用clearInterval 并为其提供作为setInterval 结果创建的间隔ID 来“取消”计时器。

示例 HTML

  • 这里我们有一个基本的 HTMl 结构,我们在其中设置控制器 timer 并设置从/到时间以及基于三种状态保存消息的目标。
  • 这三种状态分别是“之前”、“期间”(当前时间介于两者之间)和“之后”。
<section class="prose m-5">
  <div
    data-controller="timer"
    data-timer-from-value="2022-03-08T10:41:32.111Z"
    data-timer-to-value="2022-03-09T11:10:32.111Z"
  >
    <div style="display: none" data-timer-target="before">
      Deal will start on <time data-timer-target="fromTime"></time>
    </div>
    <div style="display: none" data-timer-target="during">
      Deal is active <time data-timer-target="toTimeRelative"></time>
    </div>
    <div style="display: none" data-timer-target="after">
      Deal ended on <time data-timer-target="toTime"></time>
    </div>
  </div>
</section>

示例刺激控制器

  • 这个timerController 接受tofrom 时间作为字符串(最好使用ISO 字符串,记住时区的细微差别可能很复杂)。
  • 当控制器连接时,我们做三件事; 1.设置一个定时器,每X毫秒运行一次this.update,并将定时器ID放在类上,以便稍后清除为this._timer。 2. 设置时间值(消息的内部时间标签)。 3. 初始运行this.update方法。
  • this.getTimeData 解析从/到日期时间字符串并进行一些基本验证,它还返回这些日期对象以及 status 字符串,该字符串将是 BEFORE/DURING/AFTER 之一。
  • this.update - 这会根据已解决的状态显示/隐藏相关的消息部分。
import { Controller } from '@hotwired/stimulus';

const BEFORE = 'BEFORE';
const DURING = 'DURING';
const AFTER = 'AFTER';

export default class extends Controller {
  static values = {
    interval: { default: 500, type: Number },
    locale: { default: 'en-GB', type: String },
    from: String,
    to: String,
  };

  static targets = [
    'before',
    'during',
    'after',
    'fromTime',
    'toTime',
    'toTimeRelative',
  ];

  connect() {
    this._timer = setInterval(() => {
      this.update();
    }, this.intervalValue);

    this.setTimeValues();
    this.update();
  }

  getTimeData() {
    const from = this.hasFromValue && new Date(this.fromValue);
    const to = this.hasToValue && new Date(this.toValue);

    if (!from || !to) return;
    if (from > to) {
      throw new Error('From time must be after to time.');
    }

    const now = new Date();

    const status = (() => {
      if (now < from) return BEFORE;

      if (now >= from && now <= to) return DURING;

      return AFTER;
    })();

    return { from, to, now, status };
  }

  setTimeValues() {
    const { from, to, now } = this.getTimeData();
    const locale = this.localeValue;

    const formatter = new Intl.DateTimeFormat(locale, {
      dateStyle: 'short',
      timeStyle: 'short',
    });

    this.fromTimeTargets.forEach((element) => {
      element.setAttribute('datetime', from);
      element.innerText = formatter.format(from);
    });

    this.toTimeTargets.forEach((element) => {
      element.setAttribute('datetime', to);
      element.innerText = formatter.format(to);
    });

    const relativeFormatter = new Intl.RelativeTimeFormat(locale, {
      numeric: 'auto',
    });

    this.toTimeRelativeTargets.forEach((element) => {
      element.setAttribute('datetime', to);
      element.innerText = relativeFormatter.format(
        Math.round((to - now) / 1000),
        'seconds'
      );
    });
  }

  update() {
    const { status } = this.getTimeData();

    [
      [BEFORE, this.beforeTarget],
      [DURING, this.duringTarget],
      [AFTER, this.afterTarget],
    ].forEach(([key, element]) => {
      if (key === status) {
        element.style.removeProperty('display');
      } else {
        element.style.setProperty('display', 'none');
      }
    });

    this.setTimeValues();

    if (status === AFTER) {
      this.stopTimer();
    }
  }

  stopTimer() {
    const timer = this._timer;

    if (!timer) return;

    clearInterval(timer);
  }

  disconnect() {
    // ensure we clean up so the timer is not running if the element gets removed
    this.stopTimer();
  }
}

【讨论】:

  • 实际上我想从“交易将在 {datetime} 开始”(之前)转换为“交易剩余时间:# 小时,# 分钟,# 秒”(DURING - 这减少了1 秒)无需用户刷新页面。我的理解是我需要继续使用每隔 X 秒(在我的情况下为 1 秒)运行的计时器,即使页面加载状态为 BEFORE 即交易尚未打开。我最初在当前时间
  • @farha - 这个答案现在已经更新,你可以用任何你想要的方式格式化“toTimeReleative”(只是默认在上面的代码中显示秒)。关键的变化是setTimeValues 方法现在也运行每个“间隔”,计时器将在不再需要时停止。
  • 轻微的挑剔:“将每 500 毫秒运行一次”应该是“将尝试最多每 500 毫秒运行一次”。 setInterval/setTimeout 会随着时间的推移而漂移,它本身并不是一个真正准确的时钟。
  • 谢谢。更新
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-20
  • 2019-11-25
  • 2017-12-21
  • 2014-04-12
  • 2017-05-11
  • 2011-06-15
相关资源
最近更新 更多