Active-tachometer477/dark

GitHub: Active-tachometer477/dark

一个结合 Go 后端和 Preact/React 前端的 SSR Web 框架,通过 Islands 架构和 htmx 实现高性能的服务端渲染与交互式页面。

Stars: 0 | Forks: 0

# Dark 一个由 [Preact](https://preactjs.com/) 或 [React](https://react.dev/)、[htmx](https://htmx.org/) 以及 Islands 架构驱动的 Go SSR Web 框架。 Dark 使用 [ramune](https://github.com/i2y/ramune)(一个专为 Go 打造的 JS/TS 运行时)在服务器上渲染 TSX 组件,通过 Go 的 Loader/Action 函数获取数据,并借助 htmx 的 HTML-over-the-wire 方案结合最少的客户端 JavaScript 来交付交互式页面。 ## 环境要求 Dark 使用 ramune 进行 SSR,该运行时支持两种 JS 引擎后端: | | JSC (默认) | QuickJS-NG (`-tags qjswasm`) | |---|---|---| | **引擎** | 通过 [purego](https://github.com/ebitengine/purego) 调用 Apple JavaScriptCore | 编译为 WebAssembly 的 QuickJS-NG,由 [wazero](https://github.com/tetratelabs/wazero)(纯 Go)驱动 | | **JIT** | 是 | wazero 编译器模式 (AOT WASM→本地代码) | | **平台** | macOS, Linux | macOS, Linux, Windows, FreeBSD | | **系统依赖** | macOS: 无。Linux: `apt install libjavascriptcoregtk-4.1-dev` | 无 | | **适用场景** | 生产环境性能 | 可移植性,零依赖部署 | 两者都是纯 Go 构建——无需 C 编译器或 Cgo。 ### 默认引擎 ``` # macOS — 无额外依赖 go build . # Linux sudo apt install libjavascriptcoregtk-4.1-dev go build . ``` ### QuickJS-NG 后端 ``` go build -tags qjswasm . ``` 无需共享库。在包括 Windows 的所有平台上均可运行。权衡之处:比 JSC 的 JIT 慢,但 wazero 编译器模式的 AOT 路径大幅缩小了差距。对于大多数瓶颈在 I/O(数据库、网络)的应用来说,差异微乎其微。 ## 基于 net/http 构建 Dark 遵循标准的 `net/http` 约定。没有任何外部路由依赖。 - 内部路由使用 `http.NewServeMux` 及 Go 1.22+ 增强的路由模式 (`GET /users/{id}`) - `app.Handler()` 返回 `(http.Handler, error)` —— 可将其插入任何 Go HTTP 技术栈 - 中间件采用标准的 `func(http.Handler) http.Handler` 签名 - Dark 不控制服务器 —— 你可以使用 `http.ListenAndServe` 或 `http.Server` 自行启动 ``` // Simple — MustHandler() panics on error (convenient for main) http.ListenAndServe(":3000", app.MustHandler()) // With error handling handler, err := app.Handler() if err != nil { log.Fatal(err) } http.ListenAndServe(":3000", handler) // With http.Server for full control srv := &http.Server{ Addr: ":8080", Handler: app.MustHandler(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } srv.ListenAndServe() ``` 任何现有的 `net/http` 中间件均可通过 `app.Use()` 开箱即用。 ## 功能特性 - **服务端渲染** —— 通过 Preact 或 React 的 `renderToString` 在沙箱化的 JS 运行时中渲染 TSX 模板 - **Loader/Action 模式** —— 使用 Go 函数进行数据获取和修改,props 以 JSON 格式传递 - **htmx 集成** —— 感知 HX-Request 的响应(完整页面 vs HTML 片段) - **Islands 架构** —— 结合懒加载(`load`、`idle`、`visible`)进行选择性客户端注水 - **流式 SSR** —— shell-first 渲染,提供更快的 TTFB - **嵌套布局** —— 通过路由分组实现可组合的布局 - **表单验证** —— 字段级错误提示及表单数据保留 - **会话管理** —— HMAC 签名的 Cookie 会话及 Flash 消息 - **身份验证** —— 带有 htmx 感知重定向的 `RequireAuth` 中间件 - **Head 标签管理** —— 针对每个页面的 ``、`<meta>` 和 OpenGraph 标签 - **API 路由** —— 与页面路由并存的 JSON 端点 - **开发模式** —— 热重载、带有 Source Map 的错误提示覆盖层、TypeScript 类型生成 - **SSR 缓存** —— 带有 ETag / 304 Not Modified 的 LRU 内存缓存 - **CSRF 防护** —— 基于会话的 Token,自动与 htmx/TSX 集成 - **并发 Loaders** —— 并行获取数据并合并结果 - **内嵌视图** —— 从 `embed.FS` 加载 TSX 文件以实现单二进制文件部署 - **静态文件系统** —— 从任何 `fs.FS` (embed.FS, os.DirFS) 提供静态资源服务 - **JSX 自动转换** —— 无需 `import { h }` 或 `import React` 等样板代码 - **桌面应用** —— 通过 WebView 创建原生窗口,具备 Go↔JS 绑定和双向事件通信 ## 快速开始 ``` package main import ( "log" "net/http" "github.com/i2y/dark" ) func main() { app, err := dark.New( dark.WithLayout("layouts/default.tsx"), dark.WithTemplateDir("views"), dark.WithDevMode(true), ) if err != nil { log.Fatal(err) } defer app.Close() app.Use(dark.Logger()) app.Use(dark.Recover()) app.Get("/", dark.Route{ Component: "pages/index.tsx", Loader: func(ctx dark.Context) (any, error) { return map[string]any{"message": "Hello, Dark!"}, nil }, }) log.Fatal(http.ListenAndServe(":3000", app.MustHandler())) } ``` 布局包裹了每一个页面。每个页面组件的输出都作为 `children` 传递。在处理 htmx 请求(`HX-Request` 请求头)时,会跳过布局并仅返回页面片段。 ``` // views/layouts/default.tsx export default function Layout({ children }) { return ( <html lang="en"> <head> <meta charset="UTF-8" /> <title>My App {children} ); } ``` ``` // views/pages/index.tsx export default function IndexPage({ message }) { return

{message}

; } ``` ``` go run main.go # => 正在监听 http://localhost:3000 ``` ## 路由 路由使用 Go 1.22+ 的 `ServeMux` 模式及 `{param}` 通配符。 ``` app.Get("/", dark.Route{...}) app.Get("/users/{id}", dark.Route{...}) app.Post("/users/{id}/orders", dark.Route{...}) app.Put("/posts/{id}", dark.Route{...}) app.Delete("/posts/{id}", dark.Route{...}) app.Patch("/settings", dark.Route{...}) ``` ### Route 结构体 ``` dark.Route{ Component: "pages/show.tsx", // TSX file (relative to template dir) Loader: loaderFunc, // data fetching (single) Loaders: []dark.LoaderFunc{...}, // concurrent data fetching (merged) Action: actionFunc, // mutations (POST/PUT/DELETE) Layout: "layouts/extra.tsx", // per-route layout (nests inside global layout) Streaming: &boolVal, // per-route streaming SSR override Props: MyProps{}, // zero value for TypeScript type generation } ``` ### API 路由 绕过 TSX 渲染管线的 JSON 端点: ``` app.APIGet("/api/status", dark.APIRoute{ Handler: func(ctx dark.Context) error { return ctx.JSON(200, map[string]any{"status": "ok"}) }, }) app.APIPost("/api/items", dark.APIRoute{ Handler: func(ctx dark.Context) error { var input CreateItemRequest if err := ctx.BindJSON(&input); err != nil { return dark.NewAPIError(400, "invalid JSON") } // ... return ctx.JSON(201, item) }, }) ``` ## 路由分组 分组共享 URL 前缀、布局和中间件: ``` app.Group("/admin", "layouts/admin.tsx", func(g *dark.Group) { g.Use(dark.RequireAuth()) g.Get("/dashboard", dark.Route{ Component: "pages/admin/dashboard.tsx", Loader: dashboardLoader, }) // Nested groups compose layouts g.Group("/settings", "layouts/settings.tsx", func(sg *dark.Group) { sg.Get("/profile", dark.Route{...}) }) }) ``` ## Context 上下文 `dark.Context` 封装了请求和响应: ``` ctx.Request() *http.Request ctx.ResponseWriter() http.ResponseWriter ctx.Param("id") string // path parameter ({id}) ctx.Query("page") string // query string ctx.FormData() url.Values // parsed form data ctx.Redirect("/path") error // redirect (htmx-aware) ctx.SetHeader("X-Custom", "value") // JSON ctx.JSON(200, data) error ctx.BindJSON(&input) error // Validation ctx.AddFieldError("email", "required") ctx.HasErrors() bool ctx.FieldErrors() []FieldError // Head ctx.SetTitle("Page Title") ctx.AddMeta("description", "...") ctx.AddOpenGraph("og:image", "...") // Cookies ctx.SetCookie("theme", "dark", dark.CookieMaxAge(86400)) ctx.GetCookie("theme") (string, error) ctx.DeleteCookie("theme") // Session (requires Sessions middleware) ctx.Session() *Session // Request-scoped values (set by middleware, read by loaders) ctx.Set("key", value) ctx.Get("key") any ``` ## 会话 HMAC-SHA256 签名的 Cookie 会话: ``` app.Use(dark.Sessions([]byte("secret-key-at-least-32-bytes"), dark.SessionName("app_session"), dark.SessionMaxAge(86400), dark.SessionSecure(true), )) ``` ``` // In a Loader/Action: sess := ctx.Session() sess.Set("user", username) sess.Get("user") // returns any sess.Delete("user") sess.Clear() // Flash messages (available for one request) sess.Flash("notice", "Saved!") flashes := sess.Flashes() // map[string]any ``` ## 身份验证 ``` // Basic usage — checks session key "user", redirects to "/login" g.Use(dark.RequireAuth()) // Custom options g.Use(dark.RequireAuth( dark.AuthSessionKey("account"), dark.AuthLoginURL("/auth/signin"), dark.AuthCheck(func(s *dark.Session) bool { return s.Get("role") == "admin" }), )) ``` ## CSRF 防护 基于会话的 CSRF Token,自动与 htmx 集成: ``` app.Use(dark.Sessions(secret)) app.Use(dark.CSRF()) ``` 该中间件会自动: - 生成每个会话的 token - 将 `` 注入到 `` 中 - 注入一段 htmx 配置脚本,将 `X-CSRF-Token` 附加到所有 htmx 请求中 - 在 Loader props 中添加 `_csrfToken`(用于隐藏的表单字段) - 验证 POST/PUT/DELETE/PATCH 请求上的 `X-CSRF-Token` 请求头或 `_csrf` 表单字段 ``` export default function Form({ _csrfToken }) { return (
); } ``` htmx 表单无需额外设置 —— token 请求头会自动附加。 ## 并发 Loaders 并行从多个数据源获取数据: ``` app.Get("/dashboard", dark.Route{ Component: "pages/dashboard.tsx", Loaders: []dark.LoaderFunc{ func(ctx dark.Context) (any, error) { return map[string]any{"user": fetchUser(ctx.Param("id"))}, nil }, func(ctx dark.Context) (any, error) { return map[string]any{"activity": fetchActivity(ctx.Param("id"))}, nil }, func(ctx dark.Context) (any, error) { return map[string]any{"notifications": fetchNotifications()}, nil }, }, }) ``` 结果会被合并到一个单一的 props 映射中。如果任何一个 loader 返回错误,该请求将立即失败。 ## 中间件 标准的 `func(http.Handler) http.Handler`: ``` app.Use(dark.Logger()) // request logging app.Use(dark.Recover()) // panic recovery → 500 app.Use(app.RecoverWithErrorPage()) // panic recovery → custom error page app.Use(dark.Sessions(secret)) // session management ``` 任何现有的 net/http 中间件均可兼容: ``` app.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Frame-Options", "DENY") next.ServeHTTP(w, r) }) }) ``` ## Islands 架构 注册交互式组件以进行客户端注水: ``` app.Island("counter", "islands/counter.tsx") ``` Island 组件是带有默认导出的普通 Preact/React 组件: ``` // views/islands/counter.tsx import { useState } from "preact/hooks"; export default function Counter({ initial = 0 }) { const [count, setCount] = useState(initial); return ; } ``` 在页面中,使用 `dark` 模块中的 `island()` 进行包裹,将其标记为可客户端注水: ``` // views/pages/index.tsx import { island } from "dark"; import Counter from "../islands/counter.tsx"; const InteractiveCounter = island("counter", Counter); // Lazy loading strategies: // island("counter", Counter, { load: "idle" }) — requestIdleCallback // island("counter", Counter, { load: "visible" }) — IntersectionObserver export default function Page() { return (

My Page

); } ``` ## 静态文件 从磁盘上的目录提供服务: ``` app.Static("/static/", "public") ``` 或从 `fs.FS`(如 embed.FS、os.DirFS 等)提供服务: ``` //go:embed public var publicFS embed.FS sub, _ := fs.Sub(publicFS, "public") app.StaticFS("/static/", sub) ``` ## 可选配置 ``` dark.New( dark.WithPoolSize(4), // ramune RuntimePool workers (default: runtime.NumCPU()) dark.WithTemplateDir("views"), // TSX file directory (default: "views") dark.WithViewsFS(viewsFS), // load views from fs.FS (for embed.FS) dark.WithLayout("layouts/default.tsx"), // global layout dark.WithDependencies("lodash"), // npm packages (preact is always included) dark.WithDevMode(true), // hot reload + error overlay dark.WithStreaming(true), // streaming SSR globally dark.WithSSRCache(1000), // LRU SSR output cache (enables ETag) dark.WithLogger(slog.Default()), // structured logger for framework internals dark.WithErrorComponent("errors/500.tsx"), dark.WithNotFoundComponent("errors/404.tsx"), ) ``` ## 项目结构 ``` myapp/ ├── main.go ├── views/ │ ├── layouts/ │ │ └── default.tsx │ ├── pages/ │ │ ├── index.tsx │ │ └── users/ │ │ └── show.tsx │ ├── islands/ │ │ └── counter.tsx │ └── errors/ │ ├── 404.tsx │ └── 500.tsx └── public/ └── style.css ``` ## React 支持 Dark 默认使用 Preact,但也支持 React。传入 `WithUILibrary(dark.React)` 进行切换: ``` app, err := dark.New( dark.WithUILibrary(dark.React), dark.WithLayout("layouts/default.tsx"), dark.WithTemplateDir("views"), ) ``` 组件的编写方式相同 —— 无需引入特定框架的包: ``` // views/pages/index.tsx export default function IndexPage({ message }) { return

{message}

; } ``` Islands 可直接使用 React hooks: ``` // views/islands/counter.tsx import { useState } from 'react'; export default function Counter({ initial }) { const [count, setCount] = useState(initial || 0); return ; } ``` MCP 应用也支持通过 `WithMCPUILibrary(dark.React)` 来使用 React。 ## MCP 应用 (实验性) Dark 支持 [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) —— 由 MCP 工具返回并在宿主沙箱 iframe 中渲染的交互式 HTML UI。基于官方 Go SDK [`github.com/modelcontextprotocol/go-sdk`](https://github.com/modelcontextprotocol/go-sdk) 构建。 ``` mcpApp, err := dark.NewMCPApp("my-server", "1.0.0", dark.WithMCPTemplateDir("views"), ) defer mcpApp.Close() // UI tool: returns an interactive TSX component if err := dark.AddUITool(mcpApp, "dashboard", dark.UIToolDef{ Description: "Show analytics dashboard", Component: "mcp/dashboard.tsx", }, func(ctx context.Context, args DashboardArgs) (map[string]any, error) { return map[string]any{"data": fetchData(args.Period)}, nil }); err != nil { log.Fatal(err) } // Text tool: standard MCP tool returning plain text dark.AddTextTool(mcpApp, "stats", "Get statistics", func(ctx context.Context, args StatsArgs) (string, error) { return "Stats: ...", nil }) mcpApp.RunStdio(ctx) // stdio transport // or mcpApp.StreamableHTTPHandler() // HTTP transport ``` 服务器声明 `io.modelcontextprotocol/ui` 扩展,并将每个 UI 工具注册为一个资源 (`ui://{server}/{tool}.html`)。渲染管线如下: 1. 注册时:esbuild 将组件与内联的 UI 库打包 → 生成静态应用骨架 HTML 2. 工具调用时:Go 处理程序返回 props → 作为 JSON 文本包含在工具执行结果中 3. 宿主读取资源 → 在沙箱 iframe 中渲染 HTML 4. MCP 应用桥接 (postMessage JSON-RPC,协议版本 `2026-01-26`) 将工具结果交付给 iframe 5. 组件使用接收到的 props 在客户端进行渲染 示例:[`examples/mcp-app/`](examples/mcp-app/) ## 桌面应用 Dark 可以作为原生桌面应用运行。[`desktop`](desktop/) 子包将你的 `http.Handler` 封装在一个 WebView 窗口中,提供 Go↔JS 函数绑定和双向事件系统。所有 dark 特性(SSR、Islands、htmx、会话)均能正常运作,无需修改。 ``` func init() { runtime.LockOSThread() } func main() { app, _ := dark.New(dark.WithLayout("layouts/default.tsx"), dark.WithTemplateDir("views")) defer app.Close() app.Get("/", dark.Route{Component: "pages/index.tsx", Loader: indexLoader}) // Simple one-liner desktop.Run(app.MustHandler(), desktop.WithTitle("My App")) // Or full API with bindings and events dsk := desktop.New(app.MustHandler(), desktop.WithTitle("My App"), desktop.WithDebug(true)) dsk.Bind("greet", func(name string) string { return "Hello, " + name }) dsk.On("save", func(data any) { fmt.Println("save:", data) }) dsk.Run() } ``` 有关完整的 API 参考文档(绑定、事件、窗口控制、选项),请参阅 [`desktop/README.md`](desktop/README.md)。 ## 示例 - **[hello](_examples/hello/)** —— 功能丰富的演示:路由、布局、会话、Islands、流式 SSR、表单验证 - **[showcase](_examples/showcase/)** —— CSRF、并发 Loaders、SSR 缓存 + ETag、SSG、Context.Set/Get - **[database](_examples/database/)** —— 结合会话与身份验证的 SQLite CRUD 操作 - **[desktop-demo](_examples/desktop-demo/)** —— 桌面应用:Islands、htmx、会话、Go↔JS 绑定、事件 - **[deploy](_examples/deploy/)** —— 包含 Dockerfile 和 Fly.io 配置的生产环境设置 - **[mcp-app](examples/mcp-app/)** —— MCP 应用:通过 postMessage 实现的交互式 UI 工具 ## 单二进制文件部署 使用 `embed.FS` 将视图和静态资源嵌入到 Go 二进制文件中: ``` package main import ( "embed" "io/fs" "log" "net/http" "github.com/i2y/dark" ) //go:embed views var viewsFS embed.FS //go:embed public var publicFS embed.FS func main() { views, _ := fs.Sub(viewsFS, "views") public, _ := fs.Sub(publicFS, "public") app, err := dark.New( dark.WithViewsFS(views), dark.WithLayout("layouts/default.tsx"), ) if err != nil { log.Fatal(err) } defer app.Close() app.StaticFS("/static/", public) app.Get("/", dark.Route{ Component: "pages/index.tsx", Loader: func(ctx dark.Context) (any, error) { return map[string]any{"message": "Hello!"}, nil }, }) log.Fatal(http.ListenAndServe(":3000", app.MustHandler())) } ``` 构建并部署为单一的二进制文件 —— 运行时无需 `views/` 或 `public/` 目录。 ## 命令行工具 安装 CLI 工具: ``` go install github.com/i2y/dark/cmd/dark@latest ``` ### 创建新项目脚手架 ``` dark new my-app # Preact (default) dark new my-app --ui react # React cd my-app && go mod tidy && make dev ``` ### 生成组件 ``` dark generate route users # creates views/pages/users.tsx dark generate island counter # creates views/islands/counter.tsx ``` ### 打包桌面应用 为桌面应用构建可分发的安装包: ``` dark package macos --name "My App" --icon icon.png --id com.example.myapp dark package windows --name "My App" --icon icon.png dark package linux --name "My App" --icon icon.png ``` - **macOS** —— 包含 Info.plist、启动器脚本和可选 .icns 图标的 `.app` 包 - **Windows** —— `.exe`(GUI 模式,使用 qjswasm 后端构建)+ views/public - **Linux** —— 二进制文件 + `.desktop` 文件 + views/public 可用选项:`--out`(输出目录,默认:`dist`),`--arch`(目标架构,默认:当前架构)。 ## 部署 有关包含 Docker 多阶段构建和 Fly.io 配置的生产就绪设置,请参阅 [_examples/deploy](_examples/deploy/)。 ## 许可证 MIT
标签:EVTX分析, Go, htmx, HTTP服务器, Islands architecture, JavaScriptCore, net/http, Preact, QuickJS, React, Ruby工具, SEO优化, SSR, Syscalls, TSX, TypeScript, wazero, WebAssembly, Web开发框架, 全栈, 前后端分离, 安全插件, 岛屿架构, 微前端, 日志审计, 服务端渲染