Oscilloscope (JavaScript local)¶
Objectif : construire un outil temps réel avec du JavaScript local, sans CDN, sous CSP stricte.
Ce que vous allez apprendre : servir un module JavaScript depuis static/js/, le charger via {% block scripts %}, et lui passer des données par des attributs data-* plutôt que par un script inline.
L'oscilloscope dessine une sinusoïde animée sur un canvas, pilotée par deux curseurs (fréquence, amplitude).
C'est un cas typique de la famille temps réel : le SSR seul ne suffit pas, un peu de JavaScript est légitime.
La règle reste stricte : le JavaScript est local, servi depuis 'self', jamais depuis un CDN.
Documentations
Pour bien comprendre ce palier :
| Document | Ce qu'il apporte |
|---|---|
| Outils interactifs | le motif JavaScript-live et ses contraintes |
| Front et CSS | le dossier static/js/ et le bloc {% block scripts %} |
| Le nonce CSP | pourquoi un fichier .js externe passe sous script-src 'self' sans nonce |
Pourquoi aucun nonce n'est nécessaire
La CSP de Forge est script-src 'self' par défaut.
Un fichier .js externe servi depuis static/js/ est de même origine : 'self' l'autorise déjà.
Le nonce (APP_CSP_NONCE_ENABLED) ne sert qu'aux scripts inline (<script>…</script> dans la page).
Ce motif n'en écrit aucun : toute la logique est dans le fichier externe, les données passent par des attributs data-*.
Vous n'activez donc pas le nonce pour cet outil.
Contrôleur
Le contrôleur ne fait que rendre la page, avec les valeurs initiales des curseurs.
Aucun POST, aucune session, aucun état serveur : le temps réel se joue entièrement côté navigateur.
Ajoutez la méthode à mvc/controllers/sandbox_controller.py :
@staticmethod
def oscilloscope(request: Request) -> Response:
return BaseController.render(
"sandbox/oscilloscope.html",
request=request,
context={"frequence": 2, "amplitude": 60},
)
Les valeurs frequence et amplitude sont de simples entiers passés à la vue, qui les posera dans le DOM.
Route
Ajoutez la route dans le groupe public de mvc/routes/__init__.py :
with router.group("", public=True) as public:
# ... routes existantes ...
public.add("GET", "/sandbox/oscilloscope", SandboxController.oscilloscope, name="sandbox-oscilloscope")
Une seule route GET : l'outil n'envoie rien au serveur.
Vue
Créez mvc/views/sandbox/oscilloscope.html.
Les données du serveur sont posées dans des attributs data-*, jamais dans un script inline :
{% extends "layouts/public.html" %}
{% block title %}Oscilloscope{% endblock %}
{% block content %}
<h1>Oscilloscope</h1>
<div id="oscilloscope"
data-frequence="{{ frequence }}"
data-amplitude="{{ amplitude }}">
<label>Fréquence : <input type="range" name="frequence" min="1" max="10" value="{{ frequence }}"></label>
<label>Amplitude : <input type="range" name="amplitude" min="10" max="90" value="{{ amplitude }}"></label>
<canvas width="600" height="200"></canvas>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/oscilloscope.js" defer></script>
{% endblock %}
Le bloc scripts charge le module local.
Le fichier est servi depuis static/, donc autorisé par script-src 'self'.
Module JavaScript local
Créez static/js/oscilloscope.js.
Le module lit ses paramètres dans le DOM (dataset, valeurs des curseurs) et dessine sur le canvas.
Aucune donnée n'est injectée en HTML brut, aucun appel réseau externe :
// static/js/oscilloscope.js
const racine = document.getElementById("oscilloscope");
const canvas = racine.querySelector("canvas");
const ctx = canvas.getContext("2d");
const curseurFrequence = racine.querySelector('input[name="frequence"]');
const curseurAmplitude = racine.querySelector('input[name="amplitude"]');
// Valeurs initiales lues depuis les attributs data-* posés par le serveur.
curseurFrequence.value = racine.dataset.frequence;
curseurAmplitude.value = racine.dataset.amplitude;
let phase = 0;
function dessiner() {
const frequence = Number(curseurFrequence.value);
const amplitude = Number(curseurAmplitude.value);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
for (let x = 0; x < canvas.width; x++) {
const y = canvas.height / 2 + amplitude * Math.sin((x / 40) * frequence + phase);
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
phase += 0.05;
requestAnimationFrame(dessiner);
}
requestAnimationFrame(dessiner);
Toute la logique tient dans ce fichier local : le HTML reste statique, le serveur ne fournit que les valeurs initiales.
Tests
| Action | Résultat |
|---|---|
GET https://localhost:8000/sandbox/oscilloscope |
une sinusoïde animée sur le canvas |
| Déplacer le curseur Fréquence | l'onde se resserre ou s'étale en direct |
| Déplacer le curseur Amplitude | l'onde grandit ou s'aplatit en direct |
| Ouvrir la console du navigateur | aucune violation CSP, aucun script bloqué |
Si la console signale une violation script-src, c'est que le script est resté inline ou vient d'un CDN : ramenez-le dans static/js/.
À retenir
- Le JavaScript temps réel est local, servi depuis
static/js/, jamais depuis un CDN. - Un fichier
.jsexterne passe sousscript-src 'self'sans nonce. - Les données du serveur passent par des attributs
data-*, pas par un script inline. - Le serveur ne porte que les valeurs initiales : le temps réel vit côté navigateur.