快速开始
gotsx 是一个 Go 模块。应用 = 一个 Go 包(路由、宿主模块、动作) + 一个 app/ 目录(方言写的页面、组件、岛)。编译器把 app/ 变成 gen/ 里的 Go 和 JS, 然后 go build。
1. 创建应用
一条 go install、一条 gotsx new, 出来的应用直接能跑
go install github.com/childrentime/gotsx/cmd/gotsx@latest
gotsx new hello && cd hello # scaffold an app in its own Go module (host module, page, island, action, tsconfig)
gotsx dev # compile → go build → run; rebuild + browser reload on change
gotsx check # type-check only — the same diagnostics the editor LSP shows
go build -o hello . # one self-contained binarygotsx dev 会先跑 cmd/hostgen 生成宿主类型, 再编译方言, 再 go build 并启动; 之后监视 app/, 改动后约 2 秒重新上线, 浏览器自动刷新。编译失败时旧版本继续运行。仓库自带的示例用 make dev-example / dev-shop。2. 目录约定
hello/
├── go.mod # module hello · require github.com/childrentime/gotsx (gotsx.json is optional)
├── tsconfig.json # editor support: app/.gen/gotsx.d.ts + host.d.ts are generated
├── main.go # gotsx.Serve(gotsx.Options{Routes: gen.Routes, ...})
├── host/host.go # host module (Go)
├── cmd/hostgen/ # reflect host.Registry → app/.gen/host.d.ts + host.json
├── app/
│ ├── pages/ # file routing: index.server.tsx → /, p/[id].server.tsx → /p/{id}, docs/[...slug].server.tsx → catch-all
│ ├── components/ # *.server.tsx → Go only
│ ├── islands/ # *.client.tsx → Go (SSR) + JS (signals)
│ ├── ui/ # no suffix = shared, compiled to both
│ └── tailwind.css # present → Tailwind runs
├── public/ # static files → /public/*
└── gen/ # generated output: *_gen.go, routes_gen.go, client/*.js后缀决定编译目标: .server.tsx 只编 Go, .client.tsx 编 Go + JS(岛), 无后缀是共享组件(两端都编)。pages/ 下的文件是路由, [id] 是路径参数, [...slug] 是 catch-all。页面里 redirect() / notFound() 可以中断渲染。
3. 写一个页面
页面是 export default 的组件, props 固定是 PageProps
import type { PageProps } from "gotsx";
import { models } from "host:data"; // Go-backed, zero marshalling
import Layout from "../components/Layout.server";
import ModelCard from "../components/ModelCard.server";
export default function Home({ query }: PageProps) {
const q = query.q ?? "";
const list = models.search(q); // synchronous: concurrency from goroutines
return (
<Layout title="Products">
<div class="grid">{list.map((m) => <ModelCard model={m} />)}</div>
{list.length === 0 && <p class="empty">No matching products</p>}
</Layout>
);
}注意没有 async: 宿主调用是同步的, 请求之间的并发由 goroutine 提供。
4. 写一个岛
需要交互的部分放进 .client.tsx
import { useState, useEffect } from "gotsx";
export default function Counter({ start }: { start: number }) {
const [n, setN] = useState(start);
const double = n * 2; // depends on n → auto memo
useEffect(() => { console.log(n); }); // JS backend only
return (
<button onClick={() => setN(n + 1)}>
{n} ×2 = {double}{n > 4 && <b> 🔥</b>}
</button>
);
}在服务端组件里像普通组件一样用: <Counter start={0} />。props 必须可 JSON 序列化(它会进 HTML 属性), 岛不接受 children。
5. 暴露 Go 能力
宿主模块 = 一个 Go 值; 字段按 json tag、方法首字母小写映射到方言
// host/host.go — the host module: the other side of import { models } from "host:data"
type ModelStore struct{ items []Model }
func (s *ModelStore) Search(q string) []Model { … }
func (s *ModelStore) Get(id string) (Model, error) { // error → 404 at the request layer
…
return Model{}, fmt.Errorf("%w: %s", gotsx.ErrNotFound, id)
}
var Registry = map[string]gotsx.HostModule{
"data": {Value: &DataModule{Models: store}, Go: "host.Data"},
}// app/.gen/host.d.ts — reflected from Go; what TSX sees = what Go exposes
declare module "host:data" {
export interface Model { id: string; title: string; likes: number; tags: string[] }
export interface ModelStore {
search(arg0: string): Model[];
get(arg0: string): Model;
}
export const models: ModelStore;
}编译后 models.search(q) 就是 host.Data.Models.Search(q): 没有编组, 没有反射。Go 的 int 和方言的 number 自动转换; 返回 error 的方法, error 变成 panic 由请求层 recover, 包了 gotsx.ErrNotFound 的回 404。
6. 从岛回到 Go: 类型化 action
把 Go 方法列进 Actions, 岛里直接 import 并 await —— HTTP 调用的两端由编译器生成
// host/host.go — list the methods islands may call; hostgen types them as Promise<T>
func (d *DataModule) Like(id string) (int, error) { return d.Models.Like(id) }
func (d *DataModule) Rename(req *gotsx.Req, id, title string) error { … } // *gotsx.Req: session / cookies
var Registry = map[string]gotsx.HostModule{
"data": {Value: Data, Go: "host.Data", Actions: []string{"Like", "Rename"}},
}// app/islands/LikeButton.client.tsx — a typed action: no fetch, no untyped JSON
import { like } from "host:data"; // like(id: string): Promise<number>
export default function LikeButton({ id, likes }: { id: string; likes: number }) {
const [n, setN] = useState(likes);
return <button onClick={async () => setN(await like(id))}>{n}</button>;
}
// main.go: gotsx.Options{HostActions: gen.HostActions, …}
// errors: gotsx.Invalid(fields) → 422 (e.fields), gotsx.ErrNotFound → 404, else 500 — the call throws an Error with e.status会话、flash 消息和 CSRF token 一并提供: 带 *gotsx.Req 参数的 action 可以读写签名会话; 页面拿到 props.session、props.flash 和 props.csrf(给经典表单用)。页面还可以导出 meta(props): Meta, 由布局渲染进 <head>。
7. 样式: Tailwind
有 app/tailwind.css 就会在每次构建时跑 Tailwind standalone CLI
/* app/tailwind.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme { --color-brand-600: #1557d6; }class="..." 在方言里就是普通字符串, Tailwind 构建期扫描 app/**/*.tsx 生成 public/tailwind.css。二进制查找顺序: $GOTSX_TAILWIND → 仓库 .tools/tailwindcss → PATH。同样不需要 Node。这个站的每一个 class 都是这么来的。