#gowm#WebAssembly#React

WebAssembly + Go + React : un exemple de validation de formulaire

Exemple de validation partagée entre Go compilé en WebAssembly et React, comparée à une validation JavaScript classique.

10 min de lecture
Résumé IA

Générez un résumé de cet article en un clic.

La validation d'un formulaire n'est généralement pas assez coûteuse pour justifier WebAssembly. Elle devient intéressante quand les règles sont partagées avec un autre service ou quand le calcul dépasse quelques expressions régulières. Cet article montre cette intégration avec Go, React et GoWM, sans prétendre que WASM est toujours le meilleur choix.

L'exemple utilise GoWM pour charger le module et garder une API similaire côté navigateur et côté Node.js. Le contrat de données entre le formulaire et le module compte davantage qu'une promesse de gain automatique de performance.

GoWM, qu'est-ce que c'est que ce truc ?

GoWM (Go Wasm Manager) fournit le chargement et l'appel de modules WASM Go depuis JavaScript. Il ne remplace pas le compilateur : il évite surtout de répéter le même code d'initialisation dans chaque application.

GoWM apporte :

  • Une interface unifiée pour charger tes modules WASM Go.

  • Un support pour Node.js et navigateur avec détection automatique.

  • Des hooks React intégrés (useWasm, useWasmFromNPM).

  • Des composables Vue.js pour Vue 3.

  • Un chargement depuis NPM avec résolution automatique.

  • Une gestion des erreurs et la détection de fonctions.

  • Une gestion automatique de la mémoire.

Quand WebAssembly est utile pour un formulaire

La plupart des formulaires n'ont pas besoin de WebAssembly. Le choix devient intéressant si les règles sont partagées avec un service Go, si le calcul est coûteux ou si tu veux exécuter la même logique dans le navigateur et sur le serveur.

WebAssembly permet alors d'exécuter le module Go dans le navigateur, avec un contrat d'entrée et de sortie stable. Le gain doit se mesurer sur ton cas réel : le chargement du module et les conversions de données ont aussi un coût.

Allez, on installe tout ça !

Bon, assez parlé, on passe aux choses sérieuses :

npm install gowm # ou si tu es team Yarn yarn add gowm

Facile, hein ?

Création de notre module WASM de validation

Structure du projet

D'abord, organisons-nous correctement :

validator-wasm/ ├── main.go # Notre code Go de la mort ├── go.mod # Configuration Go ├── build.sh # Script magique de compilation ├── package.json # Config NPM └── README.md # Documentation (sois pas fainéant !)

Le code Go du Wasm

Allez, on s'attaque au vif du sujet. Voici notre validateur Go qui sera builder en wasm :

//go:build js && wasm package main import ( "encoding/json" "fmt" "regexp" "strings" "syscall/js" ) type ValidationRule struct { Field string `json:"field"` Required bool `json:"required"` MinLen int `json:"minLen"` MaxLen int `json:"maxLen"` Pattern string `json:"pattern"` Type string `json:"type"` } type ValidationResult struct { Valid bool `json:"valid"` Errors map[string]string `json:"errors"` } // validateEmail - parce qu'on en a tous marre des emails pourris func validateEmail(email string) bool { pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` matched, _ := regexp.MatchString(pattern, email) return matched } // validatePhone - pour les numéros français func validatePhone(phone string) bool { pattern := `^(?:\+33|0)[1-9](?:[0-9]{8})$` matched, _ := regexp.MatchString(pattern, phone) return matched } // validateForm - la fonction qui fait tout le boulot func validateForm(this js.Value, args []js.Value) interface{} { if len(args) != 2 { return js.ValueOf(map[string]interface{}{ "valid": false, "errors": map[string]string{"system": "Arguments invalides, mon reuf !"}, }) } // Parse des données (attention, ça peut piquer) formDataJSON := args[0].String() rulesJSON := args[1].String() var formData map[string]interface{} var rules []ValidationRule if err := json.Unmarshal([]byte(formDataJSON), &formData); err != nil { return js.ValueOf(map[string]interface{}{ "valid": false, "errors": map[string]string{"system": "Données formulaire pourries"}, }) } if err := json.Unmarshal([]byte(rulesJSON), &rules); err != nil { return js.ValueOf(map[string]interface{}{ "valid": false, "errors": map[string]string{"system": "Règles de validation foireuses"}, }) } result := ValidationResult{ Valid: true, Errors: make(map[string]string), } // Le module est appelé ici for _, rule := range rules { value, exists := formData[rule.Field] valueStr := "" if exists && value != nil { valueStr = strings.TrimSpace(value.(string)) } // Champ requis ? On vérifie ! if rule.Required && valueStr == "" { result.Valid = false result.Errors[rule.Field] = "Ce champ est requis, fais un effort !" continue } if valueStr == "" { continue // On skip si pas requis et vide } // Longueur minimale if rule.MinLen > 0 && len(valueStr) < rule.MinLen { result.Valid = false result.Errors[rule.Field] = fmt.Sprintf("Minimum %d caractères requis", rule.MinLen) continue } // Longueur maximale if rule.MaxLen > 0 && len(valueStr) > rule.MaxLen { result.Valid = false result.Errors[rule.Field] = fmt.Sprintf("Maximum %d caractères autorisés", rule.MaxLen) continue } // Validation par type switch rule.Type { case "email": if !validateEmail(valueStr) { result.Valid = false result.Errors[rule.Field] = "Merci d'entrer une adresse email valide" } case "phone": if !validatePhone(valueStr) { result.Valid = false result.Errors[rule.Field] = "Ce numéro de téléphone, c'est n'importe quoi" } } // Pattern personnalisé pour les perfectionnistes if rule.Pattern != "" { matched, err := regexp.MatchString(rule.Pattern, valueStr) if err != nil || !matched { result.Valid = false result.Errors[rule.Field] = "Format invalide, recommence !" } } } // On retourne le tout en JSON resultJSON, _ := json.Marshal(result) return js.ValueOf(string(resultJSON)) } func main() { // On expose notre fonction au monde JavaScript js.Global().Set("validateForm", js.FuncOf(validateForm)) // Signal de prêt pour GoWM js.Global().Set("__gowm_ready", js.ValueOf(true)) // On maintient le programme en vie (important !) select {} }

Script de compilation en Wasm

Créons notre script build.sh qui va compiler tout ça proprement :

#!/bin/bash set -e echo "🔨 Compilation du module de validation WASM..." # Compilation optimisée (on veut du perf !) GOOS=js GOARCH=wasm go build \ -ldflags="-s -w" \ -o validator.wasm \ main.go echo "📦 Copie du runtime Go..." cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" . echo "✅ Build terminé !" echo "📊 Taille du fichier WASM: $(du -h validator.wasm | cut -f1)"

Intégration React avec GoWM

Maintenant, la partie vraiment fun : intégrer tout ça dans React ! Voici un composant :

import React, { useState, useEffect } from 'react'; import { load } from 'gowm'; const ContactForm = () => { const [wasm, setWasm] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const loadWasm = async () => { try { setLoading(true); setError(null); const wasmModule = await load('/validator.wasm'); setWasm(wasmModule); setLoading(false); } catch (err) { setError(err); setLoading(false); } }; loadWasm(); }, []); const [formData, setFormData] = useState({ name: '', email: '', phone: '', message: '' }); const [validationErrors, setValidationErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); // Nos règles de validation (modulables à souhait) const validationRules = [ { field: 'name', required: true, minLen: 2, maxLen: 50 }, { field: 'email', required: true, type: 'email' }, { field: 'phone', required: false, type: 'phone' }, { field: 'message', required: true, minLen: 10, maxLen: 500 } ]; const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // Validation en temps réel (parce qu'on est des pros) if (wasm) { validateField(name, value); } }; const validateField = async (fieldName, value) => { if (!wasm) { // Validation de fallback en JavaScript si WASM pas encore chargé const errors = {}; const rules = validationRules.filter(rule => rule.field === fieldName); for (const rule of rules) { if (rule.required && !value.trim()) { errors[fieldName] = "Ce champ est requis, fais un effort !"; break; } if (value.trim() && rule.minLen && value.length < rule.minLen) { errors[fieldName] = `Minimum ${rule.minLen} caractères requis`; break; } if (value.trim() && rule.type === 'email') { const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(value)) { errors[fieldName] = "Cet email sent le poisson pourri"; break; } } } setValidationErrors(prev => ({ ...prev, [fieldName]: errors[fieldName] || null })); return; } try { const singleFieldData = { [fieldName]: value }; const fieldRules = validationRules.filter(rule => rule.field === fieldName); // Avec gowm, les fonctions sont disponibles globalement après le chargement const result = window.validateForm( JSON.stringify(singleFieldData), JSON.stringify(fieldRules) ); const validation = JSON.parse(result); setValidationErrors(prev => ({ ...prev, [fieldName]: validation.errors[fieldName] || null })); } catch (err) { console.error('Oups, erreur de validation:', err); } }; const handleSubmit = async (e) => { e.preventDefault(); if (!wasm) { alert('Patience, le validateur charge encore !'); return; } setIsSubmitting(true); try { // Validation complète (le moment de vérité) const result = window.validateForm( JSON.stringify(formData), JSON.stringify(validationRules) ); const validation = JSON.parse(result); if (validation.valid) { alert('Formulaire nickel ! Envoi en cours...'); // Ici tu peux envoyer tes données console.log('Données prêtes à partir:', formData); } else { setValidationErrors(validation.errors); } } catch (err) { console.error('Erreur lors de la validation:', err); alert('Oups, quelque chose a foiré !'); } finally { setIsSubmitting(false); } }; if (loading) { return ( <div className="flex items-center justify-center p-8"> <div className="text-center"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div> <p className="mt-2 text-gray-600">Chargement du validateur...</p> </div> </div> ); } if (error) { return ( <div className="bg-red-50 border border-red-200 rounded-lg p-4"> <p className="text-red-600">Erreur de chargement: {error.message}</p> </div> ); } return ( <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6"> <h2 className="text-2xl font-bold mb-6 text-gray-800">Contact</h2> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Nom * </label> <input type="text" name="name" value={formData.name} onChange={handleInputChange} className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ validationErrors.name ? 'border-red-500' : 'border-gray-300' }`} /> {validationErrors.name && ( <p className="text-red-500 text-sm mt-1">{validationErrors.name}</p> )} </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Email * </label> <input type="email" name="email" value={formData.email} onChange={handleInputChange} className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ validationErrors.email ? 'border-red-500' : 'border-gray-300' }`} /> {validationErrors.email && ( <p className="text-red-500 text-sm mt-1">{validationErrors.email}</p> )} </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Téléphone </label> <input type="tel" name="phone" value={formData.phone} onChange={handleInputChange} className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ validationErrors.phone ? 'border-red-500' : 'border-gray-300' }`} /> {validationErrors.phone && ( <p className="text-red-500 text-sm mt-1">{validationErrors.phone}</p> )} </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Message * </label> <textarea name="message" rows={4} value={formData.message} onChange={handleInputChange} className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ validationErrors.message ? 'border-red-500' : 'border-gray-300' }`} /> {validationErrors.message && ( <p className="text-red-500 text-sm mt-1">{validationErrors.message}</p> )} </div> <button type="submit" disabled={isSubmitting} className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" > {isSubmitting ? 'Validation en cours...' : 'Envoyer'} </button> </form> </div> ); }; export default ContactForm;

Pourquoi cette approche ?

Performances.

Avec WebAssembly, ton code de validation tourne à la vitesse de l'éclair. Les regex complexes et la logique métier sont traitées de manière tres-efficace. Fini les freeze d'interface !

Validation en temps réel sans lag

Grâce au hook useWasm de GoWM, tu peux valider tes champs en temps réel sans que ton interface rame.

Réutilisabilité au max

Ton module WASM, tu peux :

  • Le publier sur NPM pour le réutiliser partout

  • L'utiliser côté serveur avec Node.js (pratique !)

  • L'intégrer dans des apps Vue.js avec les composables GoWM

Sécurité renforcée

La validation côté client en WASM ajoute une couche de sécurité. Bien sûr, tu gardes ta validation côté serveur (on n'est pas fous !).

Configuration avancée avec GoWM

GoWM, c'est pas que basique. Tu peux faire d’autres trucs sympa :

// Chargement simple import { load } from 'gowm'; async function loadValidator() { try { const validator = await load('/validator.wasm'); // Utilisation directe des fonctions exposées globalement const result = window.validateForm(dataJSON, rulesJSON); return JSON.parse(result); } catch (error) { console.error('Oops, chargement foiré:', error); } } // Gestion de plusieurs modules (pour les gourmands) import { load, get, listModules } from 'gowm'; const loadMultipleModules = async () => { await load('/validator.wasm', { name: 'validator' }); await load('/formatter.wasm', { name: 'formatter' }); // Lister les modules chargés console.log('Modules chargés:', listModules()); // Récupérer un module spécifique const validator = get('validator'); };

Support TypeScript (pour les perfectionnistes)

GoWM inclut des types TypeScript complets :

import { load, LoadOptions } from 'gowm'; interface ValidationResult { valid: boolean; errors: Record<string, string>; } const loadValidator = async (): Promise<void> => { await load('./validator.wasm'); // Les fonctions WASM sont disponibles globalement const result: string = (window as any).validateForm(formDataJSON, rulesJSON); const validation: ValidationResult = JSON.parse(result); };

Cette approche peut être pertinente si tu veux réutiliser la logique de validation dans plusieurs runtimes. Elle ajoute aussi une étape de build, un fichier WASM à distribuer et une frontière de sérialisation JSON. Pour un formulaire standard, une fonction TypeScript reste souvent plus simple.

  • Booster les performances de tes validations complexes

  • Simplifier l'intégration grâce aux hooks React tout prêts

  • Maintenir un code propre et testable

  • Réutiliser ta logique entre client et serveur

Que ce soit pour la validation de formulaires, le traitement d'images, ou des calculs de la mort, WebAssembly avec GoWM ouvre de nouvelles possibilités pour tes applications web

Le code source complet de cet exemple sera bientôt disponible sur GitHub.

Le code est surtout un exemple de frontière claire entre une logique Go et une interface React. À toi de mesurer avant de généraliser : le coût d'intégration et la taille du module comptent autant que le temps d'exécution.

Article co-écrit avec l'IA · Images générées avec l'IA.