返回首页

教程导航

返回首页

🏷️ 标签(1)

🗂️ 分类(1)

第 10 章 模块与包

第 10 章 模块与包

Lua 没有单独的命名空间语法。模块通常是一个返回表的文件,使用 require 加载。理解模块缓存与搜索路径,是构建可维护项目的前提。

require 与模块表

创建 math_utils.lua:

local M = {}

function M.clamp(value, minimum, maximum)
  return math.max(minimum, math.min(maximum, value))
end

return M

使用模块:

local mathUtils = require("math_utils")
print(mathUtils.clamp(12, 0, 10))

require 返回模块表,并把它缓存到 package.loaded:

local first = require("math_utils")
local second = require("math_utils")

print(first == second) -- true

模块返回值的约定

推荐每个模块只返回一个表,并把公开接口放在该表上:

local M = {}

local function normalize(value)
  return value:match("^%s*(.-)%s*$")
end

function M.parseName(value)
  value = normalize(value)
  if value == "" then
    return nil, "name is empty"
  end
  return value
end

return M

不要在模块加载时产生外部副作用,例如删除文件、连接数据库或修改全局状态。模块应该可重复加载、可测试。

包搜索路径

查看当前搜索路径:

print(package.path)

临时添加项目目录:

package.path = "./src/?.lua;./src/?/init.lua;" .. package.path

模块名中的点对应目录分隔符。例如 require("app.config") 会查找 app/config.lua 或 app/config/init.lua,具体取决于搜索模板。

避免修改全局搜索路径

长期修改 package.path 会影响整个进程。应用入口可以在启动阶段设置一次,并把路径构建逻辑封装起来:

local function addProjectPath(root)
  package.path = table.concat({
    root .. "/?.lua",
    root .. "/?/init.lua",
    package.path
  }, ";")
end

模块拆分与依赖管理

建议按职责拆分模块:

src/
├── app.lua
├── config.lua
├── storage.lua
├── commands/
│   ├── add.lua
│   └── list.lua
└── util.lua

依赖方向应尽量单向。底层工具不应依赖命令行模块,业务模块不应直接读取全局配置。

依赖注入

把依赖作为参数传入,比在模块内部硬编码 require 更容易测试:

local function createService(storage, clock)
  return {
    create = function(value)
      return storage.save({
        value = value,
        createdAt = clock.now()
      })
    end
  }
end

编写可复用模块

一个好的 Lua 模块通常具备以下特征:

  • 只返回一个明确的模块表。

  • 不污染 _G。

  • 不依赖加载顺序。

  • 输入不合法时返回 nil, error 或抛出明确的错误对象。

  • 可以在不触碰真实文件系统的情况下测试。

版本兼容

如果库需要同时支持 Lua 5.1 到 5.4,避免直接使用新语法和函数,或者通过能力探测提供降级:

local unpackValues = table.unpack or unpack

明确支持范围比“看起来兼容”更重要。项目文档应记录最低 Lua 版本和已测试版本。

上一章:字符串与模式匹配 | 下一章:元表与元方法