导入与导出

Lux 的导入导出是编译期模块系统,不是 Lua 运行时 require。编译器会先解析完整 模块图,再由目标后端生成加载代码。对 GMod 后端来说,导入通常会变成对编译器生成 的模块注册表读取,而不是直接调用 require

导入形式

命名导入会从目标模块的导出表中取出公开名,并绑定到当前 part 的局部变量。

命名导入和别名
import { arr } from "@lux/std"
import { dict as tableDict } from "@lux/std"

local names = arr.map(players, (player) => player:Nick())
local merged = tableDict.merge(defaults, overrides)

命名空间导入绑定的是整个导出表。

命名空间导入
import * as std from "@lux/std"

local names = std.arr.map(players, (player) => player:Nick())

副作用导入不创建本地绑定,只保证目标模块进入模块图并按顺序执行。

副作用导入
import "@lux/ui/register_defaults"

宏导入只在编译期可用,不会进入运行时模块图。

宏导入
import macro { dbg } from "@lux/macros"
import macro * as gmodMacros from "@lux/gmod/macros"

路径解析

相对路径引用项目模块,以 @ 开头的路径引用包。

项目模块和包模块
import { formatItem } from "../inventory"
import { theme } from "./theme"
import { valid } from "@lux/gmod"

实际模块 id 由项目 source_root、当前模块路径、包根和后端规则共同决定。文档中的 Lua 只展示形态,不要求手写这些 id。

导入绑定是不可变的

导入后得到的是当前 part 的局部绑定,并且不可重新赋值。

导入绑定不可赋值
import { arr } from "@lux/std"

arr = {}

如果要在多个 part 使用同一个包,每个 part 自己写导入。导入不会自动变成模块级 共享绑定。

导出是公开名映射

顶层声明默认私有。导出不是“取消 private”,而是从内部绑定到外部 API 名称的显式 映射。

导出同名公开 API
local player_inventory = {}

export { player_inventory }

外部模块可以导入这个公开名。

导入公开名
import { player_inventory } from "../inventory"

使用别名时,公开名发生变化。export { public_name = local_binding } 的左侧是外部 可见名称,右侧是模块内部绑定。

Lua 风格导出别名
local player_inventory = {}

export { p_inv = player_inventory }

外部只能使用公开名 p_inv,不能再导入内部名 player_inventory

导入别名后的公开名
import { p_inv } from "../inventory"
import { p_inv as inventory } from "../inventory"

Lux 也支持接近常见模块系统的别名方向:

as 风格导出别名
export { player_inventory as p_inv }

两种写法等价:

export { public_name = local_binding }
export { local_binding as public_name }

文档和团队规范推荐固定使用一种风格,避免同一项目中混用导致误读。

声明时导出

可以在声明时导出简单名字。export fn 先创建普通模块私有绑定,再把它映射到同名 公开 API。

声明时导出
export fn createInventory(owner) {
  { owner = owner, items = {} }
}

export const maxSlots = 64

GLua 风格的点号/冒号声明不能用 export fn,因为它不是模块私有绑定,而是对已有 表路径的副作用声明。

不能导出方法路径声明
export fn PANEL:Paint(w, h) {}

运行域导出

导出可以带运行域。导出的运行域决定外部模块在哪些产物中能看见这个 API。

运行域导出
server fn grantItem(player, item) {
  inventory[player:SteamID64()] = item
}

client fn openPanel() {
  vgui.Create("DFrame")
}

shared fn itemName(item) =
  item.name ?? "Unknown"

export server { grantItem }
export client { openPanel }
export shared { itemName }

声明时也可以写运行域导出。

声明时运行域导出
export server fn grantItem(player, item) {
  inventory[player:SteamID64()] = item
}

export client fn openPanel() {
  vgui.Create("DFrame")
}

如果导出没有显式运行域,默认使用绑定的有效运行域。sv_ 文件中的 export fn 默认是服务端导出;cl_ 文件中默认是客户端导出;共享文件中默认是共享导出。

只能收窄,不能扩大

导出的运行域必须是绑定可用运行域的子集。

server-only 不能导出成 shared
server fn grantItem(player, item) {
  inventory[player:SteamID64()] = item
}

export shared { grantItem }
export client { grantItem }
export server { grantItem }

共享绑定可以收窄导出。这表示函数本身可在共享代码中使用,但对外只作为服务端 API 暴露。

shared 绑定可收窄导出
shared fn normalizeName(value) =
  string.Trim(value ?? "")

export server { normalizeName }

导出限制

MVP 规则:

  • 只能导出模块作用域绑定。
  • 不能导出从其他模块导入的绑定。
  • 公开名参与重复导出检查。
  • 运行域导出不能扩大绑定的实际可用运行域。
  • 未通过公开名导出的内部名保持私有。

不能直接转导入绑定。

不能转导入绑定
import { arr } from "@lux/std"

export { arr }

需要转发 API 时,先定义自己的包装。

用包装函数转发 API
import { arr } from "@lux/std"

fn mapItems(items, callback) =
  arr.map(items, callback)

export { mapItems }

public all

public all 是显式选择“全部公开”的模块级配置,适合非常小的工具模块。

public all
public all

fn clamp(value, min, max) {
  value < min then min else value > max then max else value
}

fn lerp(t, from, to) {
  from + (to - from) * t
}

大型模块不要依赖 public all。默认私有、按名称导出更利于维护 API 边界。