强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

18 - 实战案例 / Real-World Projects

实战案例 / Real-World Projects

理论学完了,看看 Lua 在真实场景中的用法。本章覆盖:脚本任务、OpenResty Web 开发、Redis 脚本、游戏引擎脚本、自定义 DSL。

Theory is learned — let’s see Lua in action. This chapter covers scripting tasks, OpenResty web dev, Redis scripting, game engine scripting, and custom DSLs.


🟢 基础 / Basics — 日常脚本任务

1. 批量文件处理 / Batch File Processing

#!/usr/bin/env lua
-- rename_files.lua — 批量重命名文件
-- Usage: lua rename_files.lua /path/to/dir .jpeg .jpg

local dir = arg[1] or "."
local from_ext = arg[2] or ".jpeg"
local to_ext = arg[3] or ".jpg"

local function scandir(directory)
    local files = {}
    local pipe = io.popen('ls -1 "' .. directory .. '" 2>/dev/null')
    if pipe then
        for file in pipe:lines() do
            files[#files + 1] = file
        end
        pipe:close()
    end
    return files
end

local count = 0
for _, file in ipairs(scandir(dir)) do
    if file:sub(-#from_ext) == from_ext then
        local newname = file:sub(1, -#from_ext - 1) .. to_ext
        local old = dir .. "/" .. file
        local new = dir .. "/" .. newname
        os.rename(old, new)
        print(string.format("  %s → %s", file, newname))
        count = count + 1
    end
end
print(string.format("Renamed %d files", count))

2. JSON 处理 / JSON Processing

-- 使用 cjson 库(OpenResty 内置,或独立安装)
local cjson = require("cjson")

-- 编码
local data = {
    name = "Alice",
    age = 30,
    scores = {95, 87, 92},
    active = true,
}
local json_str = cjson.encode(data)
print(json_str)
-- {"name":"Alice","age":30,"scores":[95,87,92],"active":true}

-- 解码
local obj = cjson.decode(json_str)
print(obj.name)       -- Alice
print(obj.scores[1])  -- 95

-- 处理 null
cjson.decode_null_as_null(true)  -- 让 JSON null 返回 cjson.null
local result = cjson.decode('{"x": null}')
print(result.x == cjson.null)    -- true

-- 错误处理
local ok, result = pcall(cjson.decode, "invalid json")
if not ok then
    print("JSON parse error: " .. result)
end

3. 配置文件解析 / Configuration Parsing

-- config.lua — 用 Lua 作为配置文件
return {
    server = {
        host = os.getenv("APP_HOST") or "0.0.0.0",
        port = tonumber(os.getenv("APP_PORT")) or 8080,
    },
    database = {
        url = os.getenv("DATABASE_URL") or "sqlite://data.db",
        pool_size = 10,
    },
    features = {
        dark_mode = true,
        beta_features = false,
    },
}
-- main.lua — 加载配置
local config = require("config")
print(config.server.host)
print(config.database.url)

🟡 进阶 / Intermediate — OpenResty & Redis

1. OpenResty 请求处理 / OpenResty Request Handling

-- nginx.conf 中的 Lua 处理
-- location /api {
--     content_by_lua_block {
--         require("api_handler").handle()
--     }
-- }

-- api_handler.lua
local ngx = require("ngx")
local cjson = require("cjson.safe")

local _M = {}

function _M.handle()
    local method = ngx.req.get_method()
    local uri = ngx.var.uri

    -- 路由分发
    if method == "GET" and uri == "/api/health" then
        return _M.health()
    elseif method == "POST" and uri == "/api/users" then
        return _M.createUser()
    else
        ngx.status = 404
        ngx.say(cjson.encode({error = "Not Found"}))
    end
end

function _M.health()
    ngx.header.content_type = "application/json"
    ngx.say(cjson.encode({
        status = "ok",
        timestamp = ngx.now(),
    }))
end

function _M.createUser()
    ngx.req.read_body()
    local body = ngx.req.get_body_data()
    local data, err = cjson.decode(body)
    
    if not data or not data.name then
        ngx.status = 400
        return ngx.say(cjson.encode({error = "Invalid request"}))
    end
    
    -- 存储到 Redis
    local redis = require("resty.redis")
    local red = redis:new()
    red:connect("127.0.0.1", 6379)
    red:set("user:" .. data.name, cjson.encode(data))
    red:close()
    
    ngx.status = 201
    ngx.header.content_type = "application/json"
    ngx.say(cjson.encode({created = true, name = data.name}))
end

return _M

2. OpenResty 中间件 / OpenResty Middleware

-- rate_limiter.lua — 基于 Redis 的限流器
local ngx = require("ngx")
local redis = require("resty.redis")

local _M = {}

function _M.limit(key, max_requests, window_seconds)
    local red = redis:new()
    red:connect("127.0.0.1", 6379)
    
    local current = red:incr(key)
    if current == 1 then
        red:expire(key, window_seconds)
    end
    
    red:close()
    
    if current > max_requests then
        ngx.status = 429
        ngx.header["Retry-After"] = window_seconds
        ngx.say('{"error": "Rate limit exceeded"}')
        return ngx.exit(429)
    end
end

return _M

-- nginx.conf 中使用:
-- access_by_lua_block {
--     local limiter = require("rate_limiter")
--     local key = "rate:" .. ngx.var.remote_addr
--     limiter.limit(key, 100, 60)  -- 100 次/分钟
-- }

3. Redis Lua 脚本 / Redis Lua Scripting

-- Redis 使用 Lua 5.1 脚本(EVAL 命令)
-- 脚本在 Redis 服务器中原子执行

-- 示例一:原子递增并检查
local script = [[
local current = redis.call('GET', KEYS[1])
if not current then current = 0 end
current = tonumber(current) + tonumber(ARGV[1])
if current > tonumber(ARGV[2]) then
    return redis.error_reply('Limit exceeded')
end
redis.call('SET', KEYS[1], current)
redis.call('EXPIRE', KEYS[1], ARGV[3])
return current
]]

-- Redis CLI 执行:
-- EVAL "script" 1 counter 5 100 3600
-- 参数:script, numkeys, key1, arg1, arg2, arg3

-- 示例二:分布式锁
local lock_script = [[
if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
    return 1
else
    return 0
end
]]

-- 示例三:限流(令牌桶)
local rate_limit_script = [[
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)

if current and tonumber(current) >= limit then
    return 0  -- 拒绝
end

current = redis.call('INCR', key)
if current == 1 then
    redis.call('EXPIRE', key, window)
end
return 1  -- 允许
]]

🔴 高级 / Advanced — 游戏脚本与 DSL

1. 游戏引擎中的 Lua 脚本 / Game Engine Scripting

-- 游戏 AI 状态机
local StateMachine = {}
StateMachine.__index = StateMachine

function StateMachine.new(entity)
    return setmetatable({
        entity = entity,
        states = {},
        current = nil,
    }, StateMachine)
end

function StateMachine:addState(name, state)
    self.states[name] = state
    state.machine = self
    state.entity = self.entity
end

function StateMachine:switch(name, ...)
    if self.current and self.states[self.current].exit then
        self.states[self.current].exit(self.entity)
    end
    self.current = name
    if self.states[name].enter then
        self.states[name].enter(self.entity, ...)
    end
end

function StateMachine:update(dt)
    if self.current and self.states[self.current].update then
        self.states[self.current].update(self.entity, dt)
    end
end

-- NPC AI 状态
local idleState = {
    enter = function(entity)
        entity.animation = "idle"
    end,
    update = function(entity, dt)
        local dist = entity:distanceTo(entity.target)
        if dist < entity.aggroRange then
            entity.machine:switch("chase")
        end
    end,
}

local chaseState = {
    enter = function(entity)
        entity.animation = "run"
        entity.speed = entity.baseSpeed * 1.5
    end,
    update = function(entity, dt)
        local dist = entity:distanceTo(entity.target)
        if dist > entity.aggroRange * 2 then
            entity.machine:switch("idle")
        elseif dist < entity.attackRange then
            entity.machine:switch("attack")
        else
            entity:moveToward(entity.target, dt)
        end
    end,
}

local attackState = {
    enter = function(entity)
        entity.animation = "attack"
        entity.attackTimer = 0
    end,
    update = function(entity, dt)
        entity.attackTimer = entity.attackTimer + dt
        if entity.attackTimer >= entity.attackCooldown then
            entity:performAttack()
            entity.attackTimer = 0
        end
        local dist = entity:distanceTo(entity.target)
        if dist > entity.attackRange then
            entity.machine:switch("chase")
        end
    end,
}

-- 创建 NPC
local npc = createNPC({name = "Goblin", hp = 50})
npc.machine = StateMachine.new(npc)
npc.machine:addState("idle", idleState)
npc.machine:addState("chase", chaseState)
npc.machine:addState("attack", attackState)
npc.machine:switch("idle")

2. 自定义 DSL / Custom DSL

-- 用 Lua 的语法特性构建 DSL
-- 例如:任务描述 DSL

local Task = {}
Task.__index = Task

function Task.new(name)
    return setmetatable({
        name = name,
        steps = {},
        rewards = {},
        requirements = {},
    }, Task)
end

-- DSL 魔法:利用 Lua 的语法创建声明式 API
local function quest(name, body)
    local q = Task.new(name)
    local env = {
        step = function(desc, fn)
            q.steps[#q.steps + 1] = {desc = desc, action = fn}
        end,
        reward = function(item, count)
            q.rewards[#q.rewards + 1] = {item = item, count = count or 1}
        end,
        require_level = function(level)
            q.requirements[#q.requirements + 1] = {type = "level", value = level}
        end,
        require_item = function(item)
            q.requirements[#q.requirements + 1] = {type = "item", value = item}
        end,
    }
    setmetatable(env, {__index = _G})
    setfenv(body, env)    -- Lua 5.1
    body()
    return q
end

-- 使用 DSL 定义任务
local q = quest("Kill the Dragon", function()
    require_level(10)
    require_item("Sword of Light")
    
    step("Travel to Dragon Mountain", function(player)
        player:moveTo("dragon_mountain")
    end)
    
    step("Defeat the Dragon", function(player)
        local dragon = spawnMob("Dragon", {hp = 1000})
        player:combat(dragon)
    end)
    
    step("Collect Dragon Scale", function(player)
        player:loot("Dragon Scale", 1)
    end)
    
    reward("Gold", 1000)
    reward("Dragon Scale Armor", 1)
    reward("Experience", 5000)
end)

-- 任务系统可以遍历 q.steps、q.rewards、q.requirements

3. 热更新系统 / Hot-Reload System

-- 游戏中的热更新:不重启游戏,只重新加载脚本
local HotReload = {}

local loaded = {}    -- 已加载的模块
local callbacks = {} -- 热更新回调

function HotReload.watch(moduleName, callback)
    callbacks[moduleName] = callback
end

function HotReload.reload(moduleName)
    -- 清除缓存
    package.loaded[moduleName] = nil
    
    -- 重新加载
    local ok, newModule = pcall(require, moduleName)
    if not ok then
        print("Reload failed: " .. tostring(newModule))
        return false
    end
    
    -- 如果旧模块有状态,迁移状态
    if loaded[moduleName] and callbacks[moduleName] then
        callbacks[moduleName](loaded[moduleName], newModule)
    end
    
    loaded[moduleName] = newModule
    print("Reloaded: " .. moduleName)
    return true
end

-- 使用
HotReload.watch("game.player", function(old, new)
    -- 迁移玩家状态
    new.restoreState(old.getState())
end)

-- 文件系统监控触发重载(需要 inotify 或轮询)
local function checkForChanges()
    -- 检查文件修改时间
    local modules = {"game.player", "game.enemy", "game.items"}
    for _, mod in ipairs(modules) do
        local path = mod:gsub("%.", "/") .. ".lua"
        local f = io.popen("stat -c %Y " .. path)
        local mtime = tonumber(f:read("*a"))
        f:close()
        if mtime and mtime > (lastModified[mod] or 0) then
            HotReload.reload(mod)
            lastModified[mod] = mtime
        end
    end
end

小结 / Summary

层级你需要知道的 / What You Need to Know
🟢 基础文件批处理、JSON 解析、Lua 作为配置文件
🟡 进阶OpenResty 请求处理/中间件、Redis Lua 脚本原子操作
🔴 高级游戏状态机 AI、自定义 DSL、热更新系统

结语 / Conclusion

恭喜你完成了 Lua 从入门到精通的全部学习!回顾一下:

Congratulations on completing the entire Lua tutorial! Here’s a recap:

基础篇 / Fundamentals          进阶篇 / Intermediate         高级篇 / Advanced
├─ 语法基础                    ├─ 类型系统深挖                ├─ 字节码与 VM
├─ 数据类型                    ├─ 模式匹配                    ├─ 元表内部机制
├─ 控制流程                    ├─ 闭包与 Upvalue              ├─ GC 算法
├─ 函数                        ├─ OOP 模式                    ├─ C API 栈操作
├─ 表                          ├─ 模块系统                    ├─ LuaJIT 与 FFI
└─ 字符串                      ├─ 协程                        └─ DSL 设计
                               ├─ 错误处理
                               ├─ I/O 操作
                               └─ 调试与性能

下一步建议 / Next Steps:

  1. 🎮 尝试用 Lua 写一个小游戏(推荐 LÖVE 2D
  2. 🌐 用 OpenResty 搭建一个 API 服务
  3. 📝 给 Redis 写一个 Lua 脚本
  4. 🔧 阅读 Lua 源码,真正理解虚拟机
  5. 📖 阅读 Programming in Lua(Lua 之父的著作)

Happy coding!