Créer des widgets dans Mendix Avec React Partie 5 — Exécution de WebAssembly dans Mendix
Mendix vous permet de créer des widgets React personnalisés et de les utiliser dans votre application, étendant ainsi le front-end de votre application comme vous le souhaitez. Mais saviez-vous que vous pouvez également étendre votre front-end avec C, C++ et Rust ?
Il s'agit du 5e article d'une série en plusieurs parties. Les articles précédents peuvent être consultés ici :
- Créez des widgets dans Mendix avec React — Partie 1 — Compteur de couleurs
- Créer des widgets dans Mendix avec React Partie 2 — Minuterie
- Créer des widgets dans Mendix avec React Partie 3 — Kanban
- Créez des widgets dans Mendix avec React Partie 4 — Cartes ArcGIS
Ce que nous construisons
Le jeu de la vie ! Non, pas le jeu de société classique… mais un jeu d’automatisation cellulaire à un joueur. Avec quelques règles simples, vous pouvez construire un univers de cellules vivantes et mourantes créer des motifs sympas et répétitifs.

Avant de commencer — Qu'est-ce que c'est que ce bordel‽
Nous allons donc créer un jeu dans lequel nous allons exploiter du code écrit en Rust et compilé en WebAssembly.

Mais qu'est-ce que WebAssembly ? Il s'agit d'une petite machine virtuelle rapide et efficace basée sur une pile qui vous permet d'exécuter du bytecode compilé à partir de code écrit en C, C++, Rust, Python ou autres.
Mais qu'est-ce que cela signifie réellement ?
En fait, WebAssembly est essentiellement compilé dans un format binaire qui est plus petit à charger et rapide à exécuter. Vous pouvez également exploiter les bibliothèques de langages de programmation populaires et convertir le code existant pour qu'il fonctionne dans votre navigateur, comme Doom 3.

Donc c'est cool pour les jeux, mais est-ce que quelqu'un l'utilise réellement ?
La réponse courte est oui. Les applications Web à grande échelle comme Figma et Zoom sont principalement écrits à l'aide de WebAssembly. Mais au-delà de cela, toute une série de packages npm utiles exploitent les fichiers .wasm (WebAssembly compilé), y compris la bibliothèque ArcGis que nous avons utilisée dans le blog le plus récent.
Premiers pas avec WebAssembly
Il existe de nombreuses façons différentes de créer des fichiers WebAssembly, notamment en compilant du code à l'aide de S'inscrirePour cet exemple, nous utiliserons du code que nous avons écrit en Rust.
Pour écrire le jeu de la vie en WebAssembly et le publier sous forme de package de nœuds, nous pouvons facilement l'utiliser dans notre widget, J'ai suivi ce tutorielLe didacticiel est une excellente ressource pour démarrer avec WebAssembly, et si vous souhaitez vous familiariser avec Rust et WebAssembly, je le recommande vivement.
Le résultat de ce tutoriel est une bibliothèque de nœuds composée d'un fichier Wasm, contenant notre code Rust compilé, un fichier javascript définissant notre interface avec notre fichier Wasm et un fichier de types pour utiliser notre code avec Typescript.
Une fois que nous avons notre bibliothèque npm, nous pouvons exécuter notre code dans Mendix en utilisant un widget enfichable.
Heure du widget
Nous commençons par échafauder notre widget yo @mendix/widget gameOfLife et renommer nos composants. Nous importons ensuite notre bibliothèque npm WebAssembly npm i wasm-game-of-life-joerob319 (ou votre bibliothèque que vous avez créée vous-même).
Depuis notre composant enfant, nous pouvons commencer à utiliser notre fichier Wasm. Pour cela, nous devons importer notre fichier et utiliser les types fournis dans la bibliothèque npm :
import * as wasm from "wasm-game-of-life-joerob319";
import { Universe, Cell, wasm_memory } from "wasm-game-of-life-joerob319";
Ensuite, chargeons notre fichier et créons notre univers de cellules lorsque notre composant s'affiche initialement dans useEffect.
const initiateWasm = async () => {
wasm.default().then(() => {
setUniverse(wasm.Universe.new());
});
};
useEffect(() => {
initiateWasm();
}, []);
Affichage
Notre fichier WebAssembly est maintenant chargé. Nous devons réfléchir à la manière dont nous allons afficher notre univers sur notre page.
La première chose à faire est de calculer la taille de notre univers en pixels. Heureusement, nous pouvons accéder à la largeur et à la hauteur de notre univers en nombre de cellules, à partir de notre WebAssembly, puis les stocker dans un état comme celui-ci :
const [width, setWidth] = useState<number>();
const [height, setHeight] = useState<number>();
useEffect(() => {
if (universe) {
setWidth(universe!.width());
setHeight(universe!.height());
}
}, [universe]);</number></number>
Nous allons vouloir afficher notre univers sur notre écran. Pour cela, nous allons utiliser une toile – un outil puissant qui vous permet d’utiliser Javascript pour dessiner.
return (
<div>
<canvas ref="{canvasRef}"></canvas>
</div>
);
Nous mettons ensuite à jour notre useEffect pour définir la hauteur et la largeur du canvas. Pour ce faire, nous devons utiliser notre référence canvas et définir un CellSize.
const canvasRef = useRef(null)
const CellSize = 5;
useEffect(() => {
if (universe) {
setWidth(universe.width());
setHeight(universe.height());
const canvas = canvasRef.current!;
if (universe.height()) {
canvas.height = universe.height() * (CellSize + 1);
setHeight(universe.height());
}
if (universe.width()) {
canvas.width = universe.width() * (CellSize + 1);
setWidth(universe.width());
}
}
}, [universe]);
Il est temps de dessiner de jolies images ! Commençons par notre grille :
const GridColour = ‘#CCCCCC’
const drawGrid = () => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext("2d")!;
ctx.beginPath();
ctx.strokeStyle = GridColour;
// Vertical lines.
for (let i = 0; i <= width!; i++) {
ctx.moveTo(i * (CellSize + 1) + 1, 0);
ctx.lineTo(i * (CellSize + 1) + 1, (CellSize + 1) * height! + 1);
}
// Horizontal lines.
for (let j = 0; j <= height!; j++) {
ctx.moveTo(0, j * (CellSize + 1) + 1);
ctx.lineTo((CellSize + 1) * width! + 1, j * (CellSize + 1) + 1);
}
ctx.stroke();
};
L'étape suivante consiste à extraire les cellules. Dans notre fichier WebAssembly, les cellules sont stockées comme vivantes ou mortes. Pour les afficher dans notre widget, nous devons accéder à la mémoire Wasm.
Il est important de comprendre qu'une mémoire Wasm est linéaire. Cela signifie que nous pouvons y accéder en JavaScript essentiellement sous la forme d'un tableau d'octets (entier non signé de 8 bits).
Nous créons donc un tableau à partir de la mémoire tampon, en commençant au point où le Wasm nous indique que les cellules actuelles sont situées, et nous prenons la longueur de toutes les cellules de l'univers, qui est la même que le nombre de cellules hautes multiplié par le nombre de cellules larges.
const cellsPtr = universe!.cells();
const memory = wasm_memory();
const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);
Cela peut ensuite être traduit sur notre toile comme ceci :
const getIndex = (row: number, column: number) => {
return row * width! + column;
};
const DeadColour = ‘#FFFFF’
const AliveColour - ‘#3a34eb’
const drawCells = () => {
const cellsPtr = universe!.cells();
const memory = wasm_memory();
const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);
console.log (cellsPtr)
console.log (cells);
const canvas = canvasRef.current!;
const ctx = canvas.getContext("2d")!;
ctx.beginPath();
for (let row = 0; row < height!; row++) {
for (let col = 0; col < width!; col++) {
const idx = getIndex(row, col);
ctx.fillStyle = cells[idx] === Cell.Dead ? DeadColour : AliveColour;
ctx.fillRect(col * (CellSize + 1) + 1, row * (CellSize + 1) + 1, CellSize, CellSize);
}
}
ctx.stroke();
};
Nous utilisons notre getIndex pour convertir notre ligne et notre colonne en un emplacement dans notre tableau 1D créé à partir de notre mémoire Wasm. Remplissez ensuite chaque case avec la couleur appropriée, selon qu'elle est vivante ou morte.
Animation
Nous pouvons maintenant commencer à animer notre widget. Nous voulons permettre à notre utilisateur d'arrêter et de démarrer notre animation.
const [isPaused, setIsPaused] = useState(true);
const btnClick = () => {
setIsPaused(prevState => !prevState);
};
return (
<div>
<canvas ref={canvasRef} />
<div className="btnContainer">
<button onClick={btnClick} className="btn mx-button">
{isPaused ? "▶" : "⏸"}
</button>
</div>
</div>
);
Pour animer, nous allons créer une boucle de rendu qui utilise le requestAnimationFrame() pour créer une boucle pour animer notre grille. Nous utilisons le univers.tick() fonction permettant de recalculer toutes les cellules, puis de dessiner le canevas.
let animationId: number | null = null;
const renderLoop = () => {
universe!.tick();
drawGrid();
drawCells();
animationId = requestAnimationFrame(renderLoop);
};
Nous appelons ensuite cela en utilisant utiliserLayoutEffect pour qu'il soit mis à jour avant que le navigateur ne peigne l'écran.
useLayoutEffect(() => {
if (!isPaused) {
renderLoop();
return () => cancelAnimationFrame(animationId!);
} else {
if (animationId) {
cancelAnimationFrame(animationId!);
}
}
}, [isPaused]);
Nous incluons ensuite notre annulerAnimationFrame dans notre nettoyage pour éviter tout effet secondaire. Nous exécutons notre application et cliquons sur le bouton… erreurs !
Tour d'horizon des cumuls
C'est parce que nous n'avons pas réellement chargé le fichier Wasm dans notre navigateur pour l'exécuter. Pour cela, nous avons besoin de notre vieil ami Rollup.
Nous courrons npm i rollup-plugin-copy-save pour créer notre fichier rollup dans notre répertoire racine :
import copy from "rollup-plugin-copy";
export default args => {
const result = args.configDefaultConfig;
return result.map((config, index) => {
if (index === 0) {
const plugins = config.plugins || []
config.plugins = [
...plugins,
copy({
targets: [{ src: "node_modules/wasm-game-of-life-joerob319/*.wasm", dest: "dist/tmp/widgets/mendix/gameofLife/" }]
}) ]
}
return config;
});
};
Le plugin de copie déplace notre fichier Wasm pour garantir qu'il fait partie de notre package de widgets et qu'il sera servi au navigateur.
Si nous exécutons notre widget, nous avons notre jeu de vie.
Il ne reste plus que deux petites choses à faire…
Tuez ou réanimez vos cellules
Notre jeu de la vie est cool pour le moment, mais l'utilisateur ne peut pas définir l'état de l'univers. Faisons en sorte que l'utilisateur puisse cliquer pour activer ou désactiver les cellules.
Pour ce faire, nous créons une fonction appelée ajouterPointClick, puis calculez quelle cellule a été cliquée en trouvant la position en haut à gauche du canevas et la position du clic, en veillant à multiplier par toute échelle appliquée au canevas. Une fois que nous avons la ligne et la colonne, nous pouvons utiliser le basculer_cellule fonction dans notre fichier webassembly pour mettre à jour l'univers puis redessiner le canevas.
const addPointClick = (event: React.MouseEvent): void => {
event.preventDefault();
if (canvasRef.current!) {
const node = canvasRef.current;
const boundingRect = node.getBoundingClientRect();
const scaleX = node.width! / boundingRect.width;
const scaleY = node.height! / boundingRect.height!;
const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
const canvasTop = (event.clientY - boundingRect.top) * scaleY;
const row = Math.min(Math.floor(canvasTop / (CellSize + 1)), height! - 1);
const col = Math.min(Math.floor(canvasLeft / (CellSize + 1)), width! - 1);
universe!.toggle_cell(row, col);
drawGrid();
drawCells();
}
};
Assurez-vous de l'ajouter également à votre toile :
<canvas ref={canvasRef} onClick={addPointClick}/>
Et voilà… c’est vivant !


Faire le ménage
Terminons en supprimant nos variables de notre fichier CellSize, GridColour, DeadColour, AliveColour, et permettons à notre utilisateur de widget de les définir. Mettez à jour le xml comme suit :
<propertyGroup caption="General">
<property key="CellSize" type="integer" required="true" defaultValue="5">
<caption>Cell Size</caption>
<description>Size of each square in px</description>
</property>
<property key="GridColour" type="string" required="true" defaultValue="#CCCCCC">
<caption>Grid Colour</caption>
<description>Colour of Grid</description>
</property>
<property key="DeadColour" type="string" required="true" defaultValue="#FFFFFF">
<caption>Dead Colour</caption>
<description>Colour of Dead Cells</description>
</property>
<property key="AliveColour" type="string" required="true" defaultValue="#000000">
<caption>Alive Colour</caption>
<description>Colour of Alive Cells</description>
</property>
</propertyGroup>
Votre parent final devrait ressembler à ceci :
export function GameOfLife({ CellSize, GridColour, DeadColour, AliveColour }: GameOfLifeContainerProps): ReactElement {
return (
<GameOfLifeComponent
CellSize={CellSize}
GridColour={GridColour}
DeadColour={DeadColour}
AliveColour={AliveColour}
/>
);
}
Et mettez à jour l'enfant pour les accepter comme paramètre (sans oublier de supprimer les variables existantes de votre code !).
export interface GameOfLifeComponentProps {
CellSize: number;
GridColour: string;
DeadColour: string;
AliveColour: string;
}
export function GameOfLifeComponent({
CellSize,
GridColour,
DeadColour,
AliveColour
}: GameOfLifeComponentProps): ReactElement {
…..
}
Nous avons donc terminé ! Nous avons vu comment vous pouvez écrire du code en C, C++, Rust ou Python et l'exécuter dans votre Mendix app. Comme toujours, vous pouvez trouver le code complet du widget sur Github : GitHub – joe-robertson-mx/jeuDeLaVie.
La fin… enfin, pas vraiment
C'est la fin de cette série sur les widgets enfichables ! Nous sommes passés de la création d'un simple widget de compteur à l'exécution de WebAssembly dans notre Mendix app.
Cela ne veut pas dire que c'est la fin… il y aura encore plus de contenu intéressant à découvrir dans les semaines et les mois à venir. Si vous avez quelque chose que vous aimeriez voir ou des commentaires sur cette série, laissez simplement un commentaire sur cet article.