luau-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Luau Best Practices

Luau 最佳实践

Production-quality patterns for Roblox game development.
面向Roblox游戏开发的生产级代码模式。

Core Principles

核心原则

  1. Server Authority - Server owns game state; client is for presentation
  2. Fail Fast - Validate early, error loudly in development
  3. Explicit > Implicit - Clear intent beats clever code
  4. Minimal Surface Area - Expose only what's needed
  1. Server Authority(服务器权威) - 服务器掌控游戏状态,客户端仅负责展示
  2. 快速失败 - 尽早验证,开发阶段明确抛出错误
  3. 显式优于隐式 - 清晰的代码意图胜过“聪明”的代码
  4. 最小暴露面 - 仅暴露必要的内容

Code Style

代码风格

Naming Conventions

命名规范

lua
-- PascalCase: Types, Classes, Services, Modules
type PlayerData = { ... }
local ShopService = {}
local PlayerController = require(...)

-- camelCase: Variables, functions, methods
local playerCount = 0
local function getPlayerData() end
function ShopService:purchaseItem() end

-- SCREAMING_SNAKE_CASE: Constants
local MAX_PLAYERS = 50
local DEFAULT_HEALTH = 100

-- Private with underscore prefix
local function _validateInput() end
local _cache = {}
lua
-- PascalCase:类型、类、服务、模块
type PlayerData = { ... }
local ShopService = {}
local PlayerController = require(...)

-- camelCase:变量、函数、方法
local playerCount = 0
local function getPlayerData() end
function ShopService:purchaseItem() end

-- SCREAMING_SNAKE_CASE:常量
local MAX_PLAYERS = 50
local DEFAULT_HEALTH = 100

-- 以下划线开头表示私有
local function _validateInput() end
local _cache = {}

File Organization

文件组织

lua
--!strict

-- 1. Services/imports at top
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Signal = require(ReplicatedStorage.Packages.Signal)
local Types = require(script.Parent.Types)

-- 2. Constants
local MAX_RETRIES = 3
local TIMEOUT = 5

-- 3. Types
type Config = {
    enabled: boolean,
    maxItems: number,
}

-- 4. Module table
local MyModule = {}

-- 5. Private state
local _initialized = false
local _cache: { [string]: any } = {}

-- 6. Private functions
local function _helperFunction()
end

-- 7. Public API
function MyModule.init()
end

function MyModule.doSomething()
end

-- 8. Return
return MyModule
lua
--!strict

-- 1. 服务/导入放在顶部
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Signal = require(ReplicatedStorage.Packages.Signal)
local Types = require(script.Parent.Types)

-- 2. 常量定义
local MAX_RETRIES = 3
local TIMEOUT = 5

-- 3. 类型定义
type Config = {
    enabled: boolean,
    maxItems: number,
}

-- 4. 模块表
local MyModule = {}

-- 5. 私有状态
local _initialized = false
local _cache: { [string]: any } = {}

-- 6. 私有函数
local function _helperFunction()
end

-- 7. 公开API
function MyModule.init()
end

function MyModule.doSomething()
end

-- 8. 返回模块
return MyModule

Module Patterns

模块模式

Service Pattern (Server)

服务模式(服务器端)

lua
--!strict
local MyService = {}

local _started = false

function MyService:Start()
    assert(not _started, "MyService already started")
    _started = true
    -- Initialize connections, load data
end

function MyService:Stop()
    -- Cleanup for hot-reloading
end

return MyService
lua
--!strict
local MyService = {}

local _started = false

function MyService:Start()
    assert(not _started, "MyService already started")
    _started = true
    -- 初始化连接、加载数据
end

function MyService:Stop()
    -- 热重载时的清理逻辑
end

return MyService

Controller Pattern (Client)

控制器模式(客户端)

lua
--!strict
local MyController = {}

local _player = game:GetService("Players").LocalPlayer

function MyController:Init()
    -- Setup without yielding
end

function MyController:Start()
    -- Connect events, start loops
end

return MyController
lua
--!strict
local MyController = {}

local _player = game:GetService("Players").LocalPlayer

function MyController:Init()
    -- 无阻塞初始化
end

function MyController:Start()
    -- 连接事件、启动循环
end

return MyController

Lazy Initialization

延迟初始化

lua
local _data: PlayerData? = nil

local function getData(): PlayerData
    if not _data then
        _data = loadExpensiveData()
    end
    return _data
end
lua
local _data: PlayerData? = nil

local function getData(): PlayerData
    if not _data then
        _data = loadExpensiveData()
    end
    return _data
end

Error Handling

错误处理

Use pcall for External Calls

对外部调用使用pcall

lua
-- DataStore, HTTP, any Roblox API that can fail
local success, result = pcall(function()
    return dataStore:GetAsync(key)
end)

if not success then
    warn("DataStore failed:", result)
    return nil
end

return result
lua
-- DataStore、HTTP、任何可能失败的Roblox API
local success, result = pcall(function()
    return dataStore:GetAsync(key)
end)

if not success then
    warn("DataStore failed:", result)
    return nil
end

return result

Result Pattern

结果模式

lua
type Result<T> =
    { ok: true, value: T } |
    { ok: false, error: string }

local function fetchData(id: string): Result<Data>
    local success, data = pcall(function()
        return dataStore:GetAsync(id)
    end)

    if not success then
        return { ok = false, error = tostring(data) }
    end

    return { ok = true, value = data }
end
lua
type Result<T> =
    { ok: true, value: T } |
    { ok: false, error: string }

local function fetchData(id: string): Result<Data>
    local success, data = pcall(function()
        return dataStore:GetAsync(id)
    end)

    if not success then
        return { ok = false, error = tostring(data) }
    end

    return { ok = true, value = data }
end

Assert for Programming Errors

使用assert捕获编程错误

lua
-- Use assert for things that should never happen
function processPlayer(player: Player)
    assert(player, "player is required")
    assert(player:IsA("Player"), "expected Player instance")
    -- ...
end
See references/error-handling.md for comprehensive patterns.
lua
-- 对绝不应该发生的情况使用assert
function processPlayer(player: Player)
    assert(player, "player is required")
    assert(player:IsA("Player"), "expected Player instance")
    -- ...
end
查看 references/error-handling.md 获取完整的错误处理模式。

Memory Management

内存管理

Always Disconnect

务必断开连接

lua
local connection: RBXScriptConnection

connection = event:Connect(function()
    -- handler
end)

-- Later, cleanup:
connection:Disconnect()
lua
local connection: RBXScriptConnection

connection = event:Connect(function()
    -- 事件处理逻辑
end)

-- 后续清理:
connection:Disconnect()

Use Maids/Janitors

使用Maid/Janitor工具

lua
local Maid = require(Packages.Maid)

local maid = Maid.new()

maid:GiveTask(event:Connect(handler))
maid:GiveTask(instance)
maid:GiveTask(function()
    -- Custom cleanup
end)

-- Cleanup everything at once
maid:Destroy()
lua
local Maid = require(Packages.Maid)

local maid = Maid.new()

maid:GiveTask(event:Connect(handler))
maid:GiveTask(instance)
maid:GiveTask(function()
    -- 自定义清理逻辑
end)

-- 一次性清理所有资源
maid:Destroy()

Weak References for Caches

对缓存使用弱引用

lua
local cache = setmetatable({}, { __mode = "v" })

-- Values are garbage collected when no other references exist
cache[key] = expensiveObject
See references/memory.md for leak prevention patterns.
lua
local cache = setmetatable({}, { __mode = "v" })

-- 当没有其他引用时,值会被垃圾回收
cache[key] = expensiveObject
查看 references/memory.md 获取泄漏预防的完整模式。

Security Best Practices

安全最佳实践

Server Authority

服务器权威原则

lua
-- BAD: Client tells server what happened
RemoteEvent.OnServerEvent:Connect(function(player, damage)
    target.Health -= damage  -- Client controls damage!
end)

-- GOOD: Server calculates everything
RemoteEvent.OnServerEvent:Connect(function(player, targetId)
    local target = getValidTarget(player, targetId)
    if not target then return end

    local damage = calculateDamage(player)  -- Server calculates
    target.Health -= damage
end)
lua
-- 错误示例:客户端告知服务器发生了什么
RemoteEvent.OnServerEvent:Connect(function(player, damage)
    target.Health -= damage  -- 客户端可以随意控制伤害!
end)

-- 正确示例:服务器负责所有计算
RemoteEvent.OnServerEvent:Connect(function(player, targetId)
    local target = getValidTarget(player, targetId)
    if not target then return end

    local damage = calculateDamage(player)  -- 服务器计算伤害
    target.Health -= damage
end)

Validate All Input

验证所有输入

lua
RemoteFunction.OnServerInvoke = function(player, itemId, quantity)
    -- Type validation
    if typeof(itemId) ~= "string" then return end
    if typeof(quantity) ~= "number" then return end

    -- Range validation
    if quantity < 1 or quantity > 99 then return end
    if quantity ~= math.floor(quantity) then return end

    -- Business logic validation
    if not Items[itemId] then return end
    if not canAfford(player, itemId, quantity) then return end

    -- Now safe to process
    return purchaseItem(player, itemId, quantity)
end
lua
RemoteFunction.OnServerInvoke = function(player, itemId, quantity)
    -- 类型验证
    if typeof(itemId) ~= "string" then return end
    if typeof(quantity) ~= "number" then return end

    -- 范围验证
    if quantity < 1 or quantity > 99 then return end
    if quantity ~= math.floor(quantity) then return end

    -- 业务逻辑验证
    if not Items[itemId] then return end
    if not canAfford(player, itemId, quantity) then return end

    -- 此时处理请求是安全的
    return purchaseItem(player, itemId, quantity)
end)

Rate Limiting

速率限制

lua
local lastAction: { [Player]: number } = {}
local COOLDOWN = 0.5

local function isRateLimited(player: Player): boolean
    local now = os.clock()
    local last = lastAction[player] or 0

    if now - last < COOLDOWN then
        return true
    end

    lastAction[player] = now
    return false
end
See references/security.md for comprehensive security patterns.
lua
local lastAction: { [Player]: number } = {}
local COOLDOWN = 0.5

local function isRateLimited(player: Player): boolean
    local now = os.clock()
    local last = lastAction[player] or 0

    if now - last < COOLDOWN then
        return true
    end

    lastAction[player] = now
    return false
end
查看 references/security.md 获取完整的安全模式。

Common Anti-Patterns

常见反模式

Avoid

应避免

lua
-- Using wait() - use task.wait()
wait(1)  -- BAD
task.wait(1)  -- GOOD

-- spawn() - use task.spawn()
spawn(fn)  -- BAD
task.spawn(fn)  -- GOOD

-- delay() - use task.delay()
delay(1, fn)  -- BAD
task.delay(1, fn)  -- GOOD

-- Polling when events exist
while true do
    if something then break end
    task.wait()
end
-- GOOD: Use events/signals instead

-- String concatenation in loops
local s = ""
for i = 1, 1000 do
    s = s .. tostring(i)  -- O(n²)
end
-- GOOD: Use table.concat

-- FindFirstChild chains
workspace.Folder.SubFolder.Part  -- Errors if missing
-- GOOD: Safe navigation
local folder = workspace:FindFirstChild("Folder")
local part = folder and folder:FindFirstChild("SubFolder")
    and folder.SubFolder:FindFirstChild("Part")
lua
-- 使用wait() - 改用task.wait()
wait(1)  -- 错误用法
task.wait(1)  -- 正确用法

-- spawn() - 改用task.spawn()
spawn(fn)  -- 错误用法
task.spawn(fn)  -- 正确用法

-- delay() - 改用task.delay()
delay(1, fn)  -- 错误用法
task.delay(1, fn)  -- 正确用法

-- 已有事件时仍使用轮询
while true do
    if something then break end
    task.wait()
end
-- 正确用法:使用事件/信号替代

-- 循环中拼接字符串
local s = ""
for i = 1, 1000 do
    s = s .. tostring(i)  -- 时间复杂度O(n²)
end
-- 正确用法:使用table.concat

-- 链式调用FindFirstChild
workspace.Folder.SubFolder.Part  -- 若中间节点不存在会报错
-- 正确用法:安全导航
local folder = workspace:FindFirstChild("Folder")
local part = folder and folder:FindFirstChild("SubFolder")
    and folder.SubFolder:FindFirstChild("Part")

Prefer

推荐用法

lua
-- Generalized iteration
for _, v in ipairs(array) do end  -- OLD
for _, v in array do end  -- MODERN (Luau)

-- If expressions
local x = if condition then a else b  -- Clean ternary

-- Continue in loops
for _, item in items do
    if not item.valid then continue end
    process(item)
end

-- Optional chaining with and
local name = player and player.Character and player.Character.Name
lua
-- 通用迭代
for _, v in ipairs(array) do end  -- 旧写法
for _, v in array do end  -- 现代写法(Luau)

-- 条件表达式
local x = if condition then a else b  -- 简洁的三元表达式

-- 循环中使用continue
for _, item in items do
    if not item.valid then continue end
    process(item)
end

-- 使用and进行可选链式调用
local name = player and player.Character and player.Character.Name

Project Structure

项目结构

src/
├── Server/
│   ├── init.server.luau      # Bootstrap
│   ├── Services/             # Game services
│   │   ├── DataService.luau
│   │   └── CombatService.luau
│   └── Components/           # Server components
├── Client/
│   ├── init.client.luau      # Bootstrap
│   ├── Controllers/          # Client controllers
│   └── UI/                   # UI components
├── Shared/
│   ├── Types.luau            # Shared type definitions
│   ├── Constants.luau        # Shared constants
│   └── Util/                 # Shared utilities
└── Packages/                 # Wally packages
src/
├── Server/
│   ├── init.server.luau      # 启动文件
│   ├── Services/             # 游戏服务
│   │   ├── DataService.luau
│   │   └── CombatService.luau
│   └── Components/           # 服务器组件
├── Client/
│   ├── init.client.luau      # 启动文件
│   ├── Controllers/          # 客户端控制器
│   └── UI/                   # UI组件
├── Shared/
│   ├── Types.luau            # 共享类型定义
│   ├── Constants.luau        # 共享常量
│   └── Util/                 # 共享工具类
└── Packages/                 # Wally包

Quick Reference

快速参考

DoDon't
task.wait()
wait()
task.spawn()
spawn()
task.delay()
delay()
for _, v in t
for _, v in pairs(t)
Validate on serverTrust client data
Use typesUse
any
everywhere
Disconnect eventsLeave connections dangling
Use constantsMagic numbers/strings
Early returnDeep nesting
Small functions200+ line functions
推荐做法避免做法
task.wait()
wait()
task.spawn()
spawn()
task.delay()
delay()
for _, v in t
for _, v in pairs(t)
在服务器端验证信任客户端数据
使用类型定义到处使用
any
断开事件连接让连接悬空
使用常量魔法数字/字符串
提前返回深层嵌套
小函数超过200行的函数

References

参考文档

  • Code Style Guide - Naming, formatting, organization
  • Common Patterns - Services, signals, state management
  • Error Handling - pcall, Result types, retries
  • Memory Management - Cleanup, leaks, weak tables
  • Security - Server authority, validation, anti-exploit
  • Code Style Guide - 命名、格式化、组织方式
  • Common Patterns - 服务、信号、状态管理
  • Error Handling - pcall、Result类型、重试
  • Memory Management - 清理、泄漏、弱表
  • Security - 服务器权威、验证、反作弊