FiveM RegisterNUICallback คืออะไร ใช้อย่างไร? รับข้อมูลจาก NUI JavaScript เข้า Lua แบบถูกต้อง

FiveM RegisterNUICallback คือ API ฝั่ง Client ที่ใช้รับ Request จากหน้า NUI ซึ่งเขียนด้วย HTML/JavaScript แล้วนำข้อมูลจาก Browser เข้ามาประมวลผลใน FiveM Client Script เช่น Player กดปุ่มปิด Inventory, เลือกรถ, ใช้ Item, กดซื้อสินค้า หรือส่ง Form จาก UI

Flow พื้นฐานคือ

HTML / JavaScript
↓
fetch()
↓
NUI Callback
↓
FiveM Client Lua
↓
ประมวลผล
↓
cb(...)
↓
JavaScript ได้ Response

ตัวอย่าง JavaScript

const response = await fetch(
    `https://${GetParentResourceName()}/close`,
    {
        method: 'POST',

        headers: {
            'Content-Type':
                'application/json; charset=UTF-8'
        },

        body: JSON.stringify({})
    }
);

const result =
    await response.json();

ฝั่ง Client Lua

RegisterNuiCallback(
    'close',
    function(data, cb)

        SetNuiFocus(
            false,
            false
        )

        cb({
            ok = true
        })
    end
)

จุดสำคัญมากสำหรับ Code ปัจจุบันคือ Cfx.re ระบุว่า RegisterNUICallback แบบชื่อเดิมเป็น Legacy API ที่คงไว้เพื่อ Backward Compatibility และ Documentation ปัจจุบันใช้ RegisterNuiCallback ซึ่งเป็น Wrapper ของ REGISTER_NUI_CALLBACK สำหรับ Code ใหม่

ดังนั้นคุณยังอาจเจอ Script เก่าเขียน

RegisterNUICallback(...)

แต่ Resource ใหม่ควรอิง API ปัจจุบัน

RegisterNuiCallback(...)

① RegisterNUICallback คืออะไร

หน้าที่หลักคือรับ Event จาก NUI Browser

สมมติหน้า Inventory มี Button

<button id="close">
    ปิด
</button>

JavaScript เมื่อกด Button

fetch(
    `https://${GetParentResourceName()}/close`,
    {
        method: 'POST',

        headers: {
            'Content-Type':
                'application/json; charset=UTF-8'
        },

        body:
            JSON.stringify({})
    }
);

FiveM Client จะสามารถรับ Callback ชื่อ

close

ได้

② RegisterNUICallback ทำงานฝั่งไหน

ทำงานใน Client Runtime

Flow คือ

NUI Browser
↓
FiveM Client

ไม่ใช่

NUI Browser
↓
FXServer โดยตรง

ถ้าต้องใช้ Server Authority ต้องส่งต่อ

NUI
↓
Client
↓
Server

อีกชั้นหนึ่ง

③ API ปัจจุบันควรเขียนอย่างไร

Lua ปัจจุบันตาม Cfx.re Documentation ใช้

RegisterNuiCallback(
    'eventName',
    function(data, cb)

        -- handle request

        cb({
            ok = true
        })
    end
)

ตัวอย่าง

RegisterNuiCallback(
    'close',
    function(data, cb)

        print(
            'Close requested'
        )

        cb({
            ok = true
        })
    end
)

④ แล้ว RegisterNUICallback แบบตัวใหญ่ล่ะ

คุณจะยังพบ

RegisterNUICallback(
    'close',
    function(data, cb)

        cb({})
    end
)

ใน Script FiveM รุ่นเก่าจำนวนมาก

Cfx.re ปัจจุบันระบุว่า API ชื่อนี้เก็บไว้เพื่อ Backward Compatibility

จึงไม่ควรตีความว่า Script เก่าทุกตัวพังทันที

แต่ Code ใหม่ควรอิง API ปัจจุบัน

⑤ REGISTER_NUI_CALLBACK คืออะไร

REGISTER_NUI_CALLBACK คือ Native/API ระดับปัจจุบันสำหรับ Register NUI Callback

ใน Lua มี Convenience Wrapper

RegisterNuiCallback(...)

จึงไม่จำเป็นต้องเขียนชื่อ Native แบบตัวใหญ่ทุกครั้ง

จำง่าย ๆ

Native
REGISTER_NUI_CALLBACK

Lua wrapper
RegisterNuiCallback

⑥ Browser เรียก Callback อย่างไร

ใช้

fetch(
    `https://${GetParentResourceName()}/callbackName`,
    ...
);

ถ้า Lua Register

RegisterNuiCallback(
    'close',
    ...
)

JavaScript ต้องเรียก

/close

ชื่อทั้งสองต้องตรงกัน

⑦ GetParentResourceName คืออะไร

ใช้หาชื่อ Resource ที่กำลังเป็น Parent ของ NUI

เช่น

const resourceName =
    GetParentResourceName();

แล้ว

fetch(
    `https://${resourceName}/close`,
    ...
);

ดีกว่า Hardcode

fetch(
    'https://my_inventory/close',
    ...
);

เพราะหาก Rename Resource Browser Callback ยังสามารถทำงานตามชื่อจริงของ Resource

⑧ ทำไมต้องใช้ https

Resource ที่ใช้

fx_version 'cerulean'

ใช้ Secure-context NUI

ดังนั้น Callback URL ปัจจุบันเป็น

https://

ไม่ใช่ Pattern รุ่นเก่า

http://

Script NUI เก่าที่ Migration มา cerulean จึงควรตรวจจุดนี้

⑨ Callback Name ต้องตรงกัน

Lua

RegisterNuiCallback(
    'buyItem',
    ...
)

JavaScript ต้องเป็น

fetch(
    `https://${GetParentResourceName()}/buyItem`,
    ...
);

ถ้า JavaScript เรียก

/buyitem

หรือ

/buy

ก็ไม่ใช่ชื่อเดียวกัน

ควรใช้ Naming Convention ให้สม่ำเสมอ

⑩ POST Data ส่งอย่างไร

JavaScript

body:
    JSON.stringify({
        item: 'water',
        amount: 2
    })

FiveM จะ Parse POST JSON ให้ Callback ตาม Runtime API

Lua

RegisterNuiCallback(
    'buyItem',
    function(data, cb)

        print(
            data.item,
            data.amount
        )

        cb({
            ok = true
        })
    end
)

⑪ data คืออะไร

data คือ Payload ที่ NUI ส่งเข้ามา

เช่น Browser

{
  "item": "water",
  "amount": 2
}

Lua จะเข้าถึง

data.item

และ

data.amount

ได้

⑫ ต้องตรวจ data ไหม

ต้อง

อย่าทำ

local amount =
    data.amount

UseItem(
    amount
)

ทันที

ควรตรวจ

local amount =
    tonumber(
        data.amount
    )

if not amount then
    cb({
        ok = false,
        error = 'invalid_amount'
    })

    return
end

NUI เป็น Client-side Input

จึงไม่ควรเชื่อถือโดยอัตโนมัติ

⑬ cb คืออะไร

cb ใช้ตอบ Response กลับไปยัง Browser Request

เช่น

cb({
    ok = true
})

Browser

const response =
    await fetch(...);

const data =
    await response.json();

console.log(
    data.ok
);

ถ้า Lua ส่ง

cb({
    ok = true
})

JavaScript จะได้รับ Object ที่มี

data.ok

เป็น true

⑭ ต้องเรียก cb ทุกครั้งไหม

ควรเรียกทุกครั้ง

Cfx.re ระบุชัดว่าถ้า Callback ไม่คืนข้อมูลผ่าน cb Request จะ Timeout และ Error จะส่งกลับขึ้นไปยัง fetch() ฝั่ง UI

ดังนั้นแม้ไม่มีข้อมูลให้คืนก็ควรใช้

cb({})

หรือ

cb({
    ok = true
})

⑮ ตัวอย่างที่ผิด

RegisterNuiCallback(
    'close',
    function(data, cb)

        SetNuiFocus(
            false,
            false
        )

        -- ไม่มี cb()
    end
)

UI อาจปิดได้

แต่ Browser Request ยังไม่ได้รับ Response

สุดท้าย fetch() อาจ Error/Timeout

⑯ ทุก Branch ต้องตอบ cb

ตัวอย่าง

RegisterNuiCallback(
    'getItem',
    function(data, cb)

        if not data.item then

            cb({
                ok = false,
                error = 'missing_item'
            })

            return
        end

        cb({
            ok = true,
            item = data.item
        })
    end
)

ทั้ง Error และ Success มี Response

นี่เป็น Pattern ที่ดี

⑰ JSON Encodable หมายถึงอะไร

Cfx.re ระบุว่า Data ที่ส่งเข้า/ออก NUI Callback ต้องสามารถ Encode เป็น JSON ได้

เหมาะกับ

string
number
boolean
array
object/table
null-like values

ไม่เหมาะกับ Runtime Objects ที่ Serialize ไม่ได้ เช่น Function Handle หรือ Native Object บางประเภท

⑱ อย่าส่ง Entity Handle Object ซับซ้อนเข้า Browser

ถ้าต้องการแสดง Vehicle

ส่งข้อมูล Presentation เช่น

cb({
    id = vehicleId,
    plate = plate,
    model = model
})

ไม่ควรพยายามส่ง Runtime Structure ที่ Browser ใช้ไม่ได้

Browser ควรได้รับ Data Model ที่เรียบง่าย

⑲ RegisterNuiCallback ต่างจาก SendNUIMessage อย่างไร

จำง่าย ๆ

SendNUIMessage
FiveM Client
→ NUI

ส่วน

RegisterNuiCallback
NUI
→ FiveM Client

สองตัวจึงทำงานคนละทิศทาง

⑳ Flow สองทางแบบครบ

Client เปิด UI

SendNUIMessage({
    action = 'open'
})

Browser รับ

window.addEventListener(
    'message',
    (event) => {

        if (
            event.data.action
            === 'open'
        ) {
            // open UI
        }
    }
);

Player คลิกปิด

fetch(
    `https://${GetParentResourceName()}/close`,
    ...
);

Client รับ

RegisterNuiCallback(
    'close',
    ...
)

นี่คือ Communication Loop พื้นฐาน

㉑ RegisterNuiCallback ต่างจาก RegisterNetEvent อย่างไร

RegisterNuiCallback

NUI Browser
→ Client Resource

ส่วน RegisterNetEvent

Client ↔ Server Network Event

หรือ Network-safe Event ตาม Context

อย่าใช้แทนกัน

㉒ NUI Callback ไม่ใช่ Network Event

หาก NUI เรียก

buyItem

Browser Request เข้าสู่ Client Callback ก่อน

ไม่ได้หมายความว่า Server ได้รับ Event buyItem

ถ้าต้องให้ Server ซื้อ Item ต้องส่งต่อ

TriggerServerEvent(
    'shop:buyItem',
    item,
    amount
)

㉓ NUI Callback ส่งต่อ Server อย่างไร

ตัวอย่าง

RegisterNuiCallback(
    'buyItem',
    function(data, cb)

        local item =
            tostring(
                data.item or ''
            )

        local amount =
            tonumber(
                data.amount
            )

        if item == ''
            or not amount then

            cb({
                ok = false,
                error = 'invalid_input'
            })

            return
        end

        TriggerServerEvent(
            'shop:buyItem',
            item,
            amount
        )

        cb({
            ok = true,
            accepted = true
        })
    end
)

แต่ accepted = true ตรงนี้หมายถึง Client รับ Request แล้ว

ไม่ได้หมายความว่า Server ซื้อสำเร็จแล้ว

㉔ Accepted กับ Completed ต่างกัน

นี่เป็น Architecture สำคัญ

NUI
→ ขอซื้อ

Client สามารถตอบ

Request accepted

แต่ผลจริงอาจเป็น

Server:
เงินไม่พอ
Item ไม่มี
ร้านปิด
Cooldown
Permission ไม่ผ่าน

ดังนั้นอย่าใช้ cb({success=true}) ก่อน Server ตัดสิน แล้วให้ UI คิดว่าซื้อสำเร็จ

㉕ ถ้าต้องการผลจาก Server ทำอย่างไร

มี 2 Pattern หลัก

Pattern A — Request แล้ว Update UI ภายหลัง

NUI
↓
Client callback
↓
Server event
↓
Server result event
↓
Client
↓
SendNUIMessage
↓
NUI

เหมาะกับระบบ Event-driven

Pattern B — ใช้ Server Callback/Promise Library

เช่น Framework หรือ Library Callback ที่สามารถ Await Response ได้

แต่ API นี้ขึ้นกับ Library เช่น ESX, QBCore, Qbox หรือ ox_lib

FiveM Core ไม่มี Universal ServerCallback() ชื่อเดียวสำหรับทุก Framework

㉖ ตัวอย่าง Event-driven Buy Item

Client

RegisterNuiCallback(
    'buyItem',
    function(data, cb)

        TriggerServerEvent(
            'shop:buyItem',
            data.item,
            data.amount
        )

        cb({
            ok = true,
            pending = true
        })
    end
)

Server ทำงาน

Validate
↓
Check money
↓
Check stock
↓
Complete purchase

แล้ว Server ส่ง

shop:purchaseResult

กลับ Client

Client จึง

SendNUIMessage({
    action = 'purchaseResult',
    success = true
})

ให้ Browser Update

㉗ Server ต้อง Validate ซ้ำไหม

ต้อง

แม้ Client Callback ตรวจ

amount > 0

แล้ว

Server ก็ต้องตรวจอีกครั้ง

เพราะ Client Runtime สามารถถูก Manipulate ได้

หลักคือ

Client validation
→ UX / early rejection

Server validation
→ Security / authority

㉘ อย่าให้ NUI เป็น Authority

ตัวอย่างที่ผิด

NUI ส่ง

{
  "price": 1
}

แล้ว Server ใช้

price =
    clientPrice

ซื้อรถราคา 1

นี่ไม่ปลอดภัย

Server ต้องมี Price Server-side

Vehicle Model
↓
Server Config/Database
↓
Official Price

㉙ อย่าให้ NUI ส่ง Permission

ผิด

{
  "isAdmin": true
}

แล้ว Client/Server เชื่อ

Permission ต้องตรวจจาก Server Authority

เช่น ACE, Framework Permission หรือระบบ Server-side ที่กำหนด

㉚ อย่าให้ NUI ส่ง SQL

ห้ามสร้าง Generic Callback

RegisterNuiCallback(
    'database',
    function(data, cb)

        TriggerServerEvent(
            'database:run',
            data.sql
        )

        cb({})
    end
)

นี่เป็น Architecture ที่อันตรายอย่างมาก

NUI ควรส่ง Business Intent เช่น

garage:storeVehicle
shop:buyItem
phone:sendMessage

ไม่ใช่ SQL

㉛ ตัวอย่างปิด UI ที่ถูกต้อง

Client

local uiOpen =
    false

local function closeUi()

    uiOpen =
        false

    SetNuiFocus(
        false,
        false
    )

    SendNUIMessage({
        action = 'close'
    })
end

RegisterNuiCallback(
    'close',
    function(data, cb)

        closeUi()

        cb({
            ok = true
        })
    end
)

Browser

await fetch(
    `https://${GetParentResourceName()}/close`,
    {
        method: 'POST',

        headers: {
            'Content-Type':
                'application/json; charset=UTF-8'
        },

        body:
            JSON.stringify({})
    }
);

㉜ กด ESC ปิด NUI ได้ไหม

ทำได้หลายแบบ

JavaScript สามารถฟัง Key Event

document.addEventListener(
    'keydown',
    async (event) => {

        if (
            event.key === 'Escape'
        ) {
            await closeMenu();
        }
    }
);

แล้วให้ closeMenu() เรียก NUI Callback

เพื่อให้ Client เป็นผู้คืน Focus

㉝ อย่าซ่อน HTML อย่างเดียวตอนปิด

ไม่ควรมีแค่

menu.style.display =
    'none';

เพราะ Client อาจยังมี

SetNuiFocus(
    true,
    true
)

อยู่

จึงเกิดอาการ

UI หาย
แต่ Mouse/Keyboard ยังติด NUI

ควรเรียก Client Callback เพื่อคืน Focus ด้วย

㉞ SetNuiFocus ควรอยู่ Client

ตัวอย่าง

RegisterNuiCallback(
    'close',
    function(data, cb)

        SetNuiFocus(
            false,
            false
        )

        cb({
            ok = true
        })
    end
)

NUI Browser ไม่ควรเป็นคนจัด Game Focus เอง

Client Runtime เป็นตัวควบคุม

㉟ ตัวอย่าง Form Submission

HTML

<input
    id="firstname"
    type="text"
>

<input
    id="lastname"
    type="text"
>

<button id="create">
    สร้างตัวละคร
</button>

JavaScript

const firstName =
    document.getElementById(
        'firstname'
    ).value;

const lastName =
    document.getElementById(
        'lastname'
    ).value;

const response =
    await fetch(
        `https://${GetParentResourceName()}/createCharacter`,
        {
            method: 'POST',

            headers: {
                'Content-Type':
                    'application/json; charset=UTF-8'
            },

            body:
                JSON.stringify({
                    firstName,
                    lastName
                })
        }
    );

㊱ Validate Form ฝั่ง Client

RegisterNuiCallback(
    'createCharacter',
    function(data, cb)

        local firstName =
            tostring(
                data.firstName or ''
            )

        local lastName =
            tostring(
                data.lastName or ''
            )

        if firstName == ''
            or lastName == '' then

            cb({
                ok = false,
                error = 'missing_name'
            })

            return
        end

        cb({
            ok = true
        })
    end
)

แต่ Server ยังต้อง Validate ซ้ำหากข้อมูลนี้ถูก Persist

㊲ จำกัด Length ก่อนส่ง Server

เช่น

if #firstName > 50 then

    cb({
        ok = false,
        error = 'firstname_too_long'
    })

    return
end

ช่วย UX และลด Payload ผิดปกติ

แต่ Security Rule หลักต้องอยู่ Server อีกชั้นหนึ่ง

㊳ Type Validation สำคัญไหม

สำคัญ

NUI อาจส่ง

{
  "amount": "5"
}

เป็น String

Lua ควร

local amount =
    tonumber(
        data.amount
    )

แล้วตรวจ

if not amount then
    ...
end

ไม่ควรสมมติ Type จาก UI เสมอ

㊴ Range Validation

เช่นจำนวน Item

if amount < 1
    or amount > 100 then

    cb({
        ok = false,
        error = 'invalid_amount'
    })

    return
end

แต่ Server ต้องมี Limit เดียวกันหรือเข้มกว่า

㊵ Return Error ให้ Browser อย่างไร

Lua

cb({
    ok = false,
    error = 'invalid_vehicle'
})

Browser

const result =
    await response.json();

if (!result.ok) {
    console.error(
        result.error
    );
}

ช่วยให้ Error Handling เป็นระบบ

㊶ ควรใช้ Error Code หรือ Error Message

แนะนำ Error Code

{
  "ok": false,
  "error": "not_enough_money"
}

แล้ว Browser แปลงเป็นข้อความ

const messages = {
    not_enough_money:
        'เงินไม่เพียงพอ'
};

ดีกว่า Server/Client ส่งข้อความ UI ภาษาไทยทุกจุด

ช่วยรองรับหลายภาษา

㊷ Response Schema ควรคงที่

ตัวอย่าง Pattern

{
  "ok": true,
  "data": {}
}

หรือ Error

{
  "ok": false,
  "error": "invalid_input"
}

ทำให้ Frontend จัดการง่ายกว่า Callback แต่ละตัวคืน Format คนละแบบ

㊸ Browser ควรใช้ try/catch

ตัวอย่าง

try {

    const response =
        await fetch(
            `https://${GetParentResourceName()}/close`,
            options
        );

    if (!response.ok) {
        throw new Error(
            `HTTP ${response.status}`
        );
    }

    const result =
        await response.json();

} catch (error) {

    console.error(
        'NUI request failed:',
        error
    );
}

ช่วย Debug Callback Timeout หรือ Request Error

㊹ fetch ค้างควรตรวจอะไร

ตรวจ

① Callback Name ตรงไหม?
② ใช้ https:// หรือไม่?
③ GetParentResourceName ถูกไหม?
④ Client Script Register Callback หรือยัง?
⑤ Resource Started ไหม?
⑥ Handler Error ก่อน cb หรือไม่?
⑦ ทุก Branch เรียก cb หรือไม่?
⑧ F8 มี Client Error ไหม?
⑨ NUI DevTools Network มี Error ไหม?

โดยเฉพาะข้อ cb

เพราะ Cfx.re ระบุว่าไม่คืน Callback จะทำ Request Timeout

㊺ Handler Error ก่อน cb จะเกิดอะไร

ตัวอย่าง

RegisterNuiCallback(
    'test',
    function(data, cb)

        print(
            data.user.name
        )

        cb({
            ok = true
        })
    end
)

ถ้า

data.user == nil

Lua Error ก่อนถึง cb

Browser Request จึงอาจไม่มี Response

ควร Validate ก่อน Access Nested Data

㊻ Safe Nested Data

local user =
    data.user

if type(user)
    ~= 'table' then

    cb({
        ok = false,
        error = 'invalid_user'
    })

    return
end

local name =
    tostring(
        user.name or ''
    )

ช่วยลด Runtime Error

㊼ Callback ชื่อซ้ำได้ไหม

ไม่ควรออกแบบ Resource ให้ Register Callback Name เดียวกันหลาย Handler โดยไม่เข้าใจ Runtime Behavior

ใช้ชื่อแต่ละ Action ให้ชัด

close
selectVehicle
buyItem
useItem
saveSettings

ภายใน Resource เดียว

㊽ ต้องใส่ Resource Prefix ใน Callback Name ไหม

NUI Callback URL ถูก Scope ด้วย Resource อยู่แล้ว

เช่น

https://my_garage/storeVehicle

ดังนั้น Callback ภายใน Resource สามารถใช้

storeVehicle

ได้โดยไม่จำเป็นต้องเขียน

my_garage:storeVehicle

เสมอไป

แต่ Naming Convention เลือกได้ตาม Project

㊾ RegisterNuiCallback กับ RegisterRawNuiCallback ต่างกันอย่างไร

FiveM มี Raw NUI Callback APIs สำหรับระดับต่ำกว่า

และมี UNREGISTER_RAW_NUI_CALLBACK สำหรับ Cleanup Handler แบบ Raw

แต่ Resource Lua ทั่วไปที่รับ JSON ควรใช้ RegisterNuiCallback ซึ่งจัด JSON Parsing/Response ให้สะดวกกว่า

ไม่ควรเลือก Raw API ถ้าไม่มี Requirement จริง

㊿ JavaScript Client Runtime ใช้ NUI Callback ได้ไหม

ได้

Cfx.re Documentation ปัจจุบันมีตัวอย่าง

RegisterNuiCallback(
    'getItemInfo',
    (data, cb) => {

        if (
            !itemCache[data.itemId]
        ) {
            cb({
                error:
                    'No such item!'
            });

            return;
        }

        cb(
            itemCache[data.itemId]
        );
    }
);

หลักเหมือน Lua

data
+
cb

51 C# ใช้ NUI Callback ได้ไหม

ได้

Documentation ปัจจุบันมีตัวอย่าง C# ผ่าน

RegisterNuiCallback

กับ Dictionary/Object และ Callback Delegate

ดังนั้น NUI Callback ไม่ได้จำกัดเฉพาะ Lua

แต่บทความนี้ใช้ Lua เพราะเป็นภาษาที่พบมากใน FiveM Resources

52 NUI Callback เร็วไหม

การส่ง Browser → Client เป็น Local NUI Communication

แต่ Performance ยังขึ้นกับ

Payload Size
Callback Frequency
JSON Serialization
Browser JavaScript
Client Handler

อย่าใช้ Callback หลายร้อยครั้งต่อ Frame โดยไม่มีเหตุผล

53 Drag Slider ต้องยิง Callback ทุก Mouse Move ไหม

ไม่จำเป็นเสมอไป

เช่น Volume Slider สามารถ

update UI locally

ระหว่างลาก

แล้วส่ง Callback ตอน

change
mouseup
debounce

แทนการส่งทุก Pixel Movement

ช่วยลด Message Traffic

54 Search Box ควร Debounce

ไม่ควรทำ

พิมพ์ทุกตัวอักษร
→ Callback
→ Server
→ Database Query

ทันทีทุก Keystroke

ควรใช้ Debounce เช่น 200–300 ms ตาม UX

หรือ Filter Client-side ถ้าข้อมูลอยู่ใน NUI แล้ว

55 อย่าใช้ NUI Callback Query Database โดยตรงจาก Client

Flow ที่ควรเป็น

NUI
↓
Client
↓
Server
↓
Validate
↓
Database

Client Lua ไม่ควรถือ Database Credentials หรือ SQL Authority

ถ้า NUI กด Search Player ระบบสำคัญควรส่ง Request ไป Server

56 Resource Stop ต้องทำอะไรกับ NUI Callback

เมื่อ Resource Stop Runtime จะถูกหยุด

แต่สิ่งสำคัญที่ควร Cleanup คือ UI State เช่น

AddEventHandler(
    'onClientResourceStop',
    function(resourceName)

        if resourceName ~=
            GetCurrentResourceName() then
            return
        end

        SetNuiFocus(
            false,
            false
        )
    end
)

เพื่อป้องกัน Focus ค้างหลัง Restart Resource

57 Debug NUI Callback ที่ไหน

ใช้ 3 จุดหลัก

NUI DevTools
→ Browser JS / Network

F8
→ Client Lua/JS Errors

FXServer Console
→ Server Event/Database Errors

ถ้า fetch() ไม่ตอบ เริ่มจาก DevTools Network + F8

ถ้า Client รับแล้วแต่ Server ไม่ตอบ ไปดู FXServer

58 ตัวอย่าง Resource RegisterNuiCallback แบบครบ

โครงสร้าง

com_garage/
├── fxmanifest.lua
├── client.lua
└── html/
    ├── index.html
    └── app.js

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'

author 'comsiam'
description 'FiveM NUI callback example'
version '1.0.0'

ui_page 'html/index.html'

files {
    'html/index.html',
    'html/app.js'
}

client_script 'client.lua'

client.lua

local uiOpen =
    false

local function setUi(
    state
)

    uiOpen =
        state

    SetNuiFocus(
        state,
        state
    )

    SendNUIMessage({
        action =
            state
            and 'open'
            or 'close'
    })
end

RegisterCommand(
    'garageui',
    function()

        setUi(
            true
        )

    end,
    false
)

RegisterNuiCallback(
    'close',
    function(data, cb)

        setUi(
            false
        )

        cb({
            ok = true
        })
    end
)

RegisterNuiCallback(
    'selectVehicle',
    function(data, cb)

        local vehicleId =
            tonumber(
                data.vehicleId
            )

        if not vehicleId then

            cb({
                ok = false,
                error =
                    'invalid_vehicle'
            })

            return
        end

        TriggerServerEvent(
            'com_garage:selectVehicle',
            vehicleId
        )

        cb({
            ok = true,
            pending = true
        })
    end
)

AddEventHandler(
    'onClientResourceStop',
    function(resourceName)

        if resourceName ~=
            GetCurrentResourceName() then
            return
        end

        SetNuiFocus(
            false,
            false
        )
    end
)

app.js

async function nuiRequest(
    eventName,
    data = {}
) {

    const response =
        await fetch(
            `https://${GetParentResourceName()}/${eventName}`,
            {
                method: 'POST',

                headers: {
                    'Content-Type':
                        'application/json; charset=UTF-8'
                },

                body:
                    JSON.stringify(
                        data
                    )
            }
        );

    if (!response.ok) {

        throw new Error(
            `NUI request failed: ${response.status}`
        );
    }

    return response.json();
}

document
    .getElementById('close')
    .addEventListener(
        'click',
        async () => {

            try {

                await nuiRequest(
                    'close'
                );

            } catch (error) {

                console.error(
                    error
                );
            }
        }
    );

async function selectVehicle(
    vehicleId
) {

    try {

        const result =
            await nuiRequest(
                'selectVehicle',
                {
                    vehicleId
                }
            );

        if (!result.ok) {

            console.error(
                result.error
            );

            return;
        }

        console.log(
            'Vehicle request sent'
        );

    } catch (error) {

        console.error(
            error
        );
    }
}

ตรงนี้สร้าง Helper

nuiRequest()

เพื่อไม่ต้องเขียน fetch() ซ้ำทุก Callback

59 Checklist RegisterNuiCallback

ตรวจ

① Resource ใช้ fx_version cerulean หรือไม่?
② ui_page ถูกหรือไม่?
③ JavaScript อยู่ใน files หรือไม่?
④ Browser ใช้ https:// หรือไม่?
⑤ ใช้ GetParentResourceName หรือไม่?
⑥ Callback Name ตรงกันหรือไม่?
⑦ ใช้ RegisterNuiCallback สำหรับ Code ใหม่หรือไม่?
⑧ data เป็น JSON-encodable หรือไม่?
⑨ Browser POST JSON ถูกหรือไม่?
⑩ Content-Type ถูกหรือไม่?
⑪ data ถูก Validate หรือไม่?
⑫ Type ถูกตรวจหรือไม่?
⑬ Range ถูกตรวจหรือไม่?
⑭ cb ถูกเรียกหรือไม่?
⑮ Error Branch มี cb หรือไม่?
⑯ Success Branch มี cb หรือไม่?
⑰ Handler Error ก่อน cb หรือไม่?
⑱ Browser ใช้ try/catch หรือไม่?
⑲ Response Schema สม่ำเสมอหรือไม่?
⑳ NUI Input ถูกมองเป็น Untrusted หรือไม่?
㉑ Server Validate ซ้ำหรือไม่?
㉒ Client ส่ง Price/Permission ไป Server แบบเชื่อถือหรือไม่?
㉓ Generic SQL Event มีหรือไม่?
㉔ SetNuiFocus คืนตอนปิดหรือไม่?
㉕ Resource Stop คืน Focus หรือไม่?
㉖ Callback ถูกยิงถี่เกินไปหรือไม่?
㉗ Search/Slider มี Debounce หรือไม่?
㉘ F8 มี Error หรือไม่?
㉙ NUI DevTools Network มี Timeout หรือไม่?
㉚ Test หลัง Restart Resource แล้วหรือยัง?

⑥⓪ Architecture RegisterNuiCallback สำหรับ Production

สมมติระบบ Shop

NUI
│
├── buyItem
├── close
└── search

Client

RegisterNuiCallback
│
├── Validate UI payload
├── UI focus/state
└── Forward business request

Server

RegisterNetEvent / Callback
│
├── Validate source
├── Validate item
├── Check price
├── Check money
├── Check stock
├── Update inventory
└── Persist database

Database

Persistent state

Flow ซื้อสินค้า

Player คลิก Buy
↓
Browser fetch()
↓
RegisterNuiCallback
↓
Client ตรวจ Payload ขั้นต้น
↓
Server Request
↓
Server ตรวจ Item
↓
Server หา Price เอง
↓
Server ตรวจเงิน
↓
Server Update Inventory
↓
Server Result
↓
Client
↓
SendNUIMessage
↓
Browser แสดงผล

นี่เป็น Boundary ที่ปลอดภัยกว่าการให้ Browser เป็นผู้ตัดสินผลธุรกรรม

ตัวอย่าง Callback ที่ไม่ปลอดภัย

RegisterNuiCallback(
    'buyCar',
    function(data, cb)

        TriggerServerEvent(
            'garage:buyCar',
            data.model,
            data.price
        )

        cb({
            ok = true
        })
    end
)

ถ้า Server เชื่อ data.price

Player สามารถพยายามแก้ Price ใน Client ได้

Server ต้องรับเพียง Identifier ที่จำเป็น

เช่น

TriggerServerEvent(
    'garage:buyCar',
    data.model
)

แล้ว Server หา Price จาก Server-side Config เอง

Callback ที่ดีควรทำงานเล็กและชัดเจน

ไม่ควรมี Callback หนึ่งตัวทำทุกอย่าง

uiAction
→ buy
→ sell
→ delete
→ transfer
→ admin

ตามค่า data.action

แนะนำแยก

buyVehicle
sellVehicle
storeVehicle
transferVehicle

เพื่อให้อ่าน Security Boundary ง่าย

RegisterNuiCallback และการตั้งชื่อ

ตัวอย่างที่ดี

close
getVehicles
selectVehicle
storeVehicle
saveSettings
sendMessage

ชื่อควรสะท้อน Intent

หลีกเลี่ยง

doThing
action
callback1
test
execute

เพราะเมื่อ Resource ใหญ่จะ Debug ยาก

คำถามที่พบบ่อยเกี่ยวกับ FiveM RegisterNUICallback

FiveM RegisterNUICallback คืออะไร

ใช้รับ Request จาก NUI Browser เข้าสู่ Client Resource

RegisterNUICallback ยังใช้ได้ไหม

Cfx.re ระบุว่า API ชื่อเดิมยังคงไว้เพื่อ Backward Compatibility แต่ New Code ควรใช้ REGISTER_NUI_CALLBACK และ Lua Wrapper ปัจจุบัน RegisterNuiCallback

Code Lua ใหม่ควรเขียนอย่างไร

RegisterNuiCallback(
    'close',
    function(data, cb)

        cb({
            ok = true
        })
    end
)

NUI เรียก Callback อย่างไร

ผ่าน fetch()

fetch(
    `https://${GetParentResourceName()}/close`,
    ...
);

ใช้ http ได้ไหม

Resource cerulean ใช้ Secure-context NUI และ Callback URL ควรใช้ https://

data คืออะไร

Payload JSON ที่ Browser POST เข้ามา

cb คืออะไร

Function สำหรับส่ง Response กลับ Browser

ต้อง cb ทุกครั้งไหม

ใช่ ควรตอบทุก Request ไม่เช่นนั้น Request จะ Timeout

ไม่มีข้อมูลคืนใช้ cb อะไร

cb({})

หรือ

cb({
    ok = true
})

JavaScript ต้อง JSON.stringify ไหม

สำหรับ POST Object ตาม Pattern ปัจจุบันใช้ JSON.stringify(...)

GetParentResourceName คืออะไร

คืนชื่อ Resource ปัจจุบันให้ NUI Browser

Hardcode Resource Name ได้ไหม

ทำได้แต่ไม่แนะนำ เพราะ Rename Resource แล้ว Callback URL จะพัง

RegisterNuiCallback เป็น Server API ไหม

NUI Callback ใช้เชื่อม NUI กับ Client Runtime

ต้องไป Server ทำอย่างไร

Client ใช้ Network Event หรือ Server Callback Pattern ที่เหมาะสม

Client Validation พอไหม

ไม่ Server ต้อง Validate Input ที่มีผลต่อ Gameplay/Economy/Permission ซ้ำ

NUI ส่ง Price ให้ Server ได้ไหม

ส่งได้ในเชิงข้อมูล แต่ Serverไม่ควรเชื่อเป็น Authority ควรหา Price จริงเอง

NUI ส่ง Admin Permission ได้ไหม

ไม่ควรใช้เป็นหลักฐาน Permission

NUI ต่อ Database โดยตรงได้ไหม

ไม่ควร

NUI Callback ใช้กับ JavaScript Client ได้ไหม

ได้

ใช้กับ C# ได้ไหม

ได้

Callback Data ต้องเป็นแบบไหน

ต้อง JSON-encodable ตาม Cfx.re Documentation

Callback Return ต้อง JSON-encodable ไหม

ใช่ Data ที่ส่งกลับผ่าน cb ต้องอยู่ในรูปที่ NUI สามารถ Serialize/Parse ได้

fetch Timeout แก้อย่างไร

ตรวจ Callback Name, Client Error และโดยเฉพาะว่าทุก Code Path เรียก cb

UI ปิดแล้ว Cursor ค้างแก้อย่างไร

เรียก

SetNuiFocus(
    false,
    false
)

ใน Client Callback และ Cleanup ตอน Resource Stop

RegisterNuiCallback ต่างจาก SendNUIMessage อย่างไร

RegisterNuiCallback รับ Browser→Client ส่วน SendNUIMessage ส่ง Client→Browser

RegisterNuiCallback ต่างจาก RegisterNetEvent อย่างไร

NUI Callback เชื่อม Browser กับ Client ส่วน Network Event ใช้สื่อสารผ่าน FiveM Network Context

ใช้ Callback ทุก Mouse Move ได้ไหม

ทำได้แต่ไม่ควรหากไม่มีเหตุผล ควร Debounce/Throttle เพื่อ Performance

สรุป FiveM RegisterNUICallback คืออะไร ใช้อย่างไร

RegisterNUICallback/RegisterNuiCallback คือกลไกที่ทำให้ JavaScript ใน FiveM NUI ส่งข้อมูลกลับเข้าสู่ Client Script ได้

Flow หลักคือ

Browser
↓
fetch()
↓
https://resource/callbackName
↓
RegisterNuiCallback
↓
data
↓
Client Logic
↓
cb(...)
↓
Browser Response

ตัวอย่าง Browser

const response =
    await fetch(
        `https://${GetParentResourceName()}/close`,
        {
            method: 'POST',

            headers: {
                'Content-Type':
                    'application/json; charset=UTF-8'
            },

            body:
                JSON.stringify({})
        }
    );

const result =
    await response.json();

Client

RegisterNuiCallback(
    'close',
    function(data, cb)

        SetNuiFocus(
            false,
            false
        )

        cb({
            ok = true
        })
    end
)

สิ่งที่ต้องจำมากที่สุดมี 5 ข้อ

① Code ใหม่ใช้ RegisterNuiCallback/REGISTER_NUI_CALLBACK
② cerulean ใช้ https://
③ ใช้ GetParentResourceName()
④ data จาก NUI ต้องถือว่าไม่น่าเชื่อถือ
⑤ ต้องตอบ cb ทุก Request

หาก Callback ต้องทำ Business Logic สำคัญ เช่นซื้อรถหรือใช้ Item

Architecture ควรเป็น

NUI
↓
Client Callback
↓
Server
↓
Validate
↓
Execute
↓
Result
↓
Client
↓
NUI

ไม่ใช่

NUI
↓
บอกผลสำคัญ
↓
Server เชื่อทันที

สำหรับผู้ที่พัฒนา FiveM กับ comsiam จุดสำคัญของ NUI Callback ไม่ได้อยู่แค่ทำให้ Button กดแล้วทำงาน แต่ต้องทำให้ทุก Request มี Response, Input ถูก Validate และ Business Authority สำคัญไม่หลุดมาอยู่ฝั่ง Browser

หลักสำคัญจาก comsiam คือ NUI Callback เป็นสะพานระหว่าง Frontend กับ FiveM Client ไม่ใช่ช่องทางลัดข้าม Security Layer ของ Server เมื่อ Action มีผลต่อเงิน Inventory Vehicle Permission หรือ Database ให้ Server เป็นผู้ตัดสินเสมอ

หัวข้อถัดไปคือ FiveM SendNUIMessage และ SetNuiFocus ใช้อย่างไร ซึ่งจะเจาะทิศทาง Client → Browser โดยตรง ตั้งแต่การเปิด/ปิด UI, ส่ง Object/Array, อัปเดต HUD, Focus Keyboard/Mouse, Focus ค้าง, Resource Restart และวิธีลด NUI Message ที่ยิงถี่เกินไป

Comments

Popular posts from this blog

FiveM ยังน่าเล่นไหม? Enhanced เปลี่ยน FiveM แค่ไหน

FiveM คืออะไร เล่นอย่างไร สำหรับมือใหม่ เริ่มต้นตั้งแต่ศูนย์

วิธีตั้ง Admin Permission ด้วย add_ace และ add_principal FiveM แบบละเอียด