本教程是一套完整的 Go-App 框架学习指南,基于官方英文文档和最佳实践编写。
| 章节 | 内容 | 状态 |
|---|---|---|
| 第一章 | Go-App 框架介绍与快速开始 | ✅ |
| 第二章 | 声明式语法与组件系统 | ✅ |
| 第三章 | 路由与页面导航 | ✅ |
| 第四章 | 状态管理与事件处理 | ✅ |
| 第五章 | 与 JavaScript 和 DOM 交互 | ✅ |
| 第六章 | 构建 PWA 与部署 | ✅ |
Go-App 是一个用于构建渐进式 Web 应用(PWA)的 Go 语言框架,由 Maxence Charriere 开发。它允许开发者使用纯 Go 代码编写前端应用,通过 WebAssembly 在浏览器中运行。
核心特性:
本教程持续更新中,欢迎关注后续章节。
#教程 #Go #GoApp #PWA #WebAssembly #小凯
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 提供静态资源服务) │
└─────────────────────────────────────────┘
# 创建项目目录
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
创建 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)
}
}
# 设置环境变量,编译为 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/
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,你应该能看到:
type hello struct {
app.Compo // 必须嵌入 Compo
name string // 组件的状态字段
}
app.Compo 提供组件的基础能力(生命周期、渲染等)name 是组件的本地状态,修改后会触发重新渲染func (h *hello) Render() app.UI {
return app.Div().Body(
// 子元素...
)
}
Render() 返回组件的 UI 结构app.UI 是 UI 元素的接口类型.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.Handle("/", &app.Handler{
Name: "Hello",
Description: "An Hello World! example",
})
app.Handler 实现了 http.Handler 接口Go-App 支持所有主流浏览器:
| 浏览器 | 桌面版 | 移动版 |
|---|---|---|
| Chrome | ✅ | ✅ |
| Firefox | ✅ | ✅ |
| Safari | ✅ | ✅ |
| Edge | ✅ | ✅ |
| Opera | ✅ | ✅ |
在这一章中,我们:
/about,显示关于信息下一章:声明式语法与组件系统
#教程 #Go #GoApp #PWA #WebAssembly #小凯
Go-App 采用声明式编程范式描述用户界面。这与传统的命令式 DOM 操作形成鲜明对比:
| 范式 | 特点 | 示例 |
|---|---|---|
| 命令式 | 一步步操作 DOM | document.createElement, appendChild |
| 声明式 | 描述 UI 应该是什么样 | app.Div().Body(app.H1().Text(...)) |
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() // 对话框
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
app.Div().Class("container", "flex", "center")
// 动态 class(根据条件)
app.Div().Class(
"base-class",
func() string {
if isActive {
return "active"
}
return "inactive"
}(),
)
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() // 触发重新渲染
}),
)
}
// 父组件
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!")
}
}),
)
}
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),
)
}),
),
)
}
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("点击我")
}
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)
}
在这一章中,我们深入学习了:
Card 组件,接收 title、content 和 image 属性TodoList 组件,支持添加、删除和标记完成待办事项下一章:路由与页面导航
#教程 #Go #GoApp #PWA #WebAssembly #小凯
Go-App 提供了一套声明式的路由系统,支持:
/, /about, /contact/user/{id}, /post/{slug}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)
}
组件可以通过 app.Context 获取当前路由信息。
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)
}
// 在事件处理器中导航
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("联系我们"),
)
}
// 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("这是首页的内容..."),
)
}
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
}
}
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),
)
}
// 路由: /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
}
}
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),
)
}
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)
}
}
在这一章中,我们学习了:
/user/{id}/search?q=keyword下一章:状态管理与事件处理
#教程 #Go #GoApp #PWA #WebAssembly #小凯
在 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()
}),
)
}
对于跨组件共享的状态,可以使用全局状态模式。
// 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("/")
}),
)
}
Go-App 提供了对 LocalStorage 和 SessionStorage 的访问。
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()
}),
)
}
// 临时数据,关闭标签页后消失
app.SessionStorage().Set("temp-data", "value")
val := app.SessionStorage().Get("temp-data")
// 点击事件
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")
}
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),
)
}
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),
),
)
}
当多个组件需要共享状态时,将状态提升到它们的共同父组件。
// 父组件管理状态
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)
}
})
}
在这一章中,我们深入学习了:
下一章:与 JavaScript 和 DOM 交互
#教程 #Go #GoApp #PWA #WebAssembly #小凯
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)
// 调用 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)
// 获取元素
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", "纯文本内容")
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
}))
<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>
// 创建新元素
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
}))
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")
}
// 调用返回 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
}))
}
// 设置定时器
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)
}
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 包装器
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")
}
// 释放 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)
})
在这一章中,我们学习了:
syscall/js 包的基本使用js.Func 避免内存泄漏defer recover() 处理 JavaScript 错误localStorage 存储用户偏好设置下一章:构建 PWA 与部署
#教程 #Go #GoApp #PWA #WebAssembly #小凯
Go-App 原生支持渐进式 Web 应用(PWA)的所有核心特性:
| 特性 | 说明 |
|---|---|
| 离线访问 | Service Worker 缓存资源 |
| 安装到主屏幕 | 像原生应用一样添加到主屏幕 |
| 推送通知 | 支持 Web Push 通知 |
| 后台同步 | 网络恢复后自动同步数据 |
| 响应式设计 | 适配各种屏幕尺寸 |
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 # 网站图标
# 编译 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 .
#!/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
// 使用标准 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))
}
# 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
// 生成静态网站
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)
}
}
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)
}
# 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/*",
},
})
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")
}
# .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.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
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)
}
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
.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 "部署到生产环境"
在这一章中,我们学习了:
现在你已经掌握了 Go-App 的全部核心知识,可以:
| 资源 | 链接 |
|---|---|
| Go-App 官方文档 | https://go-app.dev |
| GitHub 仓库 | https://github.com/maxence-charriere/go-app |
| Go WebAssembly | https://github.com/golang/go/wiki/WebAssembly |
| PWA 文档 | https://web.dev/progressive-web-apps/ |
恭喜你完成了《Go-App框架教程》的全部章节!
回顾学习内容:
教程作者:小凯
参考文档:go-app.dev 官方文档
#教程 #Go #GoApp #PWA #WebAssembly #小凯