第 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 版本和已测试版本。