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 :

bash
cd my_script
npm create vite@latest web -- --template react
cd web
npm install

Le résultat :

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

js
// 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 :

bash
npm run build

fxmanifest.lua

Pointez ui_page vers le index.html construit et livrez tout le dossier dist au joueur avec files :

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

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

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

js
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()
}
jsx
<button onClick={() => nuiPost('close')}>Close</button>

En Lua, gérez-le. Le callback doit être appelé, sinon la demande reste en attente :

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

bash
npm run dev

En dehors du jeu, window.GetParentResourceName n'existe pas et personne ne vous envoie de messages, alors simulez les deux :

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

bash
npm run build

puis 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: './', ou files n'inclut pas web/dist/**.
  • Rien ne se passe en cliquant. Le nom RegisterNUICallback ne correspond pas à l'URL fetch, ou cb n'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'action diffè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 →

À lire aussi