如何使用 Go、Templ 和 HTMX 构建全栈应用程序
技术概述
先决条件
入门
在 Xata 上设置数据库
构建应用程序前端
整合并构建后端
结论
Go 是一种静态类型、编译型的高级编程语言,用于构建系统、命令行界面 (CLI) 等。它通常设计用于后端;然而,有时您也希望使用同一种语言来构建一个包含函数式后端和可视化前端的全栈应用程序。
大多数情况下,Go 开发者会选择 React、Vue、Angular 等前端框架/库来构建应用程序的前端部分。这意味着他们必须学习 JavaScript/TypeScript、特定于框架的范式以及其他与前端相关的开销。
在本指南中,您将学习如何使用 Templ、HTMX 和 Xata 通过 Go 构建全栈应用程序。
技术概述
Templ:是一个模板引擎,可让您使用 Go 构建 HTML。它还允许您使用 Go 语法(例如if
、switch
和for
语句)来构建强大的前端。您将使用 Templ 为前端构建可重用的组件和页面。
HTMX:这是一个前端库,可让您直接使用 HTML 而非 JavaScript 访问现代浏览器功能。您将使用 HTMX 来处理表单提交并执行其他动态操作。
Xata:是一个无服务器数据库,具有分析和自由文本搜索支持,可以轻松构建各种应用程序。
先决条件
要学习本教程,需要满足以下条件:
- 已安装 Go 1.20 或更高版本
- 对 Go 的基本了解
- Xata 账户。注册免费
入门
首先,你需要安装 Templ 二进制文件。该二进制文件会从 Templ 文件生成 Go 代码。
go install github.com/a-h/templ/cmd/templ@latest
创建目录。
mkdir go_fullstack && cd go_fullstack
接下来,初始化一个 Go 模块来管理项目依赖项。
go mod init go-fullstack
最后,我们继续安装所需的依赖项:
go get github.com/gin-gonic/gin github.com/a-h/templ github.com/joho/godotenv
github.com/gin-gonic/gin
是一个用于构建 Web 应用程序的框架。
github.com/a-h/templ
是项目中使用的Templ库。
github.com/joho/godotenv
是一个用于加载环境变量的库。
构建应用程序
为此,在我们的项目目录中创建一个cmd
、、internals
和文件夹。views
cmd
用于构建应用程序入口点。
internals
用于构建与 API 相关的文件。
views
用于构建与前端相关的文件。
在 Xata 上设置数据库
登录Xata 工作区并创建一个todo
数据库。在todo
数据库中,创建一个Todo
表并添加一个description
类型为 的列String
。
获取数据库 URL 并设置 API 密钥
要获取数据库 URL,请点击“获取代码片段”按钮并复制该 URL。然后点击“API 密钥”链接,添加新密钥,保存并复制该 API 密钥。
设置环境变量
.env
在根目录中创建一个文件并添加复制的URL和API密钥。
XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL>
XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>
构建应用程序前端
为了构建前端,您将使用 Templ 和 HTMX 来构建应用程序并为 Todo 应用程序添加动态。
创建应用程序组件
在文件夹中views
,创建一个components/header.templ
文件并添加以下代码片段:
package components
templ Header() {
<head>
<script
src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"
></script>
<script src="https://cdn.tailwindcss.com"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GO Fullstack</title>
</head>
}
该代码片段创建了一个Header
组件,并添加了 HTMX 和 TailwindCSS CDN。TailwindCSS 是一个用于样式设置的低级框架。
接下来,创建一个components/footer.templ
文件,使用 TailwindCSS 类创建应用程序页脚和样式。
package components
templ Footer() {
<footer class="fixed p-1 bottom-0 bg-gray-100 w-full border-t">
<div class="rounded-lg p-4 text-xs italic text-gray-700 text-center">
© Go Fullstack
</div>
</footer>
}
最后,index.templ
在同一个views
文件夹中创建一个文件并添加以下代码片段:
package views
import (
"fmt"
"go_fullstack/views/components"
)
type Todo struct {
Id string
Description string
}
templ Index(todos []*Todo) {
<!DOCTYPE html>
<html lang="en">
@components.Header()
<body>
<main class="min-h-screen w-full">
<nav class="flex w-full border border-b-zinc-200 px-4 py-4">
<h3 class="text-base lg:text-lg font-medium text-center">
GO Fullstack app
</h3>
</nav>
<div class="mt-6 w-full flex justify-center items-center flex-col">
// FORM PROCESSING
<form
hx-post="/"
hx-trigger="submit"
hx-swap="none"
onsubmit="reloadPage()"
class="w-96"
>
<textarea
name="description"
cols="30"
rows="2"
class="w-full border rounded-lg mb-2 p-4"
placeholder="Input todo details"
required
></textarea>
<button
class="py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800"
>
Create
</button>
</form>
<section class="border-t border-t-zinc-200 mt-6 px-2 py-4 w-96">
// LOOP THROUGH THE TODOS
<ul id="todo-list">
for _, todo := range todos {
<li class="ml-4 ml-4 border p-2 rounded-lg mb-2" id={ fmt.Sprintf("%s", todo.Id) }>
<p class="font-medium text-sm">Todo item { todo.Id }</p>
<p class="text-sm text-zinc-500 mb-2">
{ todo.Description }
</p>
<div class="flex gap-4 items-center mt-2">
<a
href="#"
class="flex items-center border py-1 px-2 rounded-lg"
>
<p class="text-sm">Edit</p>
</a>
<button
hx-delete={ fmt.Sprintf("/%s", todo.Id) }
hx-swap="delete"
hx-target={ fmt.Sprintf("#%s", todo.Id) }
class="flex items-center border py-1 px-2 rounded-lg hover:bg-red-300"
>
<p class="text-sm">Delete</p>
</button>
</div>
</li>
}
</ul>
</section>
</div>
</main>
</body>
@components.Footer()
</html>
<script>
function reloadPage() {
setTimeout(function() {
window.location.reload();
}, 2000);
}
</script>
}
上面的代码片段执行以下操作:
- 导入所需的依赖项
- 创建一个
Todo
结构体来表示来自后端的响应数据 - 创建一个
Index
使用Header
和Footer
组件构建页面的组件。然后,它使用 HTMX 属性通过调用相应的端点/
和来处理表单提交和待办事项删除。/{todo.Id}
从 Templ 文件生成 Go 文件
接下来,使用您之前安装的 Templ 二进制文件,通过在终端中运行以下命令从上面创建的视图中生成 Go 代码:
templ generate
运行此命令后,您将看到为每个视图生成新的 Go 文件。您将在下一节中使用生成的文件来渲染前端。
生成的文件不可编辑。
整合并构建后端
完成后,您可以使用它来构建后端并呈现所需的页面。
创建 API 模型和辅助函数
为了表示应用程序数据,请在文件夹model.go
内创建一个文件internals
并添加以下代码片段:
package internals
type Todo struct {
Id string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
}
type TodoRequest struct {
Description string `json:"description,omitempty"`
}
type TodoResponse struct {
Id string `json:"id,omitempty"`
}
上面的代码片段创建了一个Todo
、TodoRequest
和TodoResponse
结构,具有描述请求和响应类型所需的属性。
最后,创建一个helpers.go
具有可重用函数的文件来加载环境变量。
package internals
import (
"log"
"os"
"github.com/joho/godotenv"
)
func GetEnvVariable(key string) string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv(key)
}
创建应用程序和 API 路由
创建一个route.go
用于配置 API 路由的文件并添加以下代码片段:
package api
import "github.com/gin-gonic/gin"
type Config struct {
Router *gin.Engine
}
func (app *Config) Routes() {
//routes will come here
}
上面的代码片段执行以下操作:
- 导入所需的依赖项
- 创建一个
Config
具有Router
属性的结构来配置应用程序方法 - 创建一个以结构体为指针的
Routes
函数Config
创建 API 服务
完成后,xata_service.go
为应用程序创建一个文件并通过执行以下操作来更新它:
首先,导入所需的依赖项并创建一个辅助函数:
package internals
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
var xataAPIKey = GetEnvVariable("XATA_API_KEY")
var baseURL = GetEnvVariable("XATA_DATABASE_URL")
func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
var req *http.Request
var err error'
if method == "GET" || method == "DELETE" || bodyData == nil {
req, err = http.NewRequest(method, url, nil)
} else {
req, err = http.NewRequest(method, url, bodyData)
}
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", xataAPIKey))
return req, nil
}
上面的代码片段执行以下操作:
- 导入所需的依赖项
- 创建所需的环境变量
- 创建一个
createRequest
函数,该函数创建具有所需标头的 HTTP 请求
最后,添加createTodoService
、deleteTodoService
和getAllTodosService
方法来创建、删除和获取待办事项列表。
//imports goes here
func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
//createRequest code goes here
}
func (app *Config) createTodoService(newTodo *TodoRequest) (*TodoResponse, error) {
createTodo := TodoResponse{}
jsonData := Todo{
Description: newTodo.Description,
}
bodyData := new(bytes.Buffer)
json.NewEncoder(bodyData).Encode(jsonData)
fullURL := fmt.Sprintf("%s:main/tables/Todo/data", baseURL)
req, err := createRequest("POST", fullURL, bodyData)
if err != nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&createTodo); err != nil {
return nil, err
}
return &createTodo, nil
}
func (app *Config) deleteTodoService(id string) (string, error) {
fullURL := fmt.Sprintf("%s:main/tables/Todo/data/%s", baseURL, id)
client := &http.Client{}
req, err := createRequest("DELETE", fullURL, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
return id, nil
}
func (app *Config) getAllTodosService() ([]*Todo, error) {
var todos []*Todo
fullURL := fmt.Sprintf("%s:main/tables/Todo/query", baseURL)
client := &http.Client{}
req, err := createRequest("POST", fullURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var response struct {
Records []*Todo `json:"records"`
}
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&response); err != nil {
return nil, err
}
todos = response.Records
return todos, nil
}
创建 API 处理程序
完成后,您可以使用这些服务来创建 API 处理程序。在文件夹handler.go
中创建一个文件internals
并添加以下代码片段:
package internals
import (
"context"
"fmt"
"go_fullstack/views"
"net/http"
"time"
"github.com/a-h/templ"
"github.com/gin-gonic/gin"
)
const appTimeout = time.Second * 10
func render(ctx *gin.Context, status int, template templ.Component) error {
ctx.Status(status)
return template.Render(ctx.Request.Context(), ctx.Writer)
}
func (app *Config) indexPageHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
_, cancel := context.WithTimeout(context.Background(), appTimeout)
defer cancel()
todos, err := app.getAllTodosService()
if err != nil {
ctx.JSON(http.StatusBadRequest, err.Error())
return
}
var viewsTodos []*views.Todo
for _, todo := range todos {
viewsTodo := &views.Todo{
Id: todo.Id,
Description: todo.Description,
}
viewsTodos = append(viewsTodos, viewsTodo)
}
render(ctx, http.StatusOK, views.Index(viewsTodos))
}
}
func (app *Config) createTodoHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
_, cancel := context.WithTimeout(context.Background(), appTimeout)
description := ctx.PostForm("description")
defer cancel()
newTodo := TodoRequest{
Description: description,
}
data, err := app.createTodoService(&newTodo)
if err != nil {
ctx.JSON(http.StatusBadRequest, err.Error())
return
}
ctx.JSON(http.StatusCreated, data)
}
}
func (app *Config) deleteTodoHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
_, cancel := context.WithTimeout(context.Background(), appTimeout)
id := ctx.Param("id")
defer cancel()
data, err := app.deleteTodoService(id)
if err != nil {
ctx.JSON(http.StatusBadRequest, err.Error())
return
}
ctx.JSON(http.StatusAccepted, fmt.Sprintf("Todo with ID: %s deleted successfully!!", data))
}
}
上面的代码片段执行以下操作
- 导入所需的依赖项
- 创建一个
render
使用该Templ
包来渲染匹配模板的函数 - 创建一个
indexPageHandler
函数,返回一个 Gin-gonic 处理程序,并接受Config
结构体作为指针。在返回的处理程序中,使用该getAllTodosService
服务获取待办事项列表,然后使用 views 包(前端)生成的代码渲染相应的页面。 - 创建一个
createdTodoHandler
和deleteProjectHandler
函数,返回一个 Gin-gonic 处理程序,并接收Config
结构体作为指针。使用之前创建的服务在返回的处理程序中执行相应的操作。
更新 API 路由以使用处理程序
使用处理程序更新routes.go
文件,如下所示:
package internals
import (
"github.com/gin-gonic/gin"
)
type Config struct {
Router *gin.Engine
}
func (app *Config) Routes() {
//views
app.Router.GET("/", app.indexPageHandler())
//apis
app.Router.POST("/", app.createTodoHandler())
app.Router.DELETE("/:id", app.deleteTodoHandler())
}
整合起来
创建用于提供路由的应用程序入口点。为此,请在文件夹main.go
中创建一个文件cmd
并添加以下代码片段:
package main
import (
"go_fullstack/internals"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
//initialize config
app := internals.Config{Router: router}
//routes
app.Routes()
router.Run(":8080")
}
上面的代码片段执行以下操作:
- 导入所需的依赖项
Default
使用配置创建 Gin 路由器Config
通过传入Router
- 添加路由并在端口上运行应用程序
:8080
完成后,您可以使用以下命令启动开发服务器:
go run cmd/main.go
完整的源代码可以在GitHub上找到。
结论
本文讨论如何使用 Go、Templ、HTMX 和 Xata 构建一个全栈应用程序。您可以进一步扩展该应用程序,以支持查看和编辑待办事项。
以下资源也可能有帮助:
文章来源:https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444