Précédent: 2. Refactoriser, comprendre
C’est ici qu’il va falloir vraiment commencer un peu à réfléchir et à se constituer un bon modèle mental de ce qui se passe. Jusqu’à présent, on était dans du javascript un peu épicé par le typage, mais sans grande difficulté. Puis est venue la simple question: quelle est la bonne pratique pour séparer mon code en plusieurs fichiers ?
En Javascript “normal”, on peut faire plusieurs fichiers et les
“importer” dans le navigateur avec plusieurs
<script src=...></script>. Toutes les méthodes
vont juste être mises dans le scope global. Est-ce qu’on peut faire ça
en typescript ?
Petite refactorisation du projet actuel: on va partir sur trois
fichiers pour l’instant. drawables.ts contiendra les
définitions de choses à afficher, board.ts le
DrawingBoard, et drawing.ts va s’occuper de
créer le <canvas> et d’instancier tout ce qui doit
l’être. Pour commencer à rendre tout un peu plus propre, je met aussi
les sources dans un dossier src, et je vais mettre tout ce
qui va être exécuter par le browser dans un dossier dist,
pour avoir une structure:
projet/
src/
board.ts
drawables.ts
drawing.ts
dist/
board.js
drawables.js
drawing.js
index.html
Le compilateur Typescript a beaucoup d’options. Dès qu’on commence à
en avoir besoin, il vaut apparemment mieux utiliser un fichier
tsconfig.json qui va être automatiquement chargé par la
commande tsc pour qu’on ne doive pas faire des lignes de
commande à rallonge. On le met à la racine du projet. Bonne nouvelle: on
ne doit pas mettre toutes les options, juste celles qu’on veut changer
de leur valeur par défaut. Commençons donc simplement par fournir les
fichier sources et la destination:
{
"compilerOptions": {
"outDir": "./dist/"
},
"files": ["./src/drawables.ts", "./src/board.ts", "./src/drawing.ts"]
}Et, dans le HTML:
<script src="./drawables.js"></script>
<script src="./board.js"></script>
<script src="./drawing.js"></script>Tout fonctionne toujours… mais ce n’est pas idéal. Tout mettre dans le même scope, et ne pas explicitement indiquer ce qu’on importe, ça va vite rendre les choses compliquées.
Bon, je pense qu’il va falloir se replonger dans le Handbook.
Modules et options de compilation
L’écrasante majorité du Handbook laisse soigneusement de côté la question de “comment on gère un code modulaire”, avant le dernier chapitre: Modules, avec encore de copieuses explications complémentaires dans la référence des modules. Et là, on voit clairement arriver les problèmes liés au fait que le Javascript est devenu un langage un peu fourre-tout, qu’on va parfois vouloir exécuter via un navigateur, parfois via Node. Je vais essayer de résumer ici ce que je comprends jusqu’à présent.
Javascript définit des “modules” comme étant n’importe quel fichier
qui contient des import ou des export. Au
cours du temps et des différentes spécifications Javascript, pour le web
ou pour Node, différentes manières de gérer les modules sont apparues.
Typescript essaie de pouvoir tous les gérer. Le code en lui-même ne doit
pas changer d’un cas à l’autre, mais en contrepartie il faut donner dans
le fichier de configuration tsconfig.json les informations
nécessaires au compilateur pour qu’il sache sur quel pied danser.
On a en particulier l’option module, qui peut prendre tout
un tas de valeurs selon l’environnement d’exécution désiré. Comme on
veut pour l’instant fonctionner dans un navigateur, on va prendre
esnext, qui correspond en gros à “la dernière version de
Javascript pour les navigateurs”.
{
"compilerOptions": {
"module": "esnext",
"outDir": "./dist/"
},
"files": ["./src/drawables.ts", "./src/board.ts", "./src/drawing.ts"]
}On rajoute ensuite des export devant tout ce qu’on veut
rendre disponible pour les autres fichiers, et des import
partout où on veut les utiliser:
// drawables.ts
export interface Drawable {
draw(ctx: CanvasRenderingContext2D): void;
}
export class Rectangle {
constructor(private x: number,
private y: number,
private w: number,
private h: number
){}
public draw(ctx: CanvasRenderingContext2D): void {
ctx.rect(this.x, this.y, this.w, this.h);
ctx.stroke();
}
}
// board.ts
import { Drawable } from "./drawables.js";
export class DrawingBoard {
private drawings: Drawable[];
constructor(private ctx: CanvasRenderingContext2D) {
this.drawings = [];
}
public add(drawing: Drawable){
this.drawings.push(drawing);
}
public draw(): void {
this.drawings.forEach((e) => {
e.draw(this.ctx);
})
}
}
// drawing.ts
import { DrawingBoard } from "./board.js";
import { Rectangle } from "./drawables.js";
function createCanvas(w: number = 500, h: number = 500): HTMLCanvasElement {
const canvas = <HTMLCanvasElement>document.createElement("canvas");
canvas.width = w;
canvas.height = h;
document.body.appendChild(canvas);
return canvas;
}
const canvas = createCanvas();
const board = new DrawingBoard(canvas.getContext("2d")!);
board.add(new Rectangle(100, 150, 200, 50));
board.add(new Rectangle(150, 160, 15, 70));
board.draw();On compile, on teste, et…
Uncaught SyntaxError: export declarations may only appear at top level of a module.
Que se passe-t-il? La référence
Javascript sur Mozilla.org m’informe que, maintenant que j’ai
affaire à des modules et plus à des scripts, je dois utiliser
type="module" quand j’inclus mes fichiers .js
dans le HTML. Bonus: je ne dois plus inclure que
drawing.js. Maintenant que j’utilises les
import, le HTML n’a plus besoin de savoir tout
ce dont drawing.js a besoin.
<script src="./drawing.js" type="module"></script>Cette fois, ça fonctionne.
Pongekke
Avant de continuer, il est temps de se donner un vrai projet. C’est en cherchant à rajouter des choses (et en échouant) que je trouve les limites et les erreurs de mon modèle mental du fonctionnement du système, donc si je reste avec mes deux rectangles, on ne va pas aller très loin.
En dessinant des rectangle sur le <canvas>, ça
m’inspire vaguement à faire un petit jeu type “pong”. Mais sans doute
différent. Je ne sais pas encore trop comment, mais je vais l’appeler
provisoirement “Pongekke”. Je vais donc rapidement modifier un peu le
code, le mettre sur un repository git (https://codeberg.org/adfoucart/pongekke),
et rajouter un peu de CSS dessus pour faire joli.

Et voilà le travail! Il n’y a plus qu’à en faire quelque chose.
À suivre…
- Comment introduire correctement des tests dans tout ça.
- Comment faire une “game loop”: action utilisateur, mise à jour du plateau, affichage…