tug v0.45.0

Go + Inertia.js v3

由 Go 处理函数直接渲染 React 页面,中间无需 API。

tug 是一个 Web 框架,面向前端使用 Inertia.js 的 Go 应用。处理函数用带类型的 props 渲染 React、Vue 或 Svelte 页面,整个应用以单个二进制文件发布。

从零到单个二进制文件
$ go install github.com/cuonggt/tug/cmd/tug@latest
$ tug new blog          # -auth for accounts
$ cd blog
$ tug dev
tug  │ serving http://localhost:8080, built in 268ms
^C
$ tug build
tug build: blog, 9.6 MB, frontend included

如何衔接

一个 Go 结构体,既是页面的 props,也是它们的类型。

处理函数用一个结构体渲染页面,页面的组件把它作为 props 接收。tug gen 写出这个结构体的 TypeScript,让组件对照 Go 代码接受类型检查;Go 代码改动时,tug dev 会重新写出。

main.go
type PostsIndexProps struct {
	Posts []Post                   `json:"posts"`
	Stats inertia.DeferProp[Stats] `json:"stats"`
}

var PostsIndex = tug.Page[PostsIndexProps]("Posts/Index")

func index(c *tug.Ctx) error {
	return PostsIndex.Render(c, PostsIndexProps{
		Posts: store.List(),
		Stats: inertia.Defer(store.Count), // after the page shows
	})
}
resources/js/tug/pages.ts
// Code generated by tug gen from the Go types. DO NOT EDIT.

export interface PostsIndexProps {
  posts: Post[]
  stats?: Stats
}
resources/js/pages/Posts/Index.tsx
import { Deferred, Link } from '@inertiajs/react'
import type { PageProps } from '../../tug/pages'
import { route } from '../../tug/routes'

type Props = PageProps<'Posts/Index'>

export default function Index({ posts, stats }: Props) {
  return (
    <>
      <Deferred data="stats" fallback={<p>Counting…</p>}>
        {stats && <p>{stats.posts} posts</p>}
      </Deferred>
      {posts.map((post) => (
        <Link
          key={post.id}
          href={route('posts.show', { id: post.id })}
        >
          {post.title}
        </Link>
      ))}
    </>
  )
}

包含什么

应用所需的各个部分,都已与 Inertia 配合好。

会话、flash 消息、验证错误和 CSRF 无需配置即可与 Inertia 配合使用,底层基于标准库。

  • 完整的 Inertia v3

    延迟、合并、只加载一次和可选的 props,按路径的部分重载,无限滚动,以及由 Go 设置的页面标题和描述。

  • 来自 Go 的类型

    tug gen 为每个页面的 props、命名路由、每个表单发送的内容以及 flash 数据的键写出 TypeScript。

  • 自动验证的表单

    c.BindValid 绑定请求,并按结构体标签检查它。未通过验证的表单会带着错误返回,每个字段在失去焦点时就会检查。

  • 账户系统,只需一个参数

    tug new -auth:注册、邮箱验证、双因素验证码、通行密钥、API 令牌和通知,可用 SQLite、Postgres 或 MySQL。

  • 后台任务

    任务失败后会重新运行,可以按任意时区的计划运行,无论推送多少次都只运行一次,还可以在应用自己的事务中推送。

  • 单个二进制文件

    tug build 生成单个内含前端的静态二进制文件,前端由应用自己以 gzip 压缩发送;应用还附带一个 Dockerfile。

  • 也可以在服务端渲染

    tug new -ssr 在应用旁边的 Node 中,用 Inertia 自己的 SSR 渲染首次访问的页面。没有 Node 时,页面在浏览器中渲染。

  • 无需浏览器的测试

    tugtest 包是供 Go 测试使用的 Inertia 客户端:访问、表单、上传和登录,并把页面的 props 读入对应的 Go 结构体。

  • 跨实例的事件

    broadcast 包经由应用的数据库,把频道上的事件送到每个实例上打开的页面,页面随即重新加载变化的部分。

为什么选择 Go

一个小巧的二进制文件,用上所有核心,几毫秒即可启动。

Go 能把 Web 应用做成单个静态二进制文件:一个进程就能用上所有核心,几毫秒内启动,运行只需几十 MB 内存。tug 为 Inertia 应用保留了这一切:单个静态二进制文件,前端的构建产物就在其中,由 Go 自己的 HTTP 服务器提供服务。

  • 性能

    在笔记本电脑上每秒处理数万个请求,一个进程就能用上所有核心,只占几十 MB 内存。

  • 启动

    只需几毫秒,在可以缩容到零的平台上能压低成本。

  • 部署

    只需复制一个文件,无需安装运行时或依赖,在 Mac 上就能为 Linux 或 ARM 构建。

  • 开发

    一门小巧、编译快速的语言,标准库中的 HTTP 服务器就足以用于生产环境。

有多快

每秒 60,061 次访问,内存占用 19.9 MB。

一个 Inertia 页面(一篇文章及其评论),由一个如同 tug new 所生成的应用提供,带有会话、CSRF 和 Content-Security-Policy,运行在一个用上所有核心的进程中。

  • 60,061 每秒访问次数

    Inertia 客户端请求页面,以 JSON 响应,99% 的请求在 4.38 ms 内完成。

  • 56,146 每秒首次访问次数

    浏览器请求页面,以 HTML 响应,99% 的请求在 5.09 ms 内完成。

  • 19.9 MB 负载下的内存占用

    启动后空闲时为 6.4 MB。

  • 15 ms 启动耗时

    从启动二进制文件,到它第一次响应页面请求。

  • 9.6 MB 部署体积

    单个静态二进制文件,无需另外安装任何东西。

  • 75 ns 每个请求增加的耗时

    在单个进程中,tug 的 App 相比 Go 自带的路由器 ServeMux 多出的耗时。

于 测得,测试机器为 Apple M1 Max(10 核,macOS 27.0.1)。 全部数据,以及它们的测量方法

指南

从新建应用到部署上线。

18 个部分,按应用需要它们的顺序排列。其余细节请参阅 pkg.go.dev 上各个包自己的文档。

  1. 快速上手

    安装 tug,创建并运行应用,再添加一个页面和一个表单。

  2. 路由与处理函数

    App、路由、绝对链接与签名链接、路由组、中间件(其中包括安全响应头)、响应了请求的路由、代理之后的客户端地址、Ctx、文件、下载、流与事件、请求绑定,以及错误。

  3. 页面

    Inertia 页面及其 props、根模板及其 nonce、延后计算的 props、共享 props、重定向、页面上的下载与事件、错误页面、Vite,以及 Inertia 的 DevTools。

  4. 服务端渲染

    首次访问时,由应用旁边的 Node 在服务端渲染页面,以及 ssr 包。

  5. 表单与会话

    验证、应用自己的规则、离开字段时就检查该字段的表单、flash 消息、会话,以及 CSRF。

  6. 多语言

    用请求的语言显示 tug 和应用的文字,每种语言一个文件;lang 包,以及 tug lang。

  7. 文件

    上传的文件,按大小和实际类型检查,用 storage 包保存在应用的磁盘或 S3 上,以及指向它们的公开链接或签名链接。

  8. 账户

    认证脚手架,可用 SQLite、Postgres 或 MySQL,及其 API 令牌和通知;auth 和 mail 包,后者支持抄送、附件和一键退订链接。

  9. 数据库迁移

    数据库的表由 SQL 文件创建和修改,每个文件只运行一次,在应用启动时按顺序运行,也可以通过应用的 migrate 命令运行;用 tug migrate new 创建下一个迁移;以及 migrate 包。

  10. 授权

    用户可以对某个对象做什么,auth 包的权限(ability),拒绝时返回说明原因的 403,以及在页面的 props 中给出当前用户可以做什么。

  11. 加密

    应用的密钥,tug 用它加密和签名的内容,用来加密应用自身数据的 crypt 包,以及密钥轮换。

  12. 后台任务

    queue 包,用于比请求持续更久的工作:失败时重新运行;按计划运行,也可以按指定的时区;无论被请求多少次,都只排队一次;或者在所有实例上合计,限制同时运行多少个、每秒运行多少个;并告知任务何时彻底失败,以及每次运行的情况。

  13. 缓存

    cache 包,用于计算起来很慢的结果,存放在应用的每个实例都能找到的地方;以及锁,用于不能同时运行两次的操作。

  14. 广播

    broadcast 包,频道上的事件,由应用的数据库送到每个实例上打开的页面,页面随即重新加载变化的部分。

  15. TypeScript

    tug gen 根据 Go 代码写出的类型。

  16. 测试

    tugtest 包,即供 Go 测试使用的 Inertia 客户端,用来测试应用的页面、表单、上传和登录;用来测试邮件的 mailtest;以及应用的事件。

  17. 部署

    单个二进制文件、Dockerfile、环境变量、由应用自身提供的 HTTPS 及来自 Let's Encrypt 的证书,规定浏览器可以如何对待应用页面的响应头(其中包括 Content-Security-Policy),日志,以及指标。

  18. 命令行工具

    tug new、tug dev、tug gen、tug routes、tug lang、tug migrate、tug build 和 tug key 的完整说明。

拖船虽小,却能推动比它大许多倍的船。

创建你的第一个应用
tug