---
title: "项目结构 — zenit Zig UI 文档"
description: "把构建入口、业务界面、样式和资源放到可持续扩展的位置，并弄清每块内存归谁所有。"
url: https://zenit.z.express/zh/docs/guide/project-structure
language: zh-CN
alternate_en: https://zenit.z.express/docs/guide/project-structure.md
alternate_es: https://zenit.z.express/es/docs/guide/project-structure.md
alternate_ja: https://zenit.z.express/ja/docs/guide/project-structure.md
alternate_ko: https://zenit.z.express/ko/docs/guide/project-structure.md
alternate_fr: https://zenit.z.express/fr/docs/guide/project-structure.md
alternate_de: https://zenit.z.express/de/docs/guide/project-structure.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 项目结构

把构建入口、业务界面、样式和资源放到可持续扩展的位置，并弄清每块内存归谁所有。

## 推荐结构

zenit 不强制应用目录。推荐让构建文件保持稳定，把每个功能的树结构、状态与样式放在同一个目录里，改一个功能时不必在仓库里来回跳。

`myapp/`

```
myapp/
├── build.zig
├── build.zig.zon
└── src/
    ├── main.zig
    ├── app_state.zig
    ├── assets.zig
    └── features/
        └── dashboard/
            ├── view.zig
            ├── state.zig
            └── styles.zig
```

## 文件职责

| 文件 | 负责 | 避免 |
| --- | --- | --- |
| `main.zig` | Allocator、App 初始化、根挂载 | 堆积具体页面 UI |
| `view.zig` | 节点树与事件连接 | 裸颜色、复杂数据变换 |
| `state.zig` | 业务状态与方法 | 长期持有可能被移除或重建的 Node 指针 |
| `styles.zig` | Token 驱动的具名样式函数 `fn (*const ui.ThemeTokens) ui.BoxStyle` | 发起 IO 或改变状态 |

样式函数是纯函数，由 `ui.boxStyled` / `ui.vstackStyled` 等构造器在挂载时求值；切换主题时框架会用新 Token 重放它们，内联字面量样式则不会。

`styles.zig + view.zig`

```zig
// features/dashboard/styles.zig
const ui = @import("ui");

pub fn card(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{
        .padding = ui.Padding.all(t.space._6),
        .gap = t.space._4,
        .background = t.color.bg_secondary,
        .corner_radius = t.radius.xl,
    };
}

// features/dashboard/view.zig
const S = @import("styles.zig");
const panel = try ui.vstackStyled(cx, S.card, .{});
```

## 导入边界

公开表面分两层：常用类型与构造器位于 `ui.X`，进阶能力按关注点位于 `ui.<group>.X`（`ui.widgets`、`ui.fx`、`ui.events`、`ui.theme` 等）。两层之外的一切都是内部实现。

```zig
const ui = @import("ui");

// Tier 1: what most UI code needs
const Node = ui.Node;
const Signal = ui.Signal;

// Tier 2: grouped by concern
const Router = ui.fx.Router;
const Button = ui.widgets.Button;
const Event = ui.events.Event;
```

这条边界由构建系统保证：`zenit.attach` 只给应用加上 `ui` 与 `zenit_app` 两个模块，`render`、`gpu` 等内部模块在应用里根本导入不到（架构图见[手册首页](https://zenit.z.express/zh/docs#architecture)）。

> WARNING
> 
> **API 尚未到 1.0。** zenit 从 0.1.0 起公开发布并遵循语义化版本，但 1.0 之前 minor 版本仍可能有破坏性变更（逐条记录在 docs/MIGRATION.md）。应用应固定到明确的版本或 revision，不要跟随分支最新提交。

## 所有权与生命周期

两个所有者，两种寿命。`Cx` 是窗口级的：它持有节点树和 `cx.bindState` 分配的结构体，直到 `Cx.deinit`。`Scope` 持有响应式资源：Signal、Memo、Effect、`onCleanup` 回调和登记的资源，在 `dispose()` 时释放；子 Scope 随父 Scope 一起释放。

OWNERSHIP

销毁页面 Scope 不会释放你在该页面里 `bindState` 的结构体——它们活到窗口关闭。`Cx.deinit` 的顺序是：dispose 根 Scope → 释放节点 → 释放 state。

### 绑定状态：声明 pub deinit 即可

如果结构体声明了 **pub** `deinit(*T)`，框架会在 `Cx.deinit` 释放它之前自动调用。需要 allocator 的清理，把 allocator 存成字段。

`state.zig`

```zig
const Item = struct { title: []const u8 };

const Model = struct {
    allocator: std.mem.Allocator,
    items: std.ArrayList(Item) = .empty,

    // Must be pub: Cx.deinit detects it and calls it before freeing the struct.
    pub fn deinit(self: *Model) void {
        self.items.deinit(self.allocator);
    }
};

const model = try cx.bindState(Model, .{ .allocator = cx.allocator });
// Do NOT also call scope.onCleanup(Model, model, Model.deinit):
// that would run deinit twice.
```

> WARNING
> 
> **不要重复清理。** 对同一个 `bindState` 结构体既声明 pub `deinit` 又注册 `scope.onCleanup(…, T.deinit)`，会在页面 dispose 与 `Cx.deinit` 时各执行一次 deinit。非 pub 的 `deinit` 不会被检测到，也就不会被自动调用。

### 页面级资源：用 onCleanup

只有两种情况需要 `scope.onCleanup`：清理函数不是 pub，或者资源必须跟随某个 Scope（例如一个页面）而不是整个窗口释放。这类对象自己分配，不经过 `bindState`。

`features/dashboard/view.zig`

```zig
const PageCache = struct {
    allocator: std.mem.Allocator,
    rows: std.ArrayList(Item) = .empty,

    fn release(self: *PageCache) void {
        self.rows.deinit(self.allocator);
        self.allocator.destroy(self);
    }
};

fn mountDashboard(cx: *ui.Cx, parent: *ui.Scope) !*ui.Node {
    const scope = try parent.childScope();

    // Page-lifetime resource: freed when this scope is disposed,
    // not when the window closes.
    const cache = try cx.allocator.create(PageCache);
    cache.* = .{ .allocator = cx.allocator };
    scope.onCleanup(PageCache, cache, PageCache.release) catch |err| {
        cx.allocator.destroy(cache);
        return err;
    };

    return ui.vstack(cx, .{ .gap = cx.tokens.space._4 }, .{});
}
```

## 检查清单

-   **main.zig 保持短小。**只做 allocator、`App.init` 与 `runWith`。
    
-   **只导入 ui 与 zenit\_app。**需要的能力不在两层公开表面上时，先提 issue，而不是绕进内部目录。
    
-   **窗口寿命用 bindState，页面寿命用 Scope。**前者声明 pub `deinit`，后者用 `onCleanup`，二者不要叠加。
    
-   **样式写成具名函数。**放进 styles.zig，由 `*Styled` 构造器消费，主题切换自动生效。
