Erstellen Sie Widgets in Mendix Mit React Teil 5 — Ausführen von WebAssembly in Mendix | Mendix

Direkt zum Inhalt

Erstellen Sie Widgets in Mendix Mit React Teil 5 — Ausführen von WebAssembly in Mendix

Mendix ermöglicht es Ihnen, benutzerdefinierte React-Widgets zu erstellen und sie in Ihrer App zu verwenden, wodurch Sie das Frontend Ihrer Anwendung beliebig erweitern können. Aber wussten Sie, dass Sie Ihr Frontend auch mit C, C++ und Rust erweitern können?

Dies ist Blog 5 einer mehrteiligen Serie. Die vorherigen Blogs finden Sie hier:

Was wir bauen

Das Spiel des Lebens! Nein, nicht das klassisches Brettspiel… sondern ein Einzelspielerspiel zur zellularen Automatisierung. Mit ein paar einfachen Regeln können Sie Baue ein Universum aus lebenden und sterbenden Zellen Erstellen cooler, sich wiederholender Muster.

Bevor wir anfangen – Was zum Teufel‽

Wir werden also ein Spiel entwickeln, bei dem wir in Rust geschriebenen und in WebAssembly kompilierten Code nutzen.

Erstellen von Widgets in Mendix Mit React Teil 5_Web Assembly

Aber was ist WebAssembly? Nun, es ist eine kleine, schnelle, effiziente stapelbasierte virtuelle Maschine, mit der Sie Bytecode ausführen können, der aus in C, C++, Rust, Python oder anderen Sprachen geschriebenem Code kompiliert wurde.

Aber was bedeutet das eigentlich?

WebAssembly wird im Wesentlichen in ein Binärformat kompiliert, das kleiner zu laden und blitzschnell auszuführen ist. Sie können auch Bibliotheken aus gängigen Programmiersprachen nutzen und vorhandenen Code so konvertieren, dass er in Ihrem Browser funktioniert. wie Doom 3.

Erstellen von Widgets in Mendix Mit React Teil 5_Doom 3
Doom 3

Das ist also cool für Spiele, aber nutzt das tatsächlich jemand?

Die kurze Antwort lautet: Ja. Große Web-Apps wie Figma und Zoom werden größtenteils mit WebAssembly geschrieben. Darüber hinaus nutzen eine ganze Reihe nützlicher npm-Pakete .wasm-Dateien (kompilierte WebAssembly), einschließlich der ArcGis-Bibliothek, die wir im letzten Blog verwendet haben.

Erste Schritte mit WebAssembly

Es gibt viele verschiedene Möglichkeiten, WebAssembly-Dateien zu erstellen, einschließlich der Kompilierung von Code mit Anmelden. Für dieses Beispiel verwenden wir Code, den wir in Rust geschrieben haben.

Um das Game Of Life in WebAssembly zu schreiben und als Node-Paket zu veröffentlichen, können wir es problemlos in unserem Widget verwenden. Ich folgte diesem Tutorial. Das Tutorial ist eine hervorragende Ressource für den Einstieg in WebAssembly und wenn Sie sich mit Rust und WebAssembly vertraut machen möchten, kann ich es wärmstens empfehlen.

Die Ausgabe dieses Tutorials ist eine Knotenbibliothek, die aus einer Wasm-Datei besteht, die unseren kompilierten Rust-Code enthält, einer JavaScript-Datei, die unsere Schnittstelle mit unserer Wasm-Datei definiert, und einer Typendatei für die Verwendung unseres Codes mit Typescript.

Sobald wir unsere npm-Bibliothek haben, können wir unseren Code ausführen in Mendix mithilfe eines Pluggable Widgets.

Widget Zeit

Wir beginnen mit dem Gerüst unseres Widgets yo @mendix/widget gameOfLife und benennen unsere Komponenten um. Anschließend importieren wir unsere WebAssembly npm-Bibliothek npm i wasm-game-of-life-joerob319 (oder Ihre selbst erstellte Bibliothek).

Von unserer untergeordneten Komponente aus können wir beginnen, unsere Wasm-Datei zu verwenden. Dazu müssen wir unsere Datei importieren und die Typen verwenden, die in der npm-Bibliothek bereitgestellt werden:

import * as wasm from "wasm-game-of-life-joerob319";
import { Universe, Cell, wasm_memory } from "wasm-game-of-life-joerob319";

Als Nächstes laden wir unsere Datei und erstellen unser Zellenuniversum, wenn unsere Komponente zunächst in useEffect gerendert wird.

const initiateWasm = async () => {
  wasm.default().then(() => {
    setUniverse(wasm.Universe.new());
  });
};
useEffect(() => {
  initiateWasm();
}, []);

Wiedergabe

Unsere WebAssembly-Datei ist jetzt geladen. Wir müssen uns überlegen, wie wir unser Universum auf unserer Seite anzeigen.

Als erstes müssen wir berechnen, wie groß unser Universum in Pixeln sein wird. Glücklicherweise können wir in unserem WebAssembly anhand der Anzahl der Zellen auf die Breite und Höhe unseres Universums zugreifen und sie dann in einem Zustand wie diesem speichern:

const [width, setWidth] = useState<number>();
const [height, setHeight] = useState<number>();
useEffect(() => {
    if (universe) {
        setWidth(universe!.width());
        setHeight(universe!.height());
    }
}, [universe]);</number></number>

Wir wollen unser Universum auf unserem Bildschirm darstellen. Dazu nutzen wir Eine Leinwand – ein leistungsstarkes Tool, mit dem Sie Javascript zum Zeichnen verwenden können.

return (
    <div>
        <canvas ref="{canvasRef}"></canvas>
    </div>
);

Anschließend aktualisieren wir unseren useEffect, um die Höhe und Breite der Leinwand festzulegen. Dazu müssen wir unsere Leinwandreferenz verwenden und eine CellSize definieren.

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]);

Zeit, ein paar schöne Bilder zu zeichnen! Beginnen wir mit unserem Raster:

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();
    };

Der nächste Schritt besteht darin, die Zellen herauszuzeichnen. In unserer WebAssembly-Datei werden die Zellen entweder als lebendig oder tot gespeichert. Um sie in unserem Widget anzuzeigen, benötigen wir Zugriff auf den Wasm-Speicher.

Wichtig zu wissen ist, dass der Wasm-Speicher linear ist. Das bedeutet, dass wir in JavaScript im Wesentlichen als Byte-Array (vorzeichenlose 8-Bit-Ganzzahl) darauf zugreifen können.

Wir erstellen also ein Array aus dem Speicherpuffer, beginnend an dem Punkt, an dem sich laut Wasm die aktuellen Zellen befinden, und nehmen die Länge aller Zellen im Universum, die gleich der Anzahl der Zellen in der Höhe mal der Anzahl der Zellen in der Breite ist.

        const cellsPtr = universe!.cells();
        const memory = wasm_memory();
        const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);

Dies kann dann wie folgt auf unsere Leinwand übertragen werden:

    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();
    };

Wir verwenden unseren getIndex, um unsere Zeile und Spalte in einen Speicherort in unserem 1D-Array zu konvertieren, das aus unserem Wasm-Speicher erstellt wurde. Füllen Sie dann jedes Quadrat mit der entsprechenden Farbe aus, je nachdem, ob es lebendig oder tot ist.

Animieren

Jetzt können wir mit der Animation unseres Widgets beginnen. Wir möchten, dass der Benutzer die Animation anhält und startet.

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>
    );

Zur Animation erstellen wir eine Renderschleife, die die requestAnimationFrame() um eine Schleife zu erstellen, die unser Raster animiert. Wir verwenden die universum.tick() Funktion zum Neuberechnen aller Zellen und Zeichnen der Leinwand.

let animationId: number | null = null;
    const renderLoop = () => {
        universe!.tick();
        drawGrid();
        drawCells();
        animationId = requestAnimationFrame(renderLoop);
    };

Wir rufen dies dann mit useLayoutEffect damit es aktualisiert wird bevor der Browser den Bildschirm malt.

    useLayoutEffect(() => {
        if (!isPaused) {
            renderLoop();
            return () => cancelAnimationFrame(animationId!);
        } else {
            if (animationId) {
                cancelAnimationFrame(animationId!);
            }
        }
    }, [isPaused]);

Wir schließen dann unsere Animationsrahmen abbrechen in unserer Bereinigung, um Nebenwirkungen zu vermeiden. Wir führen unsere Anwendung aus und klicken auf die Schaltfläche … Fehler!

Rollup-Zusammenfassung

Dies liegt daran, dass wir die Wasm-Datei nicht tatsächlich in unseren Browser geladen haben, um sie auszuführen. Dafür wir brauchen unseren alten Freund Rollup.

Wir rennen npm i rollup-plugin-copy-save um unsere Rollup-Datei in unserem Stammverzeichnis zu erstellen:

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;
    });
};

Das Kopier-Plugin verschiebt unsere Wasm-Datei, um sicherzustellen, dass sie Teil unseres Widget-Pakets ist und dem Browser bereitgestellt wird.

Wenn wir unser Widget ausführen, haben wir unser Spiel des Lebens.

Es sind nur noch zwei Kleinigkeiten zu erledigen …

Töten oder beleben Sie Ihre Zellen

Unser Game of Life ist im Moment also cool, aber der Benutzer hat keine Möglichkeit, den Zustand des Universums zu bestimmen. Machen wir es so, dass der Benutzer per Klick zwischen lebendigen und toten Zellen umschalten kann.

Dazu erstellen wir eine Funktion namens addPointClick, dann berechnen Sie, welche Zelle angeklickt wurde, indem Sie die obere linke Position der Leinwand und die Klickposition ermitteln und sicherstellen, dass Sie mit jedem auf die Leinwand angewendeten Maßstab multiplizieren. Sobald wir die Zeile und Spalte haben, können wir die Zelle umschalten Funktion in unserer Webassembly-Datei, um das Universum zu aktualisieren und dann die Leinwand neu zu zeichnen.

    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();
        }
    };

Denken Sie daran, es auch zu Ihrer Leinwand hinzuzufügen:

<canvas ref={canvasRef} onClick={addPointClick}/>

Und voilà ... es lebt!

Erstellen von Widgets in Mendix Mit React Teil 5_It's Alive Meme

Erstellen von Widgets in Mendix Mit React Teil 5_Mobilfunkautomatisierung Zuletzt

Aufräumen

Zum Abschluss entfernen wir die Variablen CellSize, GridColour, DeadColour und AliveColour aus unserer Datei und erlauben unserem Widget-Benutzer, sie festzulegen. Aktualisieren Sie die XML wie folgt:

<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>

Ihr endgültiges übergeordnetes Element sollte ungefähr so ​​aussehen:

export function GameOfLife({ CellSize, GridColour, DeadColour, AliveColour }: GameOfLifeContainerProps): ReactElement {
    return (
        <GameOfLifeComponent
            CellSize={CellSize}
            GridColour={GridColour}
            DeadColour={DeadColour}
            AliveColour={AliveColour}
        />
    );
}

Und aktualisieren Sie das untergeordnete Element, um sie als Parameter zu akzeptieren (vergessen Sie nicht, die vorhandenen Variablen aus Ihrem Code zu entfernen!).

export interface GameOfLifeComponentProps {
    CellSize: number;
    GridColour: string;
    DeadColour: string;
    AliveColour: string;
}

export function GameOfLifeComponent({
    CellSize,
    GridColour,
    DeadColour,
    AliveColour
}: GameOfLifeComponentProps): ReactElement {

…..
}

Damit sind wir fertig! Wir haben gesehen, wie Sie Code in C, C++, Rust oder Python schreiben und in Ihrem Mendix app. Wie immer finden Sie den vollständigen Code für das Widget auf Github: GitHub – joe-robertson-mx/gameOfLife.

Das Ende … na ja, nicht wirklich

Das ist das Ende dieser Serie über Pluggable Widgets! Wir haben uns von der Erstellung eines einfachen Zähler-Widgets bis hin zur Ausführung von WebAssembly in unserem Mendix App.

Das heißt aber nicht, dass das das Ende ist … in den nächsten Wochen und Monaten werden noch mehr tolle Widget-Inhalte auf Sie zukommen. Wenn Sie etwas haben, was Sie gerne sehen würden, oder Feedback zu dieser Serie haben, hinterlassen Sie einfach einen Kommentar zu dieser Geschichte.

Wählen Sie Ihre Sprache