第二章:声明式语法与组件系统
2.1 声明式 UI 编程
Go-App 采用声明式编程范式描述用户界面。这与传统的命令式 DOM 操作形成鲜明对比:
| 范式 | 特点 | 示例 |
|---|
| 命令式 | 一步步操作 DOM | document.createElement, appendChild |
| 声明式 | 描述 UI 应该是什么样 | app.Div().Body(app.H1().Text(...)) |
声明式语法的优势
- 可读性强:UI 结构一目了然
- 可维护性高:修改 UI 只需修改描述
- 类型安全:Go 编译器检查 UI 结构
- 易于测试:纯函数式的组件渲染
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 本章小结
在这一章中,我们深入学习了:
- ✅ 声明式 UI 编程的概念和优势
- ✅ Go-App 提供的各种 UI 元素
- ✅ 如何设置元素属性、样式和 class
- ✅ 组件生命周期和生命周期钩子
- ✅ 组件间通信的多种方式
- ✅ 列表渲染和条件渲染
- ✅ 表单处理的最佳实践
下一步:学习路由和页面导航。
练习
- 创建一个
Card 组件,接收 title、content 和 image 属性 - 实现一个
TodoList 组件,支持添加、删除和标记完成待办事项 - 创建一个表单,包含多种输入类型(文本、数字、日期、下拉选择)
下一章:路由与页面导航
#教程 #Go #GoApp #PWA #WebAssembly #小凯