FiveM NUI mit React und Vite: Einrichtung, Callbacks und Browser-Entwicklung
Baue eine FiveM NUI mit React und Vite: ui_page und Dateien in fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus und Entwicklung im Browser.
Einfaches HTML funktioniert für eine kleine NUI, aber eine echte Benutzeroberfläche ist leichter mit React. Der Teil, der Menschen verwirrt, ist nicht React, es ist der Kleber: das Manifest, die Build-Ausgabepfade und wie Nachrichten hin und her gehen.
Dieser Artikel richtet eine React und Vite NUI ein, die in FiveM lädt, mit Lua in beide Richtungen spricht und in einem normalen Browser-Tab erstellt werden kann.
Projekt-Layout
Lege die Schnittstelle in einen web Ordner innerhalb der Resource und erstelle ihn mit Vite:
cd my_script
npm create vite@latest web -- --template react
cd web
npm installDas Ergebnis:
my_script/
fxmanifest.lua
client/main.lua
web/
package.json
vite.config.js
src/
dist/ (created by the build)vite.config: base './'
FiveM lädt deine Seite von einer nui:// Adresse, nicht von einer Webserver-Wurzel. Vites Standard-Asset-Pfade beginnen mit /, die dann auf nichts zeigen und dich mit einer leeren Seite lassen. Mache sie relativ:
// web/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: './',
build: { outDir: 'dist' },
})Dann baue:
npm run buildfxmanifest.lua
Zeige ui_page auf das erstellte index.html und versende den ganzen dist Ordner zum Spieler mit files:
fx_version 'cerulean'
game 'gta5'
ui_page 'web/dist/index.html'
files {
'web/dist/index.html',
'web/dist/**',
}
client_script 'client/main.lua'Nur die in files aufgelisteten Dateien werden zum Spieler gesendet. Wenn ein Bild oder eine Schriftart im Spiel fehlt, schloss die Datei-Glob es nicht ein. Halte node_modules und src aus der Liste: Spieler brauchen sie nicht.
Lua zur UI: SendNUIMessage
Das Client-Skript sendet eine Nachricht und gibt der Benutzeroberfläche Fokus:
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) gibt die Tastatur und Maus der Seite. Gib sie immer zurück, sonst bleibt der Cursor stecken: siehe NUI-Fokus-Cursor stecken.
In React, lausche auf die Nachricht:
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>
}Eine Seite, die nichts rendern lässt, wenn versteckt, ist das übliche Muster, da die NUI-Seite selbst geladen bleibt.
UI zu Lua: fetch und RegisterNUICallback
Für die andere Richtung, POSTs die Seite zu einer URL auf deiner Resource. GetParentResourceName() ist eine Funktion, die FiveM der Seite bereitstellt und gibt den Resource-Namen zurück:
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>In Lua, behandle es. Der Callback muss aufgerufen werden, sonst bleibt die Anfrage ausstehend:
RegisterNUICallback('close', function(data, cb)
setOpen(false) -- gives focus back
cb({ ok = true })
end)data ist der dekodierte JSON-Body und cb antwortet auf den fetch. Alles, das du zu cb übergibst, kommt als Response zurück.
Entwickle im Browser
Das Neu-Bauen und Neustarten der Resource für jede CSS-Änderung ist langsam. Führe Vite aus und arbeite in einem normalen Browser-Tab:
npm run devAußerhalb des Spiels existiert window.GetParentResourceName nicht und niemand sendet dir Nachrichten, daher simuliere beide:
export const isBrowser = !window.invokeNative
// in main.jsx, in dev only
if (isBrowser) {
setTimeout(() => {
window.postMessage({ action: 'setVisible', visible: true, data: { name: 'Mic' } }, '*')
}, 500)
}Die nuiPost Funktion oben fällt bereits auf einen Standard-Resource-Namen zurück. Im Browser schlägt die Anfrage einfach fehl, daher fange sie ab oder überspringe den Aufruf, wenn isBrowser wahr ist. Nutze einen dunklen oder karierten Hintergrund in der Entwicklung, da das echte Spiel die Welt hinter der Seite zeigt.
Iterate innerhalb des Spiels
Für eine endgültige Überprüfung im Spiel, baue und starte nur die Resource neu:
npm run builddann ensure my_script in der Server-Konsole. Mit vite build --watch, werden die Dateien jedes Mal neu gebaut, wenn du speicherst, und du startest nur die Resource neu.
Häufige Probleme
- Leere Seite. Fehlendes
base: './', oderfilesschließtweb/dist/**nicht ein. - Nichts passiert beim Klicken. Der
RegisterNUICallbackName stimmt nicht mit der fetch-URL überein, odercbwird nie aufgerufen. - Cursor steckt nach dem Schließen fest. Fehlendes
SetNuiFocus(false, false)auf jedem Schließen-Pfad, einschließlich eines Resource-Neustarts. - Schriftarten und Bilder fehlen im Spiel. Sie werden durch absoluten Pfad referenziert oder sind nicht in
dist. - Nachricht kommt nie an. Die Seite wurde noch nicht geladen, oder
actionNamen unterscheiden sich zwischen Lua und dem Listener.
Checkliste
| Symptom | Behebung |
|---|---|
| Leere UI im Spiel, funktioniert im Browser | base: './' in vite.config, dann npm run build |
| Dateien fehlen für Spieler | files { 'web/dist/**' } in fxmanifest.lua |
| Button macht nichts | Passe den RegisterNUICallback Namen an und rufe cb auf |
| Kann das Spiel nicht von der Seite aus erreichen | fetch zu https://${GetParentResourceName()}/name |
| Cursor kommt nicht zurück | SetNuiFocus(false, false) beim Schließen |
| Entwicklung ohne das Spiel | Führe vite aus, simuliere postMessage und GetParentResourceName |
Kurze Antworten
Warum ist meine React NUI eine leere Seite in FiveM?
Normalerweise nutzt der Build absolute Asset-Pfade. Stelle base: './' in vite.config fest, baue neu, und stelle sicher, dass files in fxmanifest.lua den ganzen web/dist Ordner einschließt.
Wie sendet die UI Daten zurück zum Spiel?
Mit einem fetch POST zu https://${GetParentResourceName()}/eventName, behandelt in Lua durch RegisterNUICallback('eventName', ...), das seinen Callback aufrufen muss.
Kann ich die NUI entwickeln, ohne FiveM zu starten?
Ja. Führe vite im Browser aus, erkenne, dass du außerhalb des Spiels bist, und sende gefälschte Nachrichten mit window.postMessage um zu simulieren, was Lua senden würde.
Scripts ohne dieses Problem
Mic PhoneEin faltbares Handy, das sich zum Tablet aufklappt und bis aufs echte Handy des Spielers reicht.Script ansehen →
Clothing DesignerDesigne Kleidung direkt in FiveM — Pinsel, Ebenen, Bildimport und KI — und trag sie dann.Script ansehen →
Arcade MachinesSieben spielbare Arcade-Games in echten Automaten, mit Bestenlisten und Wetten.Script ansehen →