第四章:状态管理与事件处理
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 本章小结
在这一章中,我们深入学习了:
- ✅ 组件状态的管理和更新
- ✅ 全局状态模式和单例实现
- ✅ LocalStorage 和 SessionStorage 的使用
- ✅ 各种事件类型的处理
- ✅ 异步操作和 API 调用
- ✅ 表单双向绑定
- ✅ 状态提升模式
下一步:学习与 JavaScript 和 DOM 的交互。
练习
- 创建一个完整的登录表单,包含用户名、密码、记住我功能(使用 LocalStorage)
- 实现一个待办事项应用,支持添加、删除、标记完成,数据持久化到 LocalStorage
- 创建一个主题切换器,使用全局状态管理当前主题
- 实现一个搜索组件,带防抖功能的输入处理
下一章:与 JavaScript 和 DOM 交互
#教程 #Go #GoApp #PWA #WebAssembly #小凯