FiveM NUI z React i Vite: konfiguracja, callbacks i dev przeglądarki

Zbuduj FiveM NUI z React i Vite: ui_page i pliki w fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus i tworzenie w przeglądarce.

Zwykły HTML działa dla małego NUI, ale prawdziwy interfejs jest łatwiejszy z React. Część, która przyłapuje ludzi, to nie React, to klej: manifest, ścieżki wyjścia budowy i jak wiadomości idą tam i z powrotem.

Ten artykuł konfiguruje React i Vite NUI, które ładują się w FiveM, rozmawiają z Lua w obu kierunkach i mogą być zbudowane w normalnej karcie przeglądarki.

Układ projektu

Umieść interfejs w folderze web wewnątrz zasobu i utwórz go za pomocą Vite:

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

Wynik:

text
my_script/
  fxmanifest.lua
  client/main.lua
  web/
    package.json
    vite.config.js
    src/
    dist/        (created by the build)

vite.config: base './'

FiveM ładuje Twoją stronę z adresu nui://, nie z katalogu głównego serwera sieciowego. Domyślne ścieżki zasobów Vite zaczynają się od /, które następnie wskazują na nigdzie i zostawiają Cię z pustą stroną. Uczyń je względne:

js
// web/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: './',
  build: { outDir: 'dist' },
})

Następnie zbuduj:

bash
npm run build

fxmanifest.lua

Wskaż ui_page na zbudowany index.html i wyślij cały folder dist do gracza z 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'

Tylko pliki wymienione w files są wysyłane do graczy. Jeśli obraz lub czcionka brakuje w grze, glob pliku nie go nie uwzględnił. Trzymaj node_modules i src poza listą: gracze ich nie potrzebują.

Lua do interfejsu użytkownika: SendNUIMessage

Skrypt klienta wysyła wiadomość i daje interfejsowi 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) daje klawiaturę i mysz stronie. Zawsze oddaj ją z powrotem, lub kursor się utknę: zobacz NUI focus stuck cursor.

W React, słuchaj wiadomości:

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

Strona, która nie renderuje nic, gdy jest ukryta, to zwykły wzór, ponieważ sama strona NUI pozostaje załadowana.

Interfejs do Lua: fetch i RegisterNUICallback

W innym kierunku strona POSTuje do adresu URL na Twoim zasobie. GetParentResourceName() to funkcja, którą FiveM dostarcza do strony i zwraca nazwę zasobu:

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>

W Lua obsługuj to. Callback musi być wywołany, lub żądanie pozostaje oczekujące:

lua
RegisterNUICallback('close', function(data, cb)
    setOpen(false)   -- gives focus back
    cb({ ok = true })
end)

data to zdekodowana treść JSON i cb odpowiada pobieraniu. Wszystko, co przekazujesz do cb, wraca jako odpowiedź.

Rozwijanie w przeglądarce

Przebudowanie i restartowanie zasobu dla każdej zmiany CSS jest powolne. Uruchom Vite i pracuj w normalnej karcie przeglądarki:

bash
npm run dev

Poza grą, window.GetParentResourceName nie istnieje i nikt nie wysyła Ci wiadomości, więc zaśmiesz oba:

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

Funkcja nuiPost powyżej już wznawia się do domyślnej nazwy zasobu. W przeglądarce żądanie po prostu nie powiedzie się, więc je złap lub pomiń wywołanie, gdy isBrowser jest prawdziwe. Użyj ciemnego lub szachownicowego tła w dev, ponieważ rzeczywista gra pokazuje świat za stroną.

Iteracja wewnątrz gry

Do końcowego sprawdzenia w grze, zbuduj i uruchom ponownie tylko zasób:

bash
npm run build

następnie ensure my_script w konsoli serwera. Z vite build --watch, pliki przebudowują się za każdym razem, gdy zapiszesz, i tylko restartujesz zasób.

Częste problemy

  • Pusta strona. Brakujące base: './', lub files nie obejmuje web/dist/**.
  • Nic się nie dzieje po kliknięciu. Nazwa RegisterNUICallback nie zgadza się z adresem URL pobierania, lub cb nigdy nie jest wywołany.
  • Kursor utknął po zamknięciu. Brakuje SetNuiFocus(false, false) na każdej ścieżce zamknięcia, łącznie z restartowaniem zasobu.
  • Czcionki i obrazy brakują w grze. Są Referenced przez ścieżkę bezwzględną, lub nie są w dist.
  • Wiadomość nigdy nie dociera. Strona nie została załadowana jeszcze, lub nazwy action różnią się między Lua a słuchaczem.

Checklist

Objaw Rozwiązanie
Pusty interfejs użytkownika w grze, działa w przeglądarce base: './' w vite.config, następnie npm run build
Brakujące pliki dla graczy files { 'web/dist/**' } w fxmanifest.lua
Przycisk nic nie robi Dopasuj nazwę RegisterNUICallback i wywołaj cb
Nie mogę dotrzeć do gry ze strony fetch do https://${GetParentResourceName()}/name
Kursor się nie wraca SetNuiFocus(false, false) podczas zamykania
Rozwijanie bez gry Uruchom vite, zaśmiesz postMessage i GetParentResourceName

Szybkie odpowiedzi

Dlaczego mój React NUI to pusta strona w FiveM?

Zwykle budujemy zbudować używa bezwzględnych ścieżek zasobów. Ustaw base: './' w vite.config, przebuduj i upewnij się, że files w fxmanifest.lua obejmuje cały folder web/dist.

Jak interfejs użytkownika wysyła dane z powrotem do gry?

Za pomocą fetch POST do https://${GetParentResourceName()}/eventName, obsługiwanego w Lua przez RegisterNUICallback('eventName', ...), które musi wywołać jego callback.

Czy mogę rozwijać NUI bez uruchamiania FiveM?

Tak. Uruchom vite w przeglądarce, wykryj, że jesteś poza grą, i wyślij fałszywe wiadomości za pomocą window.postMessage, aby zaśmieszyć to, co Lua by wysłała.

Skrypty bez tego problemu

Mic PhoneSkładany telefon, który rozkłada się w tablet i działa też na prawdziwym telefonie gracza.Zobacz skrypt →Clothing DesignerProjektuj ubrania w FiveM — pędzel, warstwy, import obrazów i AI — a potem je noś.Zobacz skrypt →Arcade MachinesSiedem grywalnych gier arcade w prawdziwych automatach, z rankingami i zakładami.Zobacz skrypt →

Czytaj dalej