nsgo 是做什麼的?
一個 Go UI 引擎:應用邏輯、反應式圖形、樣式、佈局和文字在 Go 中執行;每個平台使用其自身的原生視圖進行繪製。應用程式使用 Go 語言(或編譯為 goja 的 TSX)編寫。設計、名稱和行為都移植自 NSH (Rust)。
鐵律:閱讀 API,絕不猜測
NSGo 處於預發布階段,沒有穩定的 API,網路上幾乎沒有公開文件,因此回憶起的識別碼幾乎總是錯的。Go 語言會因為一個錯誤的欄位名稱而無法編譯。在編寫應用程式之前,請從模組本身獲取真實資訊:
go doc github.com/oneo-me/nsgo/core # 或:go doc -all ./core
go doc github.com/oneo-me/nsgo/base
在儲存庫的檢出目錄中,ARCHITECTURE.md、docs/architecture/*.md(25 個主題文件)和 examples/ 是權威來源;examples/gallery 包含了所有元素、元件和服務的練習。如果您既沒有模組也沒有儲存庫,請明確說明 — 不要用其他框架(Fyne、Gio、Wails)來替代使用者的實際需求。
設定
需要 Go 1.26.0+(go.mod 標示為 go 1.26.0)。
| 作業系統 | 原生目標 | 備註 |
|---|---|---|
| macOS | ✅ | 完整的工具鏈:使用 Go + Xcode 建構/簽署所有目標 |
| Windows | ✅ | 原生和網頁建構與執行 |
| Linux | ✅ | 需要 GTK 4.14+、Pango、Cairo;用於網頁檢視/影片的 WebKitGTK 6.0 和 GStreamer |
將模組及其工具新增到應用程式一次 — 然後 go tool nsgo 無需安裝即可執行:
go get -tool github.com/oneo-me/nsgo/cmd/nsgo
NSGo 沒有發布任何標記版本,因此模組會解析為一個偽版本(v0.0.0-2026…-46c7d9c)。某些區域性鏡像站台會在未標記的模組上回傳 404;如果 go get 找不到它,請固定代理:
GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo
僅在 sum.golang.org 無法訪問時才需要 GOSUMDB=off。如果改用本地檢出目錄,指向它並整理依賴:
go mod edit -replace github.com/oneo-me/nsgo=/path/to/nsgo && go mod tidy
建立一個專案
mkdir counter && cd counter
go mod init counter
go get -tool github.com/oneo-me/nsgo/cmd/nsgo # 函式庫和工具
go tool nsgo init -name counter . # 寫入 nsgo.yaml、app.go、app_test.go
go mod tidy
go tool nsgo dev . # 執行它;每次儲存時重新建構
go tool nsgo test . # 無頭模式:2 個測試通過
init 需要一個名稱。 在現有目錄中執行單純的 go tool nsgo init 會失敗,並顯示 "." is not an app name,因為預設名稱來自目錄 — 而在 . 中就是 .。請傳遞 -name,或者傳遞一個要建立的目錄:
| 指令 | 效果 |
|---|---|
go tool nsgo init -name Counter . | 寫入當前目錄,應用程式名為 Counter |
go tool nsgo init Counter | 建立 ./Counter/ 目錄,內含應用程式 |
go tool nsgo init -template tsx Counter | 建立 ./Counter/ 目錄作為一個 TSX 應用程式 |
init 會列印其自身的下一步(next: go tool nsgo dev Counter)— 請相信它而不是記憶。
-template 可以是 go(預設)、tsx、mixed。應用程式名稱可以包含字母、數字、空格、- 和 _;套件識別碼會變成 dev.nsgo.<名稱>。傳遞 -module 也會寫入 go.mod。
| 模板 | 檔案 |
|---|---|
go | nsgo.yaml、app.go、app_test.go |
tsx | nsgo.yaml、main.go、App.tsx、app.test.tsx、tsconfig.json、.gitignore |
mixed | 同上,但 App.tsx 會匯入自己的 Go 程式碼和一個 native.go |
nsgo.yaml 指定應用程式名稱及其目標:
name: Hello
id: dev.oneo.nsgo.hello
version: 0.1.0
dev:
target: native # native | web | ios | ios_simulator | android
hot_reload: true
fps: false
build:
output: dist
targets: [macos_arm64, windows_x64, ios_simulator, android_arm64, web]
也有效的屬性:build_number、appearance、theme、i18n、icon.{apple,png}、dev.icon、android.permissions、sign.{profile,keystore}、web.host。在它旁邊,resources/ 存放了發布時隨附的檔案(載入為 asset:),assets/ 存放僅供專案使用的檔案(圖示),locales/ 存放了使用 ICU MessageFormat 的 en.yaml、de.yaml…。
您獲得的應用程式(-template go)
import (
"fmt"
"github.com/oneo-me/nsgo"
"github.com/oneo-me/nsgo/base"
"github.com/oneo-me/nsgo/core"
)
func Root(cx *core.Scope) core.Node {
count := core.NewSignal(cx, 0)
return core.View{Style: core.Style{FlexGrow: 1, Padding: core.All(24), Gap: 16}, Children: []core.Node{
core.Text{ContentFunc: func() string { return fmt.Sprintf("Pressed %d times", count.Get()) }},
base.Button{
Label: "Press me",
OnPress: func(core.PressEvent) { count.Update(func(n int) int { return n + 1 }) },
Style: core.Style{Background: core.RGB(0x0969da), CornerRadius: 8, Padding: core.Symmetric(10, 16)},
Children: []core.Node{core.Text{Content: "Press me", Style: core.Style{Color: core.RGB(0xffffff)}}},
},
}}
}
func main() { nsgo.Run(Root) }
nsgo.Run 接受一個根檢視(func(cx *core.Scope) core.Node)或一個應用程式函式(func(app *core.Scope)),後者透過 desktop 打開自己的視窗。選項:WindowSize(w, h)、Appearance(...)、HideDock()、FPS(bool)、WindowMaterial(...)、Fonts(paths...)、Component(name, factory)。
心智模型
- 訊號。
core.NewSignal[T comparable](cx, v)→.Get()、.Set(v)、.Update(fn)。NewMemo、NewEffect。圖形屬於引擎 goroutine 且永不鎖定;其他 goroutine 使用Scope.Post進入它。 - 元素是結構體;零值即為預設值(類似於
http.Server)。您只設定想要不同的部分。 - 每個反應式欄位都有一個孿生體:
Content+ContentFunc func() string、Style+StyleFunc、Class+ClassFunc。XFunc優於X;同時設定兩者會在掛載時引發恐慌。沒有靜態形式的欄位只接受函式(Disabled func() bool、Selected、Expanded)。 - 元件是任何具有
Render(cx *core.Scope) core.Node方法的型別;其欄位即為 props。core.Func適用於純函式。 - 區域 —
Show、Each、Dynamic、Catch— 僅重建其自身的跨度。Each是一個泛型函式,而不是一個方法。 - 樣式設定:型別化的
core.Style值,或在Class中使用 Tailwind 類別("flex-row gap-4 p-4 bg-white dark:bg-zinc-900")。base的元件故意不設定樣式 — 僅提供行為、狀態和無障礙功能。
動畫
兩個時脈。Commit = Go 重新渲染一個跨度(訊號、Show/Each/Dynamic)。Frame = 平台的動畫器驅動 AnimatedStyle 欄位和著色器統一變數,不進行 commit。所有動畫都屬於幀時脈;在渲染期間讀取的值會重新渲染子樹並重新啟動其中的每個動畫。
| 需要 | 使用 |
|---|---|
| 狀態變更(懸停、按壓、選取、調整大小) | Style.Transition — 一個持續時間、core.EaseOut 和一個 Properties 位元遮罩;完全不需要圖形,且在減少動畫模式下會被跳過 |
| 標準的進場/出場/吸引/循環 | core.Motion + 一個 core.MotionPreset(共 38 個,全部都會遵循減少動畫模式) |
| 自訂編排、重疊、保持、交換 | AnimatedStyle + AnimatedNode 圖形(Timing、Spring、Interpolate、Sequence、Stagger、Loop) |
| 逐畫素 / 無限場域 / 3D | core.Shader 統一變數作為節點 — 零次 commit |
| 程式化繪圖 | core.Canvas Draw(每次變更時重新記錄)或 d.Bind(平台重播它,不重新記錄) |
spec := core.FadeInUp.Spec() // 或您自己的 core.MotionSpec
m := core.NewMotion(cx, &spec) // nil = 在靜止狀態建構,稍後 Play
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{card()}}
五條比 API 更有價值的規則:(1) 永遠不要在渲染中讀取訊號或動畫值 — 僅使用閉包或 AnimatedStyle;(2) 重新設定樣式而非重建;(3) 一個有限動畫 保持 其最後一個值,因此結束時的幀必須是可讀取的(一個回到起始點的循環在穩定後看起來是空白的);(4) 一個動畫元素需要一個已測量的父元素 — 一個完全絕對定位的 View 測量為 0×0 並會裁剪所有內容;(5) AnimatedStyle 欄位是 AnimatedNodes,不是數字(core.Constant(1),而不是 1),而且它沒有 Width/Height/Perspective。
在編寫任何動畫之前,請閱讀 references/animation.md:預設表格、已確認有效的緩動/持續時間數值、編排食譜、著色器和畫布動畫、3D(光線行進)、測試,以及動畫錯誤的症狀→原因表。
開發循環、建構、測試
| 任務 | 指令 |
|---|---|
| 執行,儲存時重新建構 | go tool nsgo dev <dir> |
| 在瀏覽器中執行 | go tool nsgo dev -target web <dir> |
| 在模擬器/裝置上執行 | go tool nsgo dev -target ios_simulator|android|ios <dir> |
| 追蹤每次 commit | ... -trace |
| 發布套件 | go tool nsgo build <dir>(-target all、-output DIR、-run、-device D) |
| 嵌入到主機應用程式中 | ... build -embed → XCFramework (iOS) / AAR (Android) / nsgo.js (web);桌面使用 nsgo.NewView |
| 測試 | go tool nsgo test <dir>(-run REGEXP、-junit FILE、-target macos|windows|linux|web|ios_simulator|android、-fps) |
| 型別檢查 TSX | go tool nsgo check <dir> |
| 重新生成訊息目錄 | go tool nsgo i18n <dir> |
旗標會覆蓋它們所命名的 nsgo.yaml 值。build.targets 和 -target 接受 all,或像 windows_x64、macos_arm64、linux_x64、ios、web 這樣的名稱。build 在 -output(預設為 dist)下為每個目標寫入一個目錄,目錄名稱為 <name>-<version>-<target>/。桌面測試目標僅在其對應的 OS 上執行(Windows 上為 -target windows,Mac 上為 -target macos);web、ios_simulator 和 android 從 Mac 執行。
測試的行為就像使用者一樣。nsgotest 在無頭模式下執行它們,沒有視窗;-target 在一個具有原生輸入和螢幕截圖的真實視窗中執行相同的測試(app.Screenshot("name"))。
func TestMain(m *testing.M) { nsgotest.Main(m) } // 為平台提供主執行緒
func TestPress(t *testing.T) {
app := nsgotest.Start(t, Root)
app.Press("text:Press me")
app.Find("text:Pressed 1 times")
}
選擇器是 Playwright 的:id:、label:、text:(子字串;text:"Save" 或純字串 = 整個文字)、role:<role> <name>、view:<n>、/regexp/flags,以及 >> 用於向下遍歷。步驟:Press、PressDown/PressUp、LongPress、Key、KeyPress、Type、Focus、Hover、Scroll、ScrollTo、Drag、Touch、Back、Resize、Settle、Advance、WaitFor、WaitForGone、Exists、Query、Texts、Dump。完整列表:go doc github.com/oneo-me/nsgo/nsgotest。
TSX 應用程式具有相同的結構:App.tsx 從 "nsgo" 匯入 mount, signal, Text, View(元件從 "nsgo/base" 匯入)並以 mount(App) 結尾;main.go 嵌入原始碼並呼叫 nsgo.Run(compiler.App(src, "App.tsx"))。*.test.tsx 檔案作為 Go 測試執行。請參閱 references/api.md。
常見錯誤
| 錯誤 | 現實 |
|---|---|
| 猜測欄位名稱 | NSGo 處於預發布階段,幾乎沒有公開文件,因此回憶起的識別碼是錯的。請先使用 go doc — 一個錯誤的名稱就是編譯錯誤 |
同時設定 Content 和 ContentFunc | 在掛載時引發恐慌。選擇其中一個 |
NewSignal(cx, []string{...}) | T 必須是 comparable。使用帶有您自己的 equal 的 NewSignalFunc |
在 scope 外部使用 NewSignal | 訊號需要一個 cx;圖形是 goroutine 本地的,因此 cx 是唯一的控制代碼。從另一個 goroutine 使用 Scope.Post |
期望 Button.Label 會渲染 | Label 是可存取的名稱(role:/label: 選擇器匹配的對象)。可見文字來自 Children |
匯入 internal/... | 應用程式只能匯入 nsgo、core、base、mobile、desktop、i18n、icons、tsx、nsgotest |
在 Windows 或 Linux 上執行 go build ./... | 失敗:internal/platform/apple/edge.go 缺少其 //go:build darwin 標籤並匯入了僅限 darwin 的 purego/objc。建構您自己的套件(go build .),或在檢出目錄中新增該標籤 |
JustifyContent: core.AlignCenter | 兩個列舉:AlignItems 接受 core.AlignStart/Center/End/Stretch/Baseline,JustifyContent 接受 core.JustifyStart/Center/End/SpaceBetween/...。混合它們是編譯錯誤 |
core.Shader{OnMouseMove: ...} | 著色器有 OnPress、OnLayout、Style — 沒有指標移動處理器。將 OnMouseMove func(Point) / OnHoverOut func() 放在包圍它的 core.View 上 |
只相信 MustCompileShader | Kage 在執行時編譯,因此錯誤的著色器會在啟動時引發恐慌,而不是在建構時。使用 core.CompileShader 測試每個原始碼並報告其錯誤 |
一個永不滾動且不顯示條的 Scroll | Scroll 預設為 FlexShrink: 1,但 View 父元素不會:只有 FlexGrow: 1 的欄位會增長到其內容高度,滾動視圖繼承該高度,因此沒有任何內容溢出。為該欄位同時設定 FlexGrow: 1, FlexShrink: 1(React Native 的 flex: 1)。斷言:滾動視圖的框架必須比視窗短 |
| 尋找一個永久捲軸 | 指示器是一個疊加層:原生會繪製一個在滾動時淡入、保持 700 毫秒、然後在 300 毫秒內淡出的圓角滑塊;HideIndicator 會移除它。在網頁上,它是瀏覽器自身的條(overflow-y: auto、scrollbar-width: auto) |
一個僅使用 Left 固定位置的疊加層 | 一個具有一個水平邊的絕對定位檢視會占用其包含區塊的 剩餘 寬度,而不是其內容寬度:一個在 26pt 圓形錨點內設定 Left: 34 的工具提示最終沒有剩餘空間,並會折疊成每行一個字元。為它指定一個明確的 Width |
| 在插值顏色內使用 Alpha | 由 core.Interpolate 提供、並使用 Colors: {RGBA(0xffffff, 0.12), ...} 的 AnimatedStyle.Background 會繪製成接近實心的白色,而不是 12%。對插值填充使用不透明顏色,並將 Alpha 保留給 Style.Background |
MotionPreset.Name() 作為標籤 | 回傳駝峰式命名(fadeIn、zoomIn、breathe),而不是 Go 常數的名稱 |
| 動畫「跳回起點」 | 在渲染期間讀取了訊號或動畫值,或在 Dynamic 的渲染內部建構了 NewAnimatedValue:跨度會重建,值會重新啟動。在渲染內部讀取… |
…
[內容已截斷]