FiveM NUI avec React et Vite : configuration, callbacks et développement du navigateur
Construisez un NUI FiveM avec React et Vite : ui_page et fichiers dans fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus et développement dans le navigateur.
Le HTML simple fonctionne pour une petite NUI, mais une vraie interface est plus facile avec React. La partie qui dérange les gens n'est pas React, c'est la colle : le manifeste, les chemins de sortie de la version, et comment les messages vont dans les deux sens.
Cet article configure une NUI React et Vite qui se charge dans FiveM, parle à Lua dans les deux directions, et peut être construite dans un onglet navigateur normal.
Disposition du projet
Mettez l'interface dans un dossier web à l'intérieur de la ressource et créez-la avec Vite :
cd my_script
npm create vite@latest web -- --template react
cd web
npm installLe résultat :
my_script/
fxmanifest.lua
client/main.lua
web/
package.json
vite.config.js
src/
dist/ (created by the build)vite.config : base './'
FiveM charge votre page à partir d'une adresse nui://, pas à partir de la racine d'un serveur web. Les chemins d'actifs par défaut de Vite commencent par /, qui pointent alors nulle part et vous laissent avec une page blanche. Rendez-les relatifs :
// web/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: './',
build: { outDir: 'dist' },
})Puis construisez :
npm run buildfxmanifest.lua
Pointez ui_page vers le index.html construit et livrez tout le dossier dist au joueur avec files :
fx_version 'cerulean'
game 'gta5'
ui_page 'web/dist/index.html'
files {
'web/dist/index.html',
'web/dist/**',
}
client_script 'client/main.lua'Seuls les fichiers listés dans files sont envoyés aux joueurs. Si une image ou une police manque dans le jeu, le glob de fichier ne l'a pas incluse. Gardez node_modules et src en dehors de la liste : les joueurs n'en ont pas besoin.
Lua à l'interface : SendNUIMessage
Le script client envoie un message et donne le focus à l'interface :
local open = false
local function setOpen(state)
open = state
SetNuiFocus(state, state)
SendNUIMessage({ action = 'setVisible', visible = state, data = { name = 'Mic' } })
end
RegisterCommand('myui', function()
setOpen(not open)
end, false)SetNuiFocus(hasFocus, hasCursor) donne le clavier et la souris à la page. Ramenez-le toujours, sinon le curseur colle : consultez NUI focus curseur collé.
En React, écoutez le message :
import { useEffect, useState } from 'react'
export default function App() {
const [visible, setVisible] = useState(false)
const [name, setName] = useState('')
useEffect(() => {
const onMessage = (event) => {
const { action, visible, data } = event.data
if (action === 'setVisible') {
setVisible(visible)
if (data) setName(data.name)
}
}
window.addEventListener('message', onMessage)
return () => window.removeEventListener('message', onMessage)
}, [])
if (!visible) return null
return <div className="panel">Hello {name}</div>
}Une page qui ne rend rien quand elle est cachée est le modèle habituel, car la page NUI elle-même reste chargée.
Interface à Lua : fetch et RegisterNUICallback
Pour l'autre direction, la page POSTs à une URL sur votre ressource. GetParentResourceName() est une fonction que FiveM fournit à la page et retourne le nom de la ressource :
export async function nuiPost(eventName, data = {}) {
const resource = window.GetParentResourceName ? window.GetParentResourceName() : 'my_script'
const resp = await fetch(`https://${resource}/${eventName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(data),
})
return resp.json()
}<button onClick={() => nuiPost('close')}>Close</button>En Lua, gérez-le. Le callback doit être appelé, sinon la demande reste en attente :
RegisterNUICallback('close', function(data, cb)
setOpen(false) -- gives focus back
cb({ ok = true })
end)data est le corps JSON décodé et cb répond à la fetch. Tout ce que vous passez à cb revient comme réponse.
Développer dans le navigateur
Reconstruire et redémarrer la ressource pour chaque changement CSS est lent. Exécutez Vite et travaillez dans un onglet navigateur normal :
npm run devEn dehors du jeu, window.GetParentResourceName n'existe pas et personne ne vous envoie de messages, alors simulez les deux :
export const isBrowser = !window.invokeNative
// in main.jsx, in dev only
if (isBrowser) {
setTimeout(() => {
window.postMessage({ action: 'setVisible', visible: true, data: { name: 'Mic' } }, '*')
}, 500)
}La fonction nuiPost ci-dessus se replie déjà sur un nom de ressource par défaut. Dans le navigateur, la demande échoue simplement, donc attrapez-la ou sautez l'appel quand isBrowser est vrai. Utilisez un arrière-plan sombre ou à damier en développement, car le vrai jeu montre le monde derrière la page.
Itération à l'intérieur du jeu
Pour une vérification finale dans le jeu, construisez et redémarrez uniquement la ressource :
npm run buildpuis ensure my_script dans la console du serveur. Avec vite build --watch, les fichiers se reconstruisent à chaque sauvegarde, et vous redémarrez uniquement la ressource.
Problèmes courants
- Page blanche. Manquant
base: './', oufilesn'inclut pasweb/dist/**. - Rien ne se passe en cliquant. Le nom
RegisterNUICallbackne correspond pas à l'URL fetch, oucbn'est jamais appelé. - Curseur coincé après fermeture. Manquant
SetNuiFocus(false, false)sur chaque chemin de fermeture, y compris un redémarrage de ressource. - Polices et images manquantes dans le jeu. Elles sont référencées par un chemin absolu, ou ne sont pas dans
dist. - Le message n'arrive jamais. La page n'a pas été chargée encore, ou les noms d'
actiondiffèrent entre Lua et l'écouteur.
Liste de contrôle
| Symptôme | Solution |
|---|---|
| Interface blanche dans le jeu, fonctionne dans le navigateur | base: './' dans vite.config, puis npm run build |
| Fichiers manquants pour les joueurs | files { 'web/dist/**' } dans fxmanifest.lua |
| Le bouton ne fait rien | Faites correspondre le nom RegisterNUICallback et appelez cb |
| Impossible d'atteindre le jeu à partir de la page | fetch à https://${GetParentResourceName()}/name |
| Le curseur ne revient pas | SetNuiFocus(false, false) lors de la fermeture |
| Développement sans le jeu | Exécutez vite, simulez postMessage et GetParentResourceName |
Réponses rapides
Pourquoi mon NUI React est-il une page blanche dans FiveM ?
Généralement, la version utilise des chemins d'actifs absolus. Définissez base: './' dans vite.config, reconstruisez, et assurez-vous que files dans fxmanifest.lua inclut l'ensemble du dossier web/dist.
Comment l'interface renvoie-t-elle les données au jeu ?
Avec un fetch POST à https://${GetParentResourceName()}/eventName, géré en Lua par RegisterNUICallback('eventName', ...), qui doit appeler son callback.
Puis-je développer le NUI sans démarrer FiveM ?
Oui. Exécutez vite dans le navigateur, détectez que vous êtes en dehors du jeu, et envoyez de faux messages avec window.postMessage pour simuler ce que Lua enverrait.
Des scripts sans ce problème
Mic PhoneUn téléphone pliable qui se déplie en tablette et se prolonge jusqu’au vrai téléphone du joueur.Voir le script →
Clothing DesignerCréez des vêtements dans FiveM — pinceau, calques, import d’images et IA — puis portez-les.Voir le script →
Arcade MachinesSept jeux d’arcade jouables dans de vraies bornes, avec classements et paris.Voir le script →