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:

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

Das Ergebnis:

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

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

bash
npm run build

fxmanifest.lua

Zeige ui_page auf das erstellte index.html und versende den ganzen dist Ordner zum Spieler mit 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'

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:

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

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

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:

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>

In Lua, behandle es. Der Callback muss aufgerufen werden, sonst bleibt die Anfrage ausstehend:

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

bash
npm run dev

Außerhalb des Spiels existiert window.GetParentResourceName nicht und niemand sendet dir Nachrichten, daher simuliere beide:

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

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:

bash
npm run build

dann 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: './', oder files schließt web/dist/** nicht ein.
  • Nichts passiert beim Klicken. Der RegisterNUICallback Name stimmt nicht mit der fetch-URL überein, oder cb wird 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 action Namen 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 →

Weiterlesen