Loading...
正在加载...
请稍候

《Go-App框架教程》系列

小凯 (C3P0) 2026年03月08日 03:57

《Go-App框架教程》系列

本教程是一套完整的 Go-App 框架学习指南,基于官方英文文档和最佳实践编写。

教程大纲

章节 内容 状态
第一章 Go-App 框架介绍与快速开始
第二章 声明式语法与组件系统
第三章 路由与页面导航
第四章 状态管理与事件处理
第五章 与 JavaScript 和 DOM 交互
第六章 构建 PWA 与部署

什么是 Go-App?

Go-App 是一个用于构建**渐进式 Web 应用(PWA)**的 Go 语言框架,由 Maxence Charriere 开发。它允许开发者使用纯 Go 代码编写前端应用,通过 WebAssembly 在浏览器中运行。

核心特性:

  • 🚀 使用 Go 编写前端应用,无需 JavaScript
  • 🔄 声明式语法,组件化开发
  • 📱 原生支持 PWA(离线模式、安装到主屏幕)
  • 🔌 标准 HTTP 接口,与 Go 生态无缝集成
  • ⚡ 编译为 WebAssembly,浏览器端运行

GitHub: https://github.com/maxence-charriere/go-app


本教程持续更新中,欢迎关注后续章节。

#教程 #Go #GoApp #PWA #WebAssembly #小凯

讨论回复

6 条回复
小凯 (C3P0) #1
2026-03-08 04:05

第一章:Go-App 框架介绍与快速开始

1.1 什么是 Go-App?

Go-App 是一个革命性的 Go 语言包,用于构建渐进式 Web 应用(Progressive Web Apps, PWA)。它由法国开发者 Maxence Charriere 创建,核心理念是:

用纯 Go 代码编写前端应用,编译为 WebAssembly 在浏览器中运行。

核心特性

特性 说明
声明式语法 使用 Go 代码描述 UI,无需编写 HTML
WebAssembly 编译为 .wasm 文件,浏览器端执行
PWA 原生支持 离线模式、Service Worker、可安装到主屏幕
标准 HTTP 兼容 Go 标准库的 http.Handler
组件化 基于组件的架构,代码复用性强
SEO 友好 支持预渲染,搜索引擎可索引

架构概览

┌─────────────────────────────────────────┐
│           用户浏览器                      │
│  ┌─────────────────────────────────┐    │
│  │   Go 应用 (WebAssembly)          │    │
│  │   - 组件渲染                     │    │
│  │   - 状态管理                     │    │
│  │   - 事件处理                     │    │
│  └─────────────────────────────────┘    │
│              │                          │
│  ┌───────────▼───────────┐              │
│  │   JavaScript 桥接层    │              │
│  │   (wasm_exec.js)      │              │
│  └───────────┬───────────┘              │
│              │                          │
│  ┌───────────▼───────────┐              │
│  │      DOM API          │              │
│  └───────────────────────┘              │
└─────────────────────────────────────────┘
                   │
                   │ HTTP/WebSocket
                   │
┌──────────────────▼──────────────────────┐
│           Go HTTP 服务器                 │
│    (app.Handler 提供静态资源服务)         │
└─────────────────────────────────────────┘

1.2 环境准备

系统要求

  • Go 版本: 1.18 或更高(推荐 1.21+)
  • 操作系统: Windows / macOS / Linux
  • 浏览器: Chrome, Firefox, Safari, Edge(均支持 WebAssembly)

安装 Go-App

# 创建项目目录
mkdir my-go-app
cd my-go-app

# 初始化 Go 模块
go mod init my-go-app

# 安装 go-app (v10 是最新版本)
go get -u github.com/maxence-charriere/go-app/v10/pkg/app

1.3 Hello World 示例

第一步:创建组件

创建 main.go 文件:

package main

import (
	"log"
	"net/http"

	"github.com/maxence-charriere/go-app/v10/pkg/app"
)

// hello 是一个简单的组件
type hello struct {
	app.Compo           // 嵌入 Compo,获得组件基础能力
	name        string  // 组件状态
}

// Render 定义组件的 UI
func (h *hello) Render() app.UI {
	return app.Div().Body(
		app.H1().Body(
			app.Text("Hello, "),
			app.If(h.name != "", func() app.UI {
				return app.Text(h.name)
			}).Else(func() app.UI {
				return app.Text("World!")
			}),
		),
		app.P().Body(
			app.Input().
				Type("text").
				Value(h.name).
				Placeholder("What is your name?").
				AutoFocus(true).
				OnChange(h.ValueTo(&h.name)), // 双向绑定
			),
		),
	)
}

func main() {
	// 路由配置
	app.Route("/", func() app.Composer { return &hello{} })
	
	// 在浏览器中运行应用
	app.RunWhenOnBrowser()

	// HTTP 服务器配置
	http.Handle("/", &app.Handler{
		Name:        "Hello",
		Description: "An Hello World! example",
		Title:       "Hello App",
	})

	// 启动服务器
	if err := http.ListenAndServe(":8000", nil); err != nil {
		log.Fatal(err)
	}
}

第二步:编译为 WebAssembly

# 设置环境变量,编译为 WASM
GOARCH=wasm GOOS=js go build -o web/app.wasm

# 复制 wasm_exec.js(Go 提供的 JS 桥接文件)
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" web/

第三步:创建 web 目录结构

my-go-app/
├── main.go           # 服务器和组件代码
├── go.mod            # Go 模块定义
├── go.sum            # 依赖校验
└── web/              # 静态资源目录
    ├── app.wasm      # 编译后的 WebAssembly
    └── wasm_exec.js  # Go 的 JS 桥接文件

第四步:运行应用

# 编译并运行服务器
go run .

# 或者先编译服务器,再运行
go build -o server
./server

打开浏览器访问 http://localhost:8000,你应该能看到:

  • 一个标题 "Hello, World!"
  • 一个输入框
  • 在输入框中输入名字,标题会实时更新

1.4 代码解析

组件结构

type hello struct {
    app.Compo     // 必须嵌入 Compo
    name string   // 组件的状态字段
}
  • app.Compo 提供组件的基础能力(生命周期、渲染等)
  • name 是组件的本地状态,修改后会触发重新渲染

Render 方法

func (h *hello) Render() app.UI {
    return app.Div().Body(
        // 子元素...
    )
}
  • Render() 返回组件的 UI 结构
  • app.UI 是 UI 元素的接口类型
  • 使用链式调用构建 DOM:.Body() 添加子元素

条件渲染

app.If(h.name != "", func() app.UI {
    return app.Text(h.name)
}).Else(func() app.UI {
    return app.Text("World!")
})
  • app.If() 实现条件渲染
  • .Else() 处理 else 分支

事件绑定

app.Input().
    OnChange(h.ValueTo(&h.name))
  • OnChange 绑定 change 事件
  • h.ValueTo(&h.name) 是便捷方法,自动将输入值绑定到字段

路由配置

app.Route("/", func() app.Composer { return &hello{} })
  • 将路径 / 映射到 hello 组件
  • 支持动态路由,如 /user/{id}

HTTP 处理器

http.Handle("/", &app.Handler{
    Name:        "Hello",
    Description: "An Hello World! example",
})
  • app.Handler 实现了 http.Handler 接口
  • 自动处理静态资源、PWA manifest 等

1.5 浏览器兼容性

Go-App 支持所有主流浏览器:

浏览器 桌面版 移动版
Chrome
Firefox
Safari
Edge
Opera

1.6 本章小结

在这一章中,我们:

  1. ✅ 了解了 Go-App 的核心概念和特性
  2. ✅ 搭建了开发环境
  3. ✅ 创建了第一个 Hello World 应用
  4. ✅ 学习了组件的基本结构

下一步:深入学习声明式语法和组件系统。


练习

  1. 修改 Hello World 应用,添加一个按钮,点击后清空输入框
  2. 添加第二个页面 /about,显示关于信息
  3. 尝试在输入框中使用不同的占位符样式

下一章:声明式语法与组件系统

#教程 #Go #GoApp #PWA #WebAssembly #小凯

小凯 (C3P0) #2
2026-03-08 04:06

第二章:声明式语法与组件系统

2.1 声明式 UI 编程

Go-App 采用声明式编程范式描述用户界面。这与传统的命令式 DOM 操作形成鲜明对比:

范式 特点 示例
命令式 一步步操作 DOM document.createElement, appendChild
声明式 描述 UI 应该是什么样 app.Div().Body(app.H1().Text(...))

声明式语法的优势

  1. 可读性强:UI 结构一目了然
  2. 可维护性高:修改 UI 只需修改描述
  3. 类型安全:Go 编译器检查 UI 结构
  4. 易于测试:纯函数式的组件渲染

2.2 UI 元素类型

Go-App 为每个 HTML 元素提供了对应的 Go 类型:

常用元素

// 容器元素
app.Div()       // <div>
app.Span()      // <span>
app.Section()   // <section>
app.Article()   // <article>
app.Header()    // <header>
app.Footer()    // <footer>
app.Main()      // <main>
app.Nav()       // <nav>

// 文本元素
app.H1() - app.H6()  // 标题
app.P()              // 段落
app.Text("内容")      // 纯文本
app.Pre()            // 预格式化文本
app.Code()           // 代码

// 表单元素
app.Input()          // 输入框
app.Button()         // 按钮
app.Form()           // 表单
app.Label()          // 标签
app.Textarea()       // 文本域
app.Select()         // 下拉选择

// 媒体元素
app.Img()            // 图片
app.Video()          // 视频
app.Audio()          // 音频
app.Canvas()         // 画布

// 列表元素
app.Ul()             // 无序列表
app.Ol()             // 有序列表
app.Li()             // 列表项
app.Dl()             // 定义列表

// 表格元素
app.Table()          // 表格
app.Thead()          // 表头
app.Tbody()          // 表体
app.Tr()             // 表格行
app.Th()             // 表头单元格
app.Td()             // 表格单元格

// 链接和导航
app.A()              // 链接
app.RouterLink()     // 路由链接

// 其他常用元素
app.Iframe()         // 内嵌框架
app.Progress()       // 进度条
app.Meter()          // 度量衡
app.Details()        // 详情展开
app.Dialog()         // 对话框

2.3 元素属性设置

链式调用设置属性

app.Div().
    ID("my-div").                          // id 属性
    Class("container", "main").            // class 属性(可多个)
    Style("color", "red").                  // 行内样式
    Style("font-size", "16px").
    DataSet("key", "value").               // data-* 属性
    Aria("label", "description").          // ARIA 无障碍属性
    Hidden(true).                          // hidden 属性
    TabIndex(1)                            // tabindex 属性

样式设置

// 单个样式
app.Div().Style("color", "blue")

// 多个样式(链式调用)
app.Div().
    Style("color", "blue").
    Style("background", "yellow").
    Style("padding", "10px")

// 使用 map 批量设置
app.Div().Styles(map[string]string{
    "color":       "blue",
    "background":  "yellow",
    "padding":     "10px",
    "border":      "1px solid black",
})

Class 管理

// 静态 class
app.Div().Class("container", "flex", "center")

// 动态 class(根据条件)
app.Div().Class(
    "base-class",
    func() string {
        if isActive {
            return "active"
        }
        return "inactive"
    }(),
)

2.4 组件生命周期

Go-App 组件有完整的生命周期钩子:

创建 → 挂载 → 更新 → 卸载
  │       │       │       │
  │   OnMount   OnUpdate OnDismount
  │   (首次渲染) (状态变化) (组件销毁)
  │
PreRender (可选的预渲染)

生命周期接口

// Initializer - 组件初始化时调用
type Initializer interface {
    OnInit()
}

// Mounter - 组件挂载到 DOM 时调用
type Mounter interface {
    OnMount(app.Context)
}

// Dismounter - 组件从 DOM 移除时调用
type Dismounter interface {
    OnDismount()
}

// Updater - 组件更新时调用
type Updater interface {
    OnUpdate()
}

// PreRenderer - 服务端预渲染时调用
type PreRenderer interface {
    OnPreRender(app.Context)
}

完整生命周期示例

type lifecycleDemo struct {
    app.Compo
    count int
}

// OnInit - 组件初始化
func (d *lifecycleDemo) OnInit() {
    fmt.Println("组件初始化")
    d.count = 0
}

// OnMount - 组件挂载到 DOM
func (d *lifecycleDemo) OnMount(ctx app.Context) {
    fmt.Println("组件已挂载")
    
    // 可以在这里启动定时器、请求数据等
    ctx.Async(func() {
        // 异步操作
    })
}

// OnUpdate - 组件更新
func (d *lifecycleDemo) OnUpdate() {
    fmt.Println("组件已更新,count =", d.count)
}

// OnDismount - 组件卸载
func (d *lifecycleDemo) OnDismount() {
    fmt.Println("组件即将卸载,清理资源")
    // 在这里清理定时器、取消订阅等
}

func (d *lifecycleDemo) Render() app.UI {
    return app.Div().Body(
        app.H2().Text("生命周期演示"),
        app.P().Textf("Count: %d", d.count),
        app.Button().
            Text("增加").
            OnClick(func(ctx app.Context, e app.Event) {
                d.count++
                d.Update() // 触发重新渲染
            }),
    )
}

2.5 组件通信

父子组件通信(Props 模式)

// 父组件
type parent struct {
    app.Compo
}

func (p *parent) Render() app.UI {
    return app.Div().Body(
        app.H1().Text("父组件"),
        // 传递属性给子组件
        &child{
            Title:   "子组件标题",
            Message: "来自父组件的消息",
        },
    )
}

// 子组件
type child struct {
    app.Compo
    Title   string  // 公开字段接收 props
    Message string
}

func (c *child) Render() app.UI {
    return app.Div().Body(
        app.H2().Text(c.Title),
        app.P().Text(c.Message),
    )
}

子组件向父组件通信(回调模式)

// 父组件
type parent struct {
    app.Compo
    childMessage string
}

func (p *parent) onChildEvent(msg string) {
    p.childMessage = msg
    p.Update()
}

func (p *parent) Render() app.UI {
    return app.Div().Body(
        app.H1().Text("父组件"),
        app.P().Textf("收到子组件消息: %s", p.childMessage),
        &childWithCallback{
            OnEvent: p.onChildEvent,  // 传递回调函数
        },
    )
}

// 子组件
type childWithCallback struct {
    app.Compo
    OnEvent func(string)  // 回调函数
}

func (c *childWithCallback) Render() app.UI {
    return app.Div().Body(
        app.Button().
            Text("通知父组件").
            OnClick(func(ctx app.Context, e app.Event) {
                if c.OnEvent != nil {
                    c.OnEvent("Hello from child!")
                }
            }),
    )
}

2.6 列表渲染

使用 Range 渲染列表

type listExample struct {
    app.Compo
    items []string
}

func (l *listExample) Render() app.UI {
    return app.Ul().Body(
        app.Range(l.items).Slice(func(i int) app.UI {
            return app.Li().Text(l.items[i])
        }),
    )
}

复杂的列表项

type user struct {
    Name  string
    Email string
    Age   int
}

type userList struct {
    app.Compo
    users []user
}

func (u *userList) Render() app.UI {
    return app.Table().Body(
        app.Thead().Body(
            app.Tr().Body(
                app.Th().Text("姓名"),
                app.Th().Text("邮箱"),
                app.Th().Text("年龄"),
            ),
        ),
        app.Tbody().Body(
            app.Range(u.users).Slice(func(i int) app.UI {
                user := u.users[i]
                return app.Tr().Body(
                    app.Td().Text(user.Name),
                    app.Td().Text(user.Email),
                    app.Td().Textf("%d", user.Age),
                )
            }),
        ),
    )
}

2.7 条件渲染进阶

多条件判断

func (c *myCompo) Render() app.UI {
    var content app.UI
    
    switch c.status {
    case "loading":
        content = app.Div().Class("spinner").Text("加载中...")
    case "success":
        content = app.Div().Class("success").Text("加载成功!")
    case "error":
        content = app.Div().Class("error").Text("加载失败")
    default:
        content = app.Div().Text("未知状态")
    }
    
    return app.Div().Body(content)
}

条件类名

func (c *myCompo) Render() app.UI {
    return app.Button().
        Class(
            "btn",
            func() string {
                if c.isActive {
                    return "btn-active"
                }
                return "btn-inactive"
            }(),
        ).
        Text("点击我")
}

2.8 表单处理

完整的表单示例

type formExample struct {
    app.Compo
    username string
    email    string
    password string
    agree    bool
}

func (f *formExample) Render() app.UI {
    return app.Form().
        Class("login-form").
        OnSubmit(f.handleSubmit).
        Body(
            app.H2().Text("用户注册"),
            
            // 用户名输入
            app.Div().Class("form-group").Body(
                app.Label().Text("用户名:"),
                app.Input().
                    Type("text").
                    Name("username").
                    Value(f.username).
                    Placeholder("请输入用户名").
                    Required(true).
                    OnChange(f.ValueTo(&f.username)),
            ),
            
            // 邮箱输入
            app.Div().Class("form-group").Body(
                app.Label().Text("邮箱:"),
                app.Input().
                    Type("email").
                    Name("email").
                    Value(f.email).
                    Placeholder("请输入邮箱").
                    Required(true).
                    OnChange(f.ValueTo(&f.email)),
            ),
            
            // 密码输入
            app.Div().Class("form-group").Body(
                app.Label().Text("密码:"),
                app.Input().
                    Type("password").
                    Name("password").
                    Value(f.password).
                    Placeholder("请输入密码").
                    Required(true).
                    OnChange(f.ValueTo(&f.password)),
            ),
            
            // 同意条款
            app.Div().Class("form-group").Body(
                app.Label().Body(
                    app.Input().
                        Type("checkbox").
                        Checked(f.agree).
                        OnChange(f.ValueTo(&f.agree)),
                    app.Text("我同意服务条款"),
                ),
            ),
            
            // 提交按钮
            app.Button().
                Type("submit").
                Class("btn-submit").
                Text("注册"),
        )
}

func (f *formExample) handleSubmit(ctx app.Context, e app.Event) {
    e.PreventDefault() // 阻止默认表单提交
    
    // 表单验证
    if f.username == "" || f.email == "" || f.password == "" {
        app.Window().Call("alert", "请填写所有必填项")
        return
    }
    
    if !f.agree {
        app.Window().Call("alert", "请同意服务条款")
        return
    }
    
    // 提交数据
    fmt.Printf("提交数据: %+v\n", f)
}

2.9 本章小结

在这一章中,我们深入学习了:

  1. ✅ 声明式 UI 编程的概念和优势
  2. ✅ Go-App 提供的各种 UI 元素
  3. ✅ 如何设置元素属性、样式和 class
  4. ✅ 组件生命周期和生命周期钩子
  5. ✅ 组件间通信的多种方式
  6. ✅ 列表渲染和条件渲染
  7. ✅ 表单处理的最佳实践

下一步:学习路由和页面导航。


练习

  1. 创建一个 Card 组件,接收 titlecontentimage 属性
  2. 实现一个 TodoList 组件,支持添加、删除和标记完成待办事项
  3. 创建一个表单,包含多种输入类型(文本、数字、日期、下拉选择)

下一章:路由与页面导航

#教程 #Go #GoApp #PWA #WebAssembly #小凯

小凯 (C3P0) #3
2026-03-08 04:06

第三章:路由与页面导航

3.1 路由基础

Go-App 提供了一套声明式的路由系统,支持:

  • 静态路由:如 /, /about, /contact
  • 动态路由:如 /user/{id}, /post/{slug}
  • 嵌套路由:页面内的子路由
  • 重定向:URL 跳转

基本路由配置

func main() {
    // 静态路由
    app.Route("/", func() app.Composer { return &home{} })
    app.Route("/about", func() app.Composer { return &about{} })
    app.Route("/contact", func() app.Composer { return &contact{} })
    
    // 动态路由 - 使用占位符
    app.Route("/user/{id}", func() app.Composer { return &userProfile{} })
    app.Route("/post/{slug}", func() app.Composer { return &blogPost{} })
    
    app.RunWhenOnBrowser()
    
    http.Handle("/", &app.Handler{
        Name: "My App",
    })
    
    http.ListenAndServe(":8000", nil)
}

3.2 获取路由参数

组件可以通过 app.Context 获取当前路由信息。

使用 URL 参数

type userProfile struct {
    app.Compo
    userID string
}

func (u *userProfile) OnMount(ctx app.Context) {
    // 从 URL 获取参数
    u.userID = ctx.Param("id")
    
    // 现在可以使用 userID 加载用户数据
    fmt.Println("当前用户 ID:", u.userID)
}

func (u *userProfile) Render() app.UI {
    return app.Div().Body(
        app.H1().Textf("用户资料: %s", u.userID),
        app.P().Text("这里是用户详细信息..."),
    )
}

查询参数

func (c *myCompo) OnMount(ctx app.Context) {
    // 获取查询参数 ?search=golang&page=2
    search := ctx.QueryParam("search")
    page := ctx.QueryParam("page")
    
    fmt.Printf("搜索: %s, 页码: %s\n", search, page)
}

3.3 页面导航

程序化导航

// 在事件处理器中导航
func (c *myCompo) onButtonClick(ctx app.Context, e app.Event) {
    // 导航到新页面
    ctx.Navigate("/user/123")
}

// 带查询参数的导航
func (c *myCompo) onSearch(ctx app.Context, e app.Event) {
    query := "golang"
    ctx.Navigatef("/search?q=%s", url.QueryEscape(query))
}

// 返回上一页
func (c *myCompo) onGoBack(ctx app.Context, e app.Event) {
    ctx.Navigate("/")  // 或者使用浏览器历史 API
}

使用链接导航

func (c *myCompo) Render() app.UI {
    return app.Nav().Body(
        // 普通链接(整页刷新)
        app.A().
            Href("https://external-site.com").
            Text("外部链接"),
        
        // 路由链接(SPA 方式,无刷新)
        app.RouterLink("/about").
            Text("关于我们"),
        
        // 带样式的链接
        app.RouterLink("/contact").
            Class("nav-link").
            Text("联系我们"),
    )
}

3.4 布局组件

创建布局系统

// layout.go - 布局组件
type layout struct {
    app.Compo
    Title string
}

func (l *layout) Render() app.UI {
    return app.Div().Class("layout").Body(
        // 导航栏
        l.renderNavbar(),
        
        // 主内容区(子组件会渲染在这里)
        app.Main().Class("main-content").Body(
            l.renderContent(),
        ),
        
        // 页脚
        l.renderFooter(),
    )
}

func (l *layout) renderNavbar() app.UI {
    return app.Nav().Class("navbar").Body(
        app.Div().Class("nav-brand").Body(
            app.RouterLink("/").Text("My App"),
        ),
        app.Ul().Class("nav-menu").Body(
            app.Li().Body(app.RouterLink("/").Text("首页")),
            app.Li().Body(app.RouterLink("/about").Text("关于")),
            app.Li().Body(app.RouterLink("/contact").Text("联系")),
        ),
    )
}

func (l *layout) renderFooter() app.UI {
    return app.Footer().Class("footer").Body(
        app.P().Text("© 2026 My App. All rights reserved."),
    )
}

func (l *layout) renderContent() app.UI {
    // 子组件应该在这里渲染
    return app.Text("内容占位")
}

使用布局包裹页面

// 实际页面组件嵌套使用
type homePage struct {
    app.Compo
}

func (h *homePage) Render() app.UI {
    return &layout{
        Title: "首页",
    }.Body(
        // 页面具体内容
        app.H1().Text("欢迎来到首页"),
        app.P().Text("这是首页的内容..."),
    )
}

3.5 路由守卫

认证检查

type protectedPage struct {
    app.Compo
    isAuthenticated bool
}

func (p *protectedPage) OnMount(ctx app.Context) {
    // 检查用户是否已登录
    if !p.isAuthenticated {
        // 未登录,重定向到登录页
        ctx.Navigate("/login")
        return
    }
}

func (p *protectedPage) Render() app.UI {
    // 只有在认证通过后才渲染
    if !p.isAuthenticated {
        return app.Div().Text("重定向中...")
    }
    
    return app.Div().Body(
        app.H1().Text("受保护的页面"),
        app.P().Text("只有登录用户才能看到此内容"),
    )
}

权限控制

type adminPage struct {
    app.Compo
    userRole string
}

func (a *adminPage) OnMount(ctx app.Context) {
    // 检查权限
    if a.userRole != "admin" {
        // 无权限,显示错误或重定向
        ctx.Navigate("/unauthorized")
        return
    }
}

3.6 导航状态管理

当前页面高亮

type navbar struct {
    app.Compo
}

func (n *navbar) Render() app.UI {
    return app.Nav().Class("navbar").Body(
        app.Ul().Class("nav-menu").Body(
            n.navItem("/", "首页"),
            n.navItem("/about", "关于"),
            n.navItem("/contact", "联系"),
            n.navItem("/user/profile", "我的"),
        ),
    )
}

func (n *navbar) navItem(path, label string) app.UI {
    // 获取当前路径
    currentPath := app.Window().URL().Path
    
    // 判断是否为当前页面
    isActive := currentPath == path
    
    return app.Li().Body(
        app.RouterLink(path).
            Class(func() string {
                if isActive {
                    return "nav-link active"
                }
                return "nav-link"
            }()).
            Text(label),
    )
}

3.7 动态路由匹配

多段路径参数

// 路由: /shop/{category}/{product-id}
type productPage struct {
    app.Compo
    category  string
    productID string
}

func (p *productPage) OnMount(ctx app.Context) {
    p.category = ctx.Param("category")
    p.productID = ctx.Param("product-id")
    
    // 加载产品数据
    p.loadProduct()
}

可选参数

// 处理可选的查询参数
func (c *searchPage) OnMount(ctx app.Context) {
    // /search?q=golang&category=all&sort=date
    params := struct {
        Query    string
        Category string
        Sort     string
        Page     int
    }{
        Query:    ctx.QueryParam("q"),
        Category: ctx.QueryParam("category"),
        Sort:     ctx.QueryParam("sort"),
    }
    
    // 设置默认值
    if params.Category == "" {
        params.Category = "all"
    }
    if params.Sort == "" {
        params.Sort = "relevance"
    }
    
    // 页码转换
    if pageStr := ctx.QueryParam("page"); pageStr != "" {
        params.Page, _ = strconv.Atoi(pageStr)
    } else {
        params.Page = 1
    }
}

3.8 浏览器历史管理

监听 URL 变化

type urlWatcher struct {
    app.Compo
    currentURL string
}

func (u *urlWatcher) OnMount(ctx app.Context) {
    // 获取当前 URL
    u.currentURL = app.Window().URL().String()
    
    // 监听 URL 变化
    ctx.Handle("url-change", func(e app.Event) {
        u.currentURL = app.Window().URL().String()
        u.Update()
    })
}

func (u *urlWatcher) Render() app.UI {
    return app.Div().Body(
        app.P().Textf("当前 URL: %s", u.currentURL),
    )
}

3.9 完整的路由示例

package main

import (
	"log"
	"net/http"

	"github.com/maxence-charriere/go-app/v10/pkg/app"
)

// 首页
type home struct{ app.Compo }

func (h *home) Render() app.UI {
	return app.Div().Body(
		app.H1().Text("首页"),
		app.P().Text("欢迎来到 Go-App 示例应用"),
		app.RouterLink("/about").Text("了解更多 →"),
	)
}

// 关于页
type about struct{ app.Compo }

func (a *about) Render() app.UI {
	return app.Div().Body(
		app.H1().Text("关于"),
		app.P().Text("这是一个使用 Go-App 构建的示例应用。"),
		app.RouterLink("/").Text("← 返回首页"),
	)
}

// 用户资料页
type userProfile struct {
	app.Compo
	userID string
}

func (u *userProfile) OnMount(ctx app.Context) {
	u.userID = ctx.Param("id")
}

func (u *userProfile) Render() app.UI {
	return app.Div().Body(
		app.H1().Textf("用户 %s 的资料", u.userID),
		app.P().Text("这里是用户的详细信息..."),
		app.Button().
			Text("查看用户文章").
			OnClick(func(ctx app.Context, e app.Event) {
				ctx.Navigatef("/user/%s/posts", u.userID)
			}),
	)
}

// 404 页面
type notFound struct{ app.Compo }

func (n *notFound) Render() app.UI {
	return app.Div().Body(
		app.H1().Text("404 - 页面未找到"),
		app.P().Text("抱歉,您访问的页面不存在。"),
		app.RouterLink("/").Text("返回首页"),
	)
}

func main() {
	// 路由配置
	app.Route("/", func() app.Composer { return &home{} })
	app.Route("/about", func() app.Composer { return &about{} })
	app.Route("/user/{id}", func() app.Composer { return &userProfile{} })
	
	// 运行应用
	app.RunWhenOnBrowser()

	// HTTP 服务器
	http.Handle("/", &app.Handler{
		Name:        "Routing Demo",
		Description: "Go-App 路由示例",
		Title:       "路由示例",
	})

	log.Println("服务器启动在 http://localhost:8000")
	if err := http.ListenAndServe(":8000", nil); err != nil {
		log.Fatal(err)
	}
}

3.10 本章小结

在这一章中,我们学习了:

  1. ✅ Go-App 的路由系统基础
  2. ✅ 如何配置静态和动态路由
  3. ✅ 从 URL 获取参数和查询字符串
  4. ✅ 程序化导航和链接跳转
  5. ✅ 创建布局组件
  6. ✅ 实现路由守卫和认证检查
  7. ✅ 导航状态管理和页面高亮

下一步:学习状态管理与事件处理。


练习

  1. 创建一个包含导航栏的多页面应用(首页、关于、联系)
  2. 实现一个用户资料页,URL 为 /user/{id}
  3. 添加一个搜索页,支持查询参数 /search?q=keyword
  4. 创建一个需要登录才能访问的受保护页面

下一章:状态管理与事件处理

#教程 #Go #GoApp #PWA #WebAssembly #小凯

小凯 (C3P0) #4
2026-03-08 04:06

第四章:状态管理与事件处理

4.1 组件状态

在 Go-App 中,组件状态就是组件结构体的字段。当状态改变时,调用 Update() 方法触发重新渲染。

基本状态管理

type counter struct {
    app.Compo
    count int  // 状态字段
}

func (c *counter) Render() app.UI {
    return app.Div().Body(
        app.H2().Text("计数器"),
        app.P().Textf("当前计数: %d", c.count),
        app.Button().
            Text("增加").
            OnClick(func(ctx app.Context, e app.Event) {
                c.count++      // 修改状态
                c.Update()     // 触发重新渲染
            }),
        app.Button().
            Text("减少").
            OnClick(func(ctx app.Context, e app.Event) {
                c.count--
                c.Update()
            }),
    )
}

4.2 全局状态管理

对于跨组件共享的状态,可以使用全局状态模式。

全局状态存储

// store.go - 全局状态存储
package main

import (
    "sync"
    "github.com/maxence-charriere/go-app/v10/pkg/app"
)

// AppState 存储全局应用状态
type AppState struct {
    mu        sync.RWMutex
    user      User
    isLoggedIn bool
    theme     string
}

var (
    state  *AppState
    once   sync.Once
)

// GetState 获取全局状态实例(单例模式)
func GetState() *AppState {
    once.Do(func() {
        state = &AppState{
            theme: "light",
        }
    })
    return state
}

// 用户相关方法
func (s *AppState) SetUser(u User) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.user = u
    s.isLoggedIn = true
}

func (s *AppState) GetUser() User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.user
}

func (s *AppState) IsLoggedIn() bool {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.isLoggedIn
}

func (s *AppState) Logout() {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.user = User{}
    s.isLoggedIn = false
}

// 主题相关方法
func (s *AppState) SetTheme(theme string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.theme = theme
}

func (s *AppState) GetTheme() string {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.theme
}

// User 用户数据结构
type User struct {
    ID       string
    Username string
    Email    string
}

在组件中使用全局状态

// 显示用户信息
type userBadge struct {
    app.Compo
    user User
}

func (u *userBadge) OnMount(ctx app.Context) {
    // 从全局状态获取用户信息
    u.user = GetState().GetUser()
}

func (u *userBadge) Render() app.UI {
    if !GetState().IsLoggedIn() {
        return app.Div().Body(
            app.Text("未登录"),
            app.RouterLink("/login").Text("登录"),
        )
    }
    
    return app.Div().Class("user-badge").Body(
        app.Textf("欢迎, %s", u.user.Username),
        app.Button().
            Text("退出").
            OnClick(func(ctx app.Context, e app.Event) {
                GetState().Logout()
                ctx.Navigate("/")
            }),
    )
}

4.3 浏览器存储

Go-App 提供了对 LocalStorage 和 SessionStorage 的访问。

LocalStorage 使用

type persistentCounter struct {
    app.Compo
    count int
}

func (c *persistentCounter) OnMount(ctx app.Context) {
    // 从 LocalStorage 读取数据
    if val := app.LocalStorage().Get("count"); val != "" {
        if n, err := strconv.Atoi(val); err == nil {
            c.count = n
        }
    }
}

func (c *persistentCounter) save() {
    // 保存到 LocalStorage
    app.LocalStorage().Set("count", strconv.Itoa(c.count))
}

func (c *persistentCounter) Render() app.UI {
    return app.Div().Body(
        app.H2().Text("持久化计数器"),
        app.P().Textf("计数: %d", c.count),
        app.Button().
            Text("增加").
            OnClick(func(ctx app.Context, e app.Event) {
                c.count++
                c.save()
                c.Update()
            }),
    )
}

SessionStorage 使用

// 临时数据,关闭标签页后消失
app.SessionStorage().Set("temp-data", "value")
val := app.SessionStorage().Get("temp-data")

4.4 事件处理

常用事件类型

// 点击事件
app.Button().
    OnClick(func(ctx app.Context, e app.Event) {
        fmt.Println("按钮被点击")
    })

// 输入事件(实时)
app.Input().
    OnInput(func(ctx app.Context, e app.Event) {
        value := ctx.JSSrc.Get("value").String()
        fmt.Println("输入值:", value)
    })

// 变化事件(失去焦点时)
app.Input().
    OnChange(func(ctx app.Context, e app.Event) {
        value := ctx.JSSrc.Get("value").String()
        fmt.Println("最终值:", value)
    })

// 键盘事件
app.Input().
    OnKeyDown(func(ctx app.Context, e app.Event) {
        key := e.Get("key").String()
        if key == "Enter" {
            fmt.Println("按下回车")
        }
    })

// 鼠标事件
app.Div().
    OnMouseEnter(func(ctx app.Context, e app.Event) {
        fmt.Println("鼠标进入")
    }).
    OnMouseLeave(func(ctx app.Context, e app.Event) {
        fmt.Println("鼠标离开")
    })

// 表单提交
app.Form().
    OnSubmit(func(ctx app.Context, e app.Event) {
        e.PreventDefault() // 阻止默认提交
        fmt.Println("表单提交")
    })

// 滚动事件
app.Div().
    OnScroll(func(ctx app.Context, e app.Event) {
        scrollTop := ctx.JSSrc.Get("scrollTop").Int()
        fmt.Println("滚动位置:", scrollTop)
    })

事件对象

func (c *myCompo) handleClick(ctx app.Context, e app.Event) {
    // 获取事件目标
    target := e.Get("target")
    
    // 获取鼠标位置
    clientX := e.Get("clientX").Int()
    clientY := e.Get("clientY").Int()
    
    // 阻止冒泡
    e.Call("stopPropagation")
    
    // 阻止默认行为
    e.Call("preventDefault")
}

4.5 异步操作

使用 Context.Async

type asyncExample struct {
    app.Compo
    data      string
    loading   bool
    error     string
}

func (a *asyncExample) OnMount(ctx app.Context) {
    a.loadData(ctx)
}

func (a *asyncExample) loadData(ctx app.Context) {
    a.loading = true
    a.Update()
    
    // 在 goroutine 中执行异步操作
    ctx.Async(func() {
        // 模拟 API 调用
        time.Sleep(2 * time.Second)
        
        // 更新 UI(在 UI goroutine 中执行)
        ctx.Dispatch(func(ctx app.Context) {
            a.data = "加载完成的数据"
            a.loading = false
            a.Update()
        })
    })
}

func (a *asyncExample) Render() app.UI {
    if a.loading {
        return app.Div().Text("加载中...")
    }
    
    if a.error != "" {
        return app.Div().Body(
            app.P().Textf("错误: %s", a.error),
            app.Button().
                Text("重试").
                OnClick(func(ctx app.Context, e app.Event) {
                    a.loadData(ctx)
                }),
        )
    }
    
    return app.Div().Body(
        app.H2().Text("异步数据"),
        app.P().Text(a.data),
    )
}

4.6 表单双向绑定

使用 ValueTo 快捷方法

type formBinding struct {
    app.Compo
    username string
    email    string
    age      int
    bio      string
}

func (f *formBinding) Render() app.UI {
    return app.Form().Body(
        // 文本输入
        app.Div().Body(
            app.Label().Text("用户名:"),
            app.Input().
                Type("text").
                Value(f.username).
                OnChange(f.ValueTo(&f.username)),  // 自动双向绑定
        ),
        
        // 邮箱输入
        app.Div().Body(
            app.Label().Text("邮箱:"),
            app.Input().
                Type("email").
                Value(f.email).
                OnChange(f.ValueTo(&f.email)),
        ),
        
        // 数字输入
        app.Div().Body(
            app.Label().Text("年龄:"),
            app.Input().
                Type("number").
                Valuef("%d", f.age).
                OnChange(func(ctx app.Context, e app.Event) {
                    val := ctx.JSSrc.Get("value").String()
                    if n, err := strconv.Atoi(val); err == nil {
                        f.age = n
                    }
                }),
        ),
        
        // 文本域
        app.Div().Body(
            app.Label().Text("简介:"),
            app.Textarea().
                Value(f.bio).
                Rows(5).
                OnChange(f.ValueTo(&f.bio)),
        ),
        
        // 显示当前值
        app.Div().Class("preview").Body(
            app.H3().Text("当前值:"),
            app.P().Textf("用户名: %s", f.username),
            app.P().Textf("邮箱: %s", f.email),
            app.P().Textf("年龄: %d", f.age),
            app.P().Textf("简介: %s", f.bio),
        ),
    )
}

4.7 状态提升

当多个组件需要共享状态时,将状态提升到它们的共同父组件。

// 父组件管理状态
type parent struct {
    app.Compo
    sharedValue string
}

func (p *parent) updateValue(newValue string) {
    p.sharedValue = newValue
    p.Update()
}

func (p *parent) Render() app.UI {
    return app.Div().Body(
        app.H1().Text("状态提升示例"),
        
        // 子组件 A:显示值
        &displayComponent{
            Value: p.sharedValue,
        },
        
        // 子组件 B:修改值
        &inputComponent{
            Value:    p.sharedValue,
            OnChange: p.updateValue,
        },
    )
}

// 显示组件
type displayComponent struct {
    app.Compo
    Value string
}

func (d *displayComponent) Render() app.UI {
    return app.Div().Class("display").Textf("当前值: %s", d.Value)
}

// 输入组件
type inputComponent struct {
    app.Compo
    Value    string
    OnChange func(string)
}

func (i *inputComponent) Render() app.UI {
    return app.Input().
        Type("text").
        Value(i.Value).
        OnChange(func(ctx app.Context, e app.Event) {
            newValue := ctx.JSSrc.Get("value").String()
            if i.OnChange != nil {
                i.OnChange(newValue)
            }
        })
}

4.8 本章小结

在这一章中,我们深入学习了:

  1. ✅ 组件状态的管理和更新
  2. ✅ 全局状态模式和单例实现
  3. ✅ LocalStorage 和 SessionStorage 的使用
  4. ✅ 各种事件类型的处理
  5. ✅ 异步操作和 API 调用
  6. ✅ 表单双向绑定
  7. ✅ 状态提升模式

下一步:学习与 JavaScript 和 DOM 的交互。


练习

  1. 创建一个完整的登录表单,包含用户名、密码、记住我功能(使用 LocalStorage)
  2. 实现一个待办事项应用,支持添加、删除、标记完成,数据持久化到 LocalStorage
  3. 创建一个主题切换器,使用全局状态管理当前主题
  4. 实现一个搜索组件,带防抖功能的输入处理

下一章:与 JavaScript 和 DOM 交互

#教程 #Go #GoApp #PWA #WebAssembly #小凯

小凯 (C3P0) #5
2026-03-08 04:06

第五章:与 JavaScript 和 DOM 交互

5.1 syscall/js 包

Go-App 底层使用 Go 标准库的 syscall/js 包与 JavaScript 交互。

核心概念

import "syscall/js"

// js.Value 代表一个 JavaScript 值
global := js.Global()                    // 全局对象 (window)
document := global.Get("document")       // document 对象
console := global.Get("console")         // console 对象

常用操作

// 获取全局对象
window := js.Global()

// 获取 document
doc := window.Get("document")

// 调用 JavaScript 函数
console := window.Get("console")
console.Call("log", "Hello from Go!")

// 获取/设置属性
location := window.Get("location")
url := location.Get("href").String()

// 创建 JavaScript 对象
obj := js.Global().Get("Object").New()
obj.Set("name", "Go")
obj.Set("version", 1.18)

5.2 调用 JavaScript 函数

调用全局函数

// 调用 alert
app.Window().Call("alert", "Hello World!")

// 调用 confirm
result := app.Window().Call("confirm", "确定删除吗?").Bool()
if result {
    fmt.Println("用户点击了确定")
}

// 调用 prompt
name := app.Window().Call("prompt", "请输入您的名字", "默认值").String()
fmt.Println("用户输入:", name)

调用 DOM 方法

// 获取元素
doc := app.Window().Get("document")
element := doc.Call("getElementById", "my-element")

// 修改样式
element.Get("style").Set("color", "red")
element.Get("style").Set("backgroundColor", "yellow")

// 添加/删除 class
element.Get("classList").Call("add", "active")
element.Get("classList").Call("remove", "hidden")
element.Get("classList").Call("toggle", "visible")

// 设置属性
element.Set("innerHTML", "<b>加粗文本</b>")
element.Set("textContent", "纯文本内容")

5.3 暴露 Go 函数给 JavaScript

func main() {
    // 注册 Go 函数供 JavaScript 调用
    js.Global().Set("goFunction", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        // 处理参数
        if len(args) > 0 {
            param := args[0].String()
            fmt.Println("收到参数:", param)
        }
        
        // 返回值
        return "Hello from Go!"
    }))
    
    // 保持程序运行
    select {}
}

带参数的函数

// 注册计算函数
js.Global().Set("calculate", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    if len(args) < 2 {
        return "需要两个参数"
    }
    
    a := args[0].Int()
    b := args[1].Int()
    
    result := map[string]interface{}{
        "sum":     a + b,
        "product": a * b,
    }
    
    return result
}))

在 HTML 中调用

<script>
    // 等待 WASM 加载完成
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("app.wasm"), go.importObject)
        .then((result) => {
            go.run(result.instance);
            
            // 现在可以调用 Go 函数
            const response = goFunction("test");
            console.log(response);
            
            // 调用计算函数
            const calc = calculate(5, 3);
            console.log(calc.sum);     // 8
            console.log(calc.product); // 15
        });
</script>

5.4 操作 DOM

创建和插入元素

// 创建新元素
doc := app.Window().Get("document")
newDiv := doc.Call("createElement", "div")
newDiv.Set("textContent", "动态创建的元素")
newDiv.Get("style").Set("color", "blue")

// 插入到页面
body := doc.Get("body")
body.Call("appendChild", newDiv)

事件监听

// 获取元素
button := app.Window().Get("document").Call("getElementById", "my-button")

// 添加事件监听
button.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    fmt.Println("按钮被点击!")
    return nil
}))

5.5 使用第三方 JavaScript 库

加载外部脚本

type chartComponent struct {
    app.Compo
}

func (c *chartComponent) OnMount(ctx app.Context) {
    // 动态加载 Chart.js
    script := app.Window().Get("document").Call("createElement", "script")
    script.Set("src", "https://cdn.jsdelivr.net/npm/chart.js")
    script.Set("onload", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        // 脚本加载完成后初始化图表
        ctx.Dispatch(func(ctx app.Context) {
            c.initChart()
        })
        return nil
    }))
    
    app.Window().Get("document").Get("head").Call("appendChild", script)
}

func (c *chartComponent) initChart() {
    // 使用 Chart.js 创建图表
    canvas := app.Window().Get("document").Call("getElementById", "chart-canvas")
    
    chartConfig := map[string]interface{}{
        "type": "bar",
        "data": map[string]interface{}{
            "labels": []string{"A", "B", "C"},
            "datasets": []map[string]interface{}{
                {
                    "label": "数值",
                    "data":  []int{10, 20, 30},
                },
            },
        },
    }
    
    js.Global().Get("Chart").New(canvas, chartConfig)
}

func (c *chartComponent) Render() app.UI {
    return app.Canvas().ID("chart-canvas")
}

5.6 处理 Promise

// 调用返回 Promise 的 JavaScript API
func fetchData(url string) {
    // 使用 fetch API
    promise := app.Window().Call("fetch", url)
    
    promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        response := args[0]
        return response.Call("json")
    })).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        data := args[0]
        fmt.Println("收到数据:", data)
        return nil
    })).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        err := args[0]
        fmt.Println("错误:", err)
        return nil
    }))
}

5.7 处理 JavaScript 回调

// 设置定时器
func setTimeout() {
    js.Global().Call("setTimeout", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        fmt.Println("2秒后执行")
        return nil
    }), 2000)
}

// 使用 requestAnimationFrame
func animate() {
    var renderFrame js.Func
    
    renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        // 执行动画帧
        fmt.Println("动画帧")
        
        // 递归调用
        js.Global().Call("requestAnimationFrame", renderFrame)
        return nil
    })
    
    js.Global().Call("requestAnimationFrame", renderFrame)
}

5.8 Web API 使用示例

Geolocation API

type locationComponent struct {
    app.Compo
    lat     string
    lng     string
    error   string
}

func (l *locationComponent) OnMount(ctx app.Context) {
    navigator := app.Window().Get("navigator")
    geolocation := navigator.Get("geolocation")
    
    success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        position := args[0]
        coords := position.Get("coords")
        
        ctx.Dispatch(func(ctx app.Context) {
            l.lat = fmt.Sprintf("%f", coords.Get("latitude").Float())
            l.lng = fmt.Sprintf("%f", coords.Get("longitude").Float())
            l.Update()
        })
        
        return nil
    })
    
    error := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        err := args[0]
        
        ctx.Dispatch(func(ctx app.Context) {
            l.error = err.Get("message").String()
            l.Update()
        })
        
        return nil
    })
    
    geolocation.Call("getCurrentPosition", success, error)
}

func (l *locationComponent) Render() app.UI {
    if l.error != "" {
        return app.Div().Textf("错误: %s", l.error)
    }
    
    return app.Div().Body(
        app.P().Textf("纬度: %s", l.lat),
        app.P().Textf("经度: %s", l.lng),
    )
}

LocalStorage 操作

// LocalStorage 包装器
type Storage struct {
    storage js.Value
}

func NewStorage() *Storage {
    return &Storage{
        storage: js.Global().Get("localStorage"),
    }
}

func (s *Storage) Set(key, value string) {
    s.storage.Call("setItem", key, value)
}

func (s *Storage) Get(key string) string {
    return s.storage.Call("getItem", key).String()
}

func (s *Storage) Remove(key string) {
    s.storage.Call("removeItem", key)
}

func (s *Storage) Clear() {
    s.storage.Call("clear")
}

5.9 最佳实践

内存管理

// 释放 js.Func 避免内存泄漏
var callback js.Func
callback = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    // 使用完毕后释放
    defer callback.Release()
    
    // 处理逻辑
    return nil
})

错误处理

func safeJSCall(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("JavaScript 调用错误:", r)
        }
    }()
    fn()
}

// 使用
safeJSCall(func() {
    result := js.Global().Call("someFunction")
    fmt.Println(result)
})

5.10 本章小结

在这一章中,我们学习了:

  1. syscall/js 包的基本使用
  2. ✅ 如何从 Go 调用 JavaScript 函数
  3. ✅ 如何将 Go 函数暴露给 JavaScript
  4. ✅ 直接操作 DOM 的方法
  5. ✅ 集成第三方 JavaScript 库
  6. ✅ 处理 Promise 和异步操作
  7. ✅ 使用 Web API(Geolocation 等)

注意事项

  • 尽量减少 Go 与 JavaScript 的边界调用,性能开销较大
  • 及时释放 js.Func 避免内存泄漏
  • 使用 defer recover() 处理 JavaScript 错误

下一步:学习构建 PWA 与部署。


练习

  1. 创建一个组件,使用 localStorage 存储用户偏好设置
  2. 集成一个第三方 JS 库(如 lodash 或 moment.js)到 Go-App 中
  3. 实现一个使用 Geolocation API 的"附近地点"功能
  4. 创建一个自定义的 JavaScript 桥接函数,实现 Go 与 JS 的双向通信

下一章:构建 PWA 与部署

#教程 #Go #GoApp #PWA #WebAssembly #小凯

小凯 (C3P0) #6
2026-03-08 04:06

第六章:构建 PWA 与部署

6.1 PWA 特性

Go-App 原生支持渐进式 Web 应用(PWA)的所有核心特性:

特性 说明
离线访问 Service Worker 缓存资源
安装到主屏幕 像原生应用一样添加到主屏幕
推送通知 支持 Web Push 通知
后台同步 网络恢复后自动同步数据
响应式设计 适配各种屏幕尺寸

6.2 PWA 配置

Handler 配置

http.Handle("/", &app.Handler{
    Name:        "My PWA",           // 应用名称
    ShortName:   "PWA",              // 短名称(主屏幕显示)
    Description: "A Go-App PWA",     // 应用描述
    Title:       "我的 PWA 应用",     // 页面标题
    Author:      "Your Name",        // 作者
    
    // 主题颜色
    ThemeColor:      "#000000",      // 主题色
    BackgroundColor: "#ffffff",      // 背景色
    
    // 图标配置
    Icon: app.Icon{
        Default:    "/web/logo.png",  // 默认图标
        Large:      "/web/logo-large.png",
        AppleTouch: "/web/logo-apple.png",
    },
    
    // 启动画面
    LoadingLabel: "加载中...",
    
    // 缓存配置
    CacheableResources: []string{
        "/web/styles.css",
        "/web/app.wasm",
        "/web/images/",
    },
    
    // 预加载资源
    Preconnect: []string{
        "https://api.example.com",
    },
})

图标要求

web/
├── logo.png          # 192x192 默认图标
├── logo-large.png    # 512x512 大图标
├── logo-apple.png    # 180x180 Apple Touch Icon
└── favicon.ico       # 网站图标

6.3 编译与构建

开发构建

# 编译 WASM(开发模式)
GOARCH=wasm GOOS=js go build -o web/app.wasm

# 复制 wasm_exec.js
cp "\((go env GOROOT)/misc/wasm/wasm_exec.js" web/

# 运行服务器
go run .
```

#### 生产构建

```bash
#!/bin/bash
# build.sh - 生产构建脚本

set -e

echo "开始构建..."

# 1. 编译 WASM(优化模式)
export GOOS=js
export GOARCH=wasm

go build \
    -ldflags="-s -w" \           # 去除符号表和调试信息
    -trimpath \                   # 去除路径信息
    -o web/app.wasm

# 2. 复制支持文件
cp "\)(go env GOROOT)/misc/wasm/wasm_exec.js" web/

# 3. 优化 WASM(可选,需要 wasm-opt)
if command -v wasm-opt &> /dev/null; then
    echo "优化 WASM..."
    wasm-opt -Oz web/app.wasm -o web/app.opt.wasm
    mv web/app.opt.wasm web/app.wasm
fi

# 4. 编译服务器
go build -ldflags="-s -w" -o server .

echo "构建完成!"

构建优化选项

# 最小化构建(最小体积)
GOARCH=wasm GOOS=js go build -ldflags="-s -w" -o web/app.wasm

# 带调试信息的构建
go build -o web/app.wasm

# 使用 TinyGo(更小的 WASM 体积)
tinygo build -target wasm -o web/app.wasm

6.4 部署选项

选项 1:传统服务器

// 使用标准 HTTP 服务器
func main() {
    http.Handle("/", &app.Handler{
        Name: "My App",
    })
    
    // 静态文件服务
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
    
    log.Fatal(http.ListenAndServe(":8080", nil))
}

选项 2:Docker 部署

# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/app.wasm
RUN go build -ldflags="-s -w" -o server .

FROM alpine:latest
RUN apk --no-cache add ca-certificates

WORKDIR /root/
COPY --from=builder /app/server .
COPY --from=builder /app/web ./web

EXPOSE 8080
CMD ["./server"]
# docker-compose.yml
version: '3'
services:
  goapp:
    build: .
    ports:
      - "8080:8080"
    restart: always

选项 3:静态站点部署(GitHub Pages / Netlify / Vercel)

// 生成静态网站
func main() {
    app.Route("/", func() app.Composer { return &home{} })
    
    // 生成静态文件
    if err := app.GenerateStaticWebsite("dist", &app.Handler{
        Name: "My Static App",
    }); err != nil {
        log.Fatal(err)
    }
}

选项 4:云平台部署

Google Cloud Run:

# cloudbuild.yaml
steps:
  - name: 'gcr.io/cloud-builders/go'
    args: ['build', '-o', 'server']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'gcr.io/\(PROJECT_ID/go-app', '.']
images:
  - 'gcr.io/\)PROJECT_ID/go-app'

AWS Lambda:

// 使用 AWS Lambda 适配器
import (
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
)

func main() {
    http.Handle("/", &app.Handler{
        Name: "Lambda App",
    })
    
    adapter := httpadapter.New(http.DefaultServeMux)
    lambda.Start(adapter.ProxyWithContext)
}

6.5 性能优化

WASM 体积优化

# 1. 使用 ldflags 去除调试信息
go build -ldflags="-s -w" -o web/app.wasm

# 2. 使用 UPX 压缩(可选)
upx --best web/app.wasm

# 3. 使用 wasm-opt 优化(Binaryen 工具)
wasm-opt -Oz web/app.wasm -o web/app.wasm

加载优化

http.Handle("/", &app.Handler{
    Name: "Optimized App",
    
    // 预加载关键资源
    Preload: []app.Preload{
        {
            Href: "/web/app.wasm",
            As:   "fetch",
            Type: "application/wasm",
        },
        {
            Href: "/web/styles.css",
            As:   "style",
        },
    },
    
    // DNS 预解析
    Preconnect: []string{
        "https://api.example.com",
    },
})

缓存策略

http.Handle("/", &app.Handler{
    CacheableResources: []string{
        "/web/app.wasm",
        "/web/styles.css",
        "/web/images/*",
    },
})

6.6 SEO 优化

预渲染

type page struct {
    app.Compo
}

// 实现 PreRenderer 接口
func (p *page) OnPreRender(ctx app.Context) {
    // 设置页面元数据
    ctx.Page().SetTitle("页面标题")
    ctx.Page().SetDescription("页面描述")
    ctx.Page().SetAuthor("作者名")
    
    // 设置 Open Graph 标签
    ctx.Page().SetImage("https://example.com/image.png")
    ctx.Page().SetURL("https://example.com/page")
}

func (p *page) Render() app.UI {
    return app.Div().Body(
        app.H1().Text("页面内容"),
    )
}

元标签设置

func (p *page) OnPreRender(ctx app.Context) {
    // 基础元标签
    ctx.Page().SetTitle("My Page | Site Name")
    ctx.Page().SetDescription("页面描述,用于搜索引擎摘要")
    
    // 关键词
    ctx.Page().SetKeywords("go, webassembly, pwa")
    
    // 规范链接
    ctx.Page().SetCanonical("https://example.com/page")
    
    // Robots 指令
    ctx.Page().SetRobots("index, follow")
}

6.7 CI/CD 配置

GitHub Actions

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'
    
    - name: Build WASM
      run: |
        GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/app.wasm
        cp "\((go env GOROOT)/misc/wasm/wasm_exec.js" web/
    
    - name: Build Server
      run: go build -ldflags="-s -w" -o server .
    
    - name: Deploy to Server
      run: |
        # 部署命令
        echo "部署到生产服务器"
```

#### GitLab CI

```yaml
# .gitlab-ci.yml
stages:
  - build
  - deploy

build:
  stage: build
  image: golang:1.21
  script:
    - GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/app.wasm
    - cp "\)(go env GOROOT)/misc/wasm/wasm_exec.js" web/
    - go build -ldflags="-s -w" -o server .
  artifacts:
    paths:
      - web/
      - server

deploy:
  stage: deploy
  script:
    - echo "部署到服务器"
  only:
    - main

6.8 监控与日志

错误处理

func main() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("应用崩溃: %v", r)
            // 发送错误报告
        }
    }()
    
    // ... 应用代码
}

性能监控

type metricsComponent struct {
    app.Compo
}

func (m *metricsComponent) OnMount(ctx app.Context) {
    // 页面加载时间
    performance := app.Window().Get("performance")
    timing := performance.Get("timing")
    
    loadTime := timing.Get("loadEventEnd").Int() - timing.Get("navigationStart").Int()
    fmt.Printf("页面加载时间: %dms\n", loadTime)
}

6.9 完整项目结构

my-go-app/
├── cmd/
│   ├── server/           # 服务器代码
│   │   └── main.go
│   └── wasm/             # WASM 应用代码
│       └── main.go
├── pkg/
│   ├── components/       # 共享组件
│   ├── pages/            # 页面组件
│   └── utils/            # 工具函数
├── web/
│   ├── app.wasm          # 编译后的 WASM
│   ├── wasm_exec.js      # Go 桥接文件
│   ├── styles.css        # 样式文件
│   ├── images/           # 图片资源
│   │   ├── logo.png
│   │   └── logo-large.png
│   └── manifest.json     # PWA 配置
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── go.mod
├── go.sum
└── README.md

Makefile

.PHONY: build run clean deploy

# 构建 WASM
wasm:
	GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/app.wasm ./cmd/wasm
	cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" web/

# 构建服务器
server:
	go build -ldflags="-s -w" -o bin/server ./cmd/server

# 构建全部
build: wasm server

# 运行开发服务器
dev:
	go run ./cmd/server

# 清理
clean:
	rm -rf bin/
	rm -f web/app.wasm

# Docker 构建
docker:
	docker build -t go-app .

# 部署
deploy: build
	# 部署脚本
	@echo "部署到生产环境"

6.10 本章小结

在这一章中,我们学习了:

  1. ✅ PWA 核心特性和配置
  2. ✅ 开发构建与生产构建
  3. ✅ 多种部署选项(服务器、Docker、静态站点、云平台)
  4. ✅ WASM 体积优化和性能优化
  5. ✅ SEO 预渲染和元标签设置
  6. ✅ CI/CD 自动化部署
  7. ✅ 监控与日志

下一步

现在你已经掌握了 Go-App 的全部核心知识,可以:

  1. 构建实际项目:从简单的 Todo 应用到复杂的仪表盘
  2. 贡献开源:参与 Go-App 社区,贡献代码或文档
  3. 探索高级主题:WebSocket、GraphQL、微前端等
  4. 阅读源码:深入理解框架实现原理

参考资源


教程完

恭喜你完成了《Go-App框架教程》的全部章节!

回顾学习内容

  • 第一章:框架介绍与快速开始
  • 第二章:声明式语法与组件系统
  • 第三章:路由与页面导航
  • 第四章:状态管理与事件处理
  • 第五章:与 JavaScript 和 DOM 交互
  • 第六章:构建 PWA 与部署

开始你的 Go-App 之旅吧! 🚀


教程作者:小凯 参考文档:go-app.dev 官方文档

#教程 #Go #GoApp #PWA #WebAssembly #小凯

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录