Comptes à Rebours en Direct Sans JavaScript : Turbo Streams + relative_time_in_words
La plupart des minuteries de compte à rebours nécessitent des bibliothèques JavaScript. Mais avec le nouveau helper relative_time_in_words de Rails 8 et Turbo Streams, vous pouvez construire des comptes à rebours qui se mettent à jour en direct entièrement côté serveur. Pas de packages npm, pas de calculs de dates côté client, et cela fonctionne correctement quand JavaScript est désactivé.
Ceci est un exemple un peu artificiel—une exploration amusante de ce qui est possible avec le rendu purement côté serveur. En production, vous combineriez probablement des comptes à rebours JavaScript côté client avec une validation côté serveur, ce qui utilise moins de ressources serveur tout en maintenant la précision là où ça compte (l’application réelle de l’échéance). Mais voyons jusqu’où nous pouvons pousser l’approche côté serveur.
Le Nouveau Helper
Rails 8 ajoute relative_time_in_words, qui gère à la fois les temps passés et futurs :
relative_time_in_words(3.hours.from_now)
# => "in about 3 hours"
relative_time_in_words(2.days.ago)
# => "2 days ago"
C’est différent de time_ago_in_words, qui ne gère que le passé. Avoir un seul helper pour les deux directions rend les interfaces de compte à rebours beaucoup plus simples.
Construire une Minuterie d’Enchères en Direct
Construisons un compte à rebours d’enchères qui se met à jour chaque minute sans écrire de JavaScript.
Le Modèle
class Auction < ApplicationRecord
def ended?
ends_at <= Time.current
end
def time_remaining
ends_at - Time.current
end
end
La Vue
<%= turbo_stream_from "auction_#{@auction.id}" %>
<div class="auction-timer">
<span id="countdown_<%= @auction.id %>"
class="<%= urgency_class(@auction.ends_at) %>">
<%= relative_time_in_words(@auction.ends_at) %>
</span>
</div>
Styles Basés sur l’Urgence
Ajoutez un helper qui retourne des classes CSS basées sur le temps restant :
# app/helpers/countdowns_helper.rb
module CountdownsHelper
def urgency_class(deadline)
return "ended" if deadline <= Time.current
remaining = deadline - Time.current
case remaining
when 0..1.hour then "critical"
when 1.hour..1.day then "warning"
else "normal"
end
end
end
.critical { color: #dc2626; font-weight: bold; }
.warning { color: #d97706; }
.normal { color: #059669; }
.ended { color: #6b7280; }
Le Job de Diffusion
C’est là que Turbo Streams entre en jeu. Un job récurrent diffuse les mises à jour du compte à rebours :
# app/jobs/countdown_broadcast_job.rb
class CountdownBroadcastJob < ApplicationJob
def perform(auction)
return if auction.ended?
Turbo::StreamsChannel.broadcast_update_to(
"auction_#{auction.id}",
target: "countdown_#{auction.id}",
html: render_countdown(auction)
)
# Planifier la prochaine mise à jour
self.class.set(wait: update_interval(auction)).perform_later(auction)
end
private
def render_countdown(auction)
ApplicationController.render(
partial: "auctions/countdown",
locals: { auction: auction }
)
end
def update_interval(auction)
remaining = auction.time_remaining
case remaining
when 0..5.minutes then 10.seconds
when 5.minutes..1.hour then 1.minute
else 5.minutes
end
end
end
Le job se planifie lui-même avec des intervalles dynamiques—mettant à jour toutes les 10 secondes quand l’échéance est proche, mais seulement toutes les 5 minutes quand elle est loin. Cela garde votre file de jobs gérable.
Le Partial
<%# app/views/auctions/_countdown.html.erb %>
<span class="<%= urgency_class(auction.ends_at) %>">
<% if auction.ended? %>
Terminée
<% else %>
<%= relative_time_in_words(auction.ends_at) %>
<% end %>
</span>
Démarrer le Compte à Rebours
Lancez le job de diffusion quand la page d’enchères est consultée :
class AuctionsController < ApplicationController
def show
@auction = Auction.find(params[:id])
CountdownBroadcastJob.perform_later(@auction) unless @auction.ended?
end
end
Autres Cas d’Utilisation
Ce pattern fonctionne pour tout affichage sensible au temps :
Ventes Flash
<div id="sale_timer">
La vente se termine <%= relative_time_in_words(@sale.ends_at) %>
</div>
SLAs de Tickets de Support
def sla_status(ticket)
deadline = ticket.created_at + ticket.sla_hours.hours
if deadline.future?
"Réponse attendue #{relative_time_in_words(deadline)}"
else
"SLA non respecté #{relative_time_in_words(deadline)}"
end
end
Horaires d’Événements
<% if @event.starts_at.future? %>
Commence <%= relative_time_in_words(@event.starts_at) %>
<% else %>
A commencé <%= relative_time_in_words(@event.starts_at) %>
<% end %>
Points d’Attention
Surcharge de la File de Jobs
Chaque compte à rebours actif génère des jobs récurrents. Pour les pages à fort trafic, considérez :
- Regrouper les mises à jour pour plusieurs enchères
- Utiliser Action Cable pour des mises à jour vraiment en temps réel
- Limiter le nombre de jobs de compte à rebours concurrents
Fuseaux Horaires
relative_time_in_words utilise Time.current, qui respecte Time.zone. Assurez-vous que la gestion des fuseaux horaires de votre application est cohérente.
Dégradation Gracieuse
Le compte à rebours fonctionne sans JavaScript—les utilisateurs voient simplement un temps statique qui était précis au chargement de la page. Turbo Streams ajoute les mises à jour en direct comme une amélioration.
I18n
Le helper utilise les traductions intégrées de Rails. Personnalisez-les dans vos fichiers de locale :
fr:
datetime:
relative:
past: "il y a %{time}"
future: "dans %{time}"
Conclusion
Cette approche vous donne des comptes à rebours en direct avec une complexité minimale. Le serveur est la source de vérité pour le temps, éliminant les problèmes de décalage d’horloge côté client. Et comme c’est juste du HTML diffusé en streaming, ça fonctionne partout où Turbo fonctionne.
Pour une précision vraiment en temps réel (seconde par seconde), vous auriez toujours besoin de JavaScript. Mais pour la plupart des cas d’utilisation—enchères, ventes, SLAs, événements—des mises à jour à la minute suffisent, et cette approche côté serveur est plus simple à construire et maintenir.
Consultez le PR #55405 pour l’implémentation de relative_time_in_words.