FiveM NUI с React и Vite: setup, callbacks и разработка браузера

Постройте FiveM NUI с React и Vite: ui_page и файлы в fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus и разработка в браузере.

Обычный HTML работает для маленького NUI, но реальный интерфейс легче с React. Часть которая смущает людей это не React, это клей: манифест, пути вывода сборки, и как сообщения ходят туда и обратно.

Эта статья настраивает React и Vite NUI который загружается в FiveM, разговаривает с Lua в обе стороны, и может быть построен в нормальной вкладке браузера.

Макет проекта

Поместите интерфейс в папку web внутри ресурса и создайте его с Vite:

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

Результат:

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

vite.config: base './'

FiveM загружает вашу страницу из адреса nui://, не из корня веб сервера. Пути активов Vite по умолчанию начинаются с /, который затем указывает в никуда и оставляет вас с пустой страницей. Сделайте их относительными:

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

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

Затем соберите:

bash
npm run build

fxmanifest.lua

Укажите ui_page на встроенный index.html и отправьте целую папку dist игроку с 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'

Только файлы перечисленные в files отправляются игрокам. Если изображение или шрифт отсутствует в игре, глоб файлов его не включил. Держите node_modules и src вне списка: игроки их не нужны.

Lua в UI: SendNUIMessage

Клиент скрипт отправляет сообщение и даёт интерфейсу фокус:

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) даёт клавиатуру и мышь странице. Всегда верните её, или курсор застрянет: смотрите NUI фокус застрял курсор.

В React, слушайте сообщение:

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

Страница которая отображает ничего когда скрыта это обычный паттерн, так как сама страница NUI остаётся загруженной.

UI к Lua: fetch и RegisterNUICallback

Для другого направления, страница POSTs в URL вашего ресурса. GetParentResourceName() это функция которую FiveM предоставляет странице и возвращает имя ресурса:

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>

В Lua, обработайте это. Callback должен быть вызван, или запрос остаётся в ожидании:

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

data это декодированное JSON тело и cb отвечает на fetch. Что-либо что вы передадите в cb возвращается как ответ.

Разработка в браузере

Пересборка и перезапуск ресурса для каждого изменения CSS медленно. Запустите Vite и работайте в нормальной вкладке браузера:

bash
npm run dev

Вне игры, window.GetParentResourceName не существует и никто не отправляет вам сообщения, поэтому имитируйте оба:

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

Функция nuiPost выше уже откатывается к имени ресурса по умолчанию. В браузере, запрос просто падает, поэтому поймайте это или пропустите вызов когда isBrowser это истина. Используйте тёмный или клеточный фон в разработке, так как реальная игра показывает мир позади страницы.

Итерация внутри игры

Для финальной проверки в игре, соберите и перезапустите только ресурс:

bash
npm run build

затем ensure my_script в консоли сервера. С vite build --watch, файлы пересобираются каждый раз когда вы сохраняете, и вы только перезапускаете ресурс.

Распространённые проблемы

  • Пустая страница. Отсутствует base: './', или files не включает web/dist/**.
  • Ничего не происходит при нажатии. Имя RegisterNUICallback не совпадает с URL fetch, или cb никогда не вызывается.
  • Курсор застрял после закрытия. Отсутствует SetNuiFocus(false, false) на каждом пути закрытия, включая перезагрузку ресурса.
  • Шрифты и изображения отсутствуют в игре. Они ссылаются по абсолютному пути, или не находятся в dist.
  • Сообщение никогда не приходит. Страница ещё не загружена, или имена action отличаются между Lua и слушателем.

Контрольный список

Симптом Решение
Пустой UI в игре, работает в браузере base: './' в vite.config, затем npm run build
Файлы отсутствуют для игроков files { 'web/dist/**' } в fxmanifest.lua
Кнопка не делает ничего Совпадите имя RegisterNUICallback и вызовите cb
Не могу достичь игру со страницы fetch в https://${GetParentResourceName()}/name
Курсор не возвращается SetNuiFocus(false, false) при закрытии
Разработка без игры Запустите vite, имитируйте postMessage и GetParentResourceName

Короткие ответы

Почему мой React NUI это пустая страница в FiveM?

Обычно сборка использует абсолютные пути активов. Установите base: './' в vite.config, пересоберите, и убедитесь что files в fxmanifest.lua включает целую папку web/dist.

Как UI отправляет данные обратно в игру?

С fetch POST к https://${GetParentResourceName()}/eventName, обработано в Lua с RegisterNUICallback('eventName', ...), который должен вызвать его callback.

Могу ли я разрабатывать NUI без запуска FiveM?

Да. Запустите vite в браузере, обнаружьте что вы вне игры, и отправьте поддельные сообщения с window.postMessage чтобы имитировать что Lua отправил бы.

Скрипты без этой проблемы

Mic PhoneСкладной телефон, который раскладывается в планшет и работает и на настоящем телефоне игрока.Смотреть скрипт →Clothing DesignerСоздавайте одежду прямо в FiveM — кисть, слои, импорт изображений и ИИ — и носите её.Смотреть скрипт →Arcade MachinesСемь аркадных игр в настоящих автоматах, с таблицами рекордов и ставками.Смотреть скрипт →

Читайте также

FiveMNUILuaКурсор FiveM зависает или не может двигаться после закрытия меню: исправление фокуса NUIКурсор зависает на экране или не может двигаться после закрытия меню? Как работает SetNuiFocus, закрытие обратным вызовом NUI, обработка ESC в JavaScript и поиск причины.1 октября 2026 г. · 3 мин чтенияЧитать →FiveMNUIСкриптыВоспроизведение пользовательских звуков в FiveM: NUI аудио, громкость по расстоянию, форматыВоспроизведение пользовательских звуков в FiveM с помощью NUI аудио (SendNUIMessage и HTML элемента audio), громкость на основе расстояния, файлы OGG, xsound и встроенные аудиобанки.1 октября 2026 г. · 3 мин чтенияЧитать →FiveMNUIОшибкиОтладка FiveM NUI: nui_devtools, ошибки консоли и неудачные обратные вызовыОтладьте FiveM NUI: откройте Chromium devtools с nui_devtools, прочитайте ошибки Uncaught TypeError, исправьте неудачную выборку в https://resource/cb и протестируйте пользовательский интерфейс в браузере.1 октября 2026 г. · 4 мин чтенияЧитать →