很多人问的一个关于 HTMX 的问题是,特别是那些刚试用过这个库的开发者经常会问:“但是你能真正用它做什么呢?”
非常好的问题!首先,我们将会一步一步来,通过使用HTMX和Go作为后端语言来构建一个基于数据库的简单CRUD应用。
顺便说一句,如果你想了解如何使用HTMX和Golang构建全栈应用程序的实际项目指南,可以看看我的 HTMX + Go:使用Golang和HTMX构建全栈应用程序[包含优惠]课程 .
咱们开始吧。
我们究竟在建什么?我更愿意称它为任务管理应用,但我猜你可能已经猜到了,那只是另一个待办事项应用的别致名字而已。别担心,待办事项应用非常适合学习语言、库和框架的基础操作,所以我们将继续使用这种方法。
我们的应用可以做到以下几点:
- 查看任务
- 添加新任务
- 更新现有任务以及
- 移除任务
所以首先,我们需要一个数据库。在这个演示项目中,我将使用MySQL。您可以根据自己的喜好选择任何数据库,并在阅读本文时根据您的数据库进行必要的代码调整。
我们将保持简单,不做复杂的模式设计。首先,我们创建名为 testdb
的数据库,在该数据库内创建 todos
表(你可以根据喜好给数据库和表起任何名字,但确保在 SQL 语句中使用相同的名字)
在 todos
表中,请使用以下结构:
id
: 主键,自动增长task
: VARCHAR(200) - 任务项内容done
: INT(1),默认 = 0(是否完成)
你可以选择在数据库表中添加一些任务,这样我们第一次打开应用时就能看到一些任务。
创建 超媒体 API开始设置之前,您可以在您开发计算机上的任何方便位置创建一个项目文件夹来存放这个小小的应用程序。
mkdir 创建 任务管理
在项目的根目录下运行以下命令来初始化该项目为Golang项目:
go mod init <module_name>
请将 <module_name>
替换为实际的模块名称。
go mod init 任务管理工具
接下来,我们需要安装一些依赖包。因为我们已经知道我们将使用MySQL作为我们的数据库,因此,我们需要安装用于Go语言的Go语言的MySQL驱动Golang的MySQL驱动。
我们也需要安装Gorilla Mux Router,它将是我们项目使用的路由库。请在项目根目录中运行下面的两个命令以将这些库安装到项目中。
MySQL:
go get -u github.com/go-sql-driver/mysql
使用 go get -u github.com/go-sql-driver/mysql
命令来更新 MySQL 驱动程序。
大猩猩Mux:
运行以下命令来更新 Gorilla 路由器包: go get -u github.com/gorilla/mux
有了这些库,在项目根目录下创建一个 main.go
文件,并在其中添加以下代码:
package main
import (
"database/sql"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"strings"
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
)
var tmpl *template.Template
var db *sql.DB
type Task struct {
Id int
Task string
Done bool
}
func init() {
tmpl, _ = template.ParseGlob("templates/*.html")
}
func initDB() {
var err error
db, err = sql.Open("mysql", "root:root@(127.0.0.1:3333)/testdb?parseTime=true")
if err != nil {
log.Fatal(err)
}
if err = db.Ping(); err != nil {
log.Fatal(err)
}
}
func main() {
gRouter := mux.NewRouter()
initDB()
defer db.Close() // 延迟关闭数据库连接
gRouter.HandleFunc("/", Homepage)
gRouter.HandleFunc("/tasks", fetchTasks).Methods("GET")
gRouter.HandleFunc("/newtaskform", getTaskForm)
gRouter.HandleFunc("/tasks", addTask).Methods("POST")
gRouter.HandleFunc("/gettaskupdateform/{id}", getTaskUpdateForm).Methods("GET")
gRouter.HandleFunc("/tasks/{id}", updateTask).Methods("PUT", "POST")
gRouter.HandleFunc("/tasks/{id}", deleteTask).Methods("DELETE")
http.ListenAndServe(":4000", gRouter)
}
func Homepage(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "home.html", nil)
}
func fetchTasks(w http.ResponseWriter, r *http.Request) {
todos, _ := getTasks(db)
tmpl.ExecuteTemplate(w, "todoList", todos)
}
func getTaskForm(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "addTaskForm", nil)
}
func addTask(w http.ResponseWriter, r *http.Request) {
task := r.FormValue("task")
fmt.Println(task)
query := "INSERT INTO tasks (task, done) VALUES (?, ?)"
stmt, err := db.Prepare(query)
defer stmt.Close()
_, executeErr := stmt.Exec(task, 0)
if executeErr != nil {
log.Fatal(executeErr)
}
todos, _ := getTasks(db)
tmpl.ExecuteTemplate(w, "todoList", todos) // 刷新整个任务列表以保持最新状态,但这可能不是最佳策略
}
func getTaskUpdateForm(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId, _ := strconv.Atoi(vars["id"])
task, err := getTaskByID(db, taskId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
tmpl.ExecuteTemplate(w, "updateTaskForm", task)
}
func updateTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskItem := r.FormValue("task")
taskStatus := false
fmt.Println(r.FormValue("done"))
switch strings.ToLower(r.FormValue("done")) {
case "yes", "on":
taskStatus = true
case "no", "off":
taskStatus = false
}
taskId, _ := strconv.Atoi(vars["id"])
task := Task{
taskId, taskItem, taskStatus,
}
updateErr := updateTaskById(db, task)
if updateErr != nil {
log.Fatal(updateErr)
}
todos, _ := getTasks(db)
tmpl.ExecuteTemplate(w, "todoList", todos)
}
func deleteTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId, _ := strconv.Atoi(vars["id"])
err := deleTaskWithID(db, taskId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
todos, _ := getTasks(db)
tmpl.ExecuteTemplate(w, "todoList", todos) // 返回任务列表
}
func getTasks(dbPointer *sql.DB) ([]Task, error) {
query := "SELECT id, task, done FROM tasks"
rows, err := dbPointer.Query(query)
defer rows.Close()
var tasks []Task
for rows.Next() {
var todo Task
rowErr := rows.Scan(&todo.Id, &todo.Task, &todo.Done)
if rowErr != nil {
return nil, err
}
tasks = append(tasks, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return tasks, nil
}
func getTaskByID(dbPointer *sql.DB, id int) (*Task, error) {
query := "SELECT id, task, done FROM tasks WHERE id = ?"
var task Task
row := dbPointer.QueryRow(query, id)
err := row.Scan(&task.Id, &task.Task, &task.Done)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("未找到id为%d的任务", id)
}
return nil, err
}
return &task, nil
}
func updateTaskById(dbPointer *sql.DB, task Task) error {
query := "UPDATE tasks SET task = ?, done = ? WHERE id = ?"
result, err := dbPointer.Exec(query, task.Task, task.Done, task.Id)
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
fmt.Println("没有行被更新")
} else {
fmt.Printf("%d 行被更新\n", rowsAffected)
}
return nil
}
func deleTaskWithID(dbPointer *sql.DB, id int) error {
query := "DELETE FROM tasks WHERE id = ?"
stmt, err := dbPointer.Prepare(query)
defer stmt.Close()
result, err := stmt.Exec(id)
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("未找到id为%d的任务", id)
}
fmt.Printf("已删除%d个任务\n", rowsAffected)
return nil
}
嗯,代码确实挺多。别担心,我们会从头开始讲起,一步一步来。
首先,我们需要导入所有必要的包,包括我们已经安装的MySQL驱动程序和Gorilla Mux路由器,还有一些来自Go标准库中在我们的代码操作中会用到的包。
import (
"database/sql"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"strings"
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
)
接下来,我们创建一个用于存储加载模板的 tmpl
变量和一个指向数据库连接的 db
变量,以便执行数据库任务的 db
变量。然后,我们定义一个自定义的 Task
结构体来定义任务类型。
在 init()
函数中,我们从 templates
文件夹中加载所有的模板。所有的模板都应具有 .html
扩展名,因为 HTMX 要求我们返回 HTML,因此这样设置很合理。
在项目根目录下创建一个templates
文件夹,这样我们就可以开始从那里加载所有的模板。
我们也有一个 initDB()
函数,它负责处理与数据库的连接设置过程,并返回一个指向我们数据库的指针引用。请确保将连接字符串修改为与您的数据库相匹配,例如凭据、主机名、端口号、数据库名称等。
在 main
函数中,我们初始化路由器并调用 initDB()
函数来初始化数据库。然后定义了所有的路由及其处理程序,最后我们监听端口 4000
,这就是我们提供应用服务的地方。
我们现在开始拆解我们的路由及其对应的处理程序。
- The
**GET /**
基础路由: 这是我们的基础路由,加载应用的首页。处理器Homepage
返回home.html
文件给客户端。 - The
**GET /tasks**
路由: 这个路由使用fetchTasks
处理器从数据库获取所有任务,并使用todoList
模板将它们作为一个 HTML 列表形式返回给客户端。 - The
**GET /newtaskform**
路由: 这个路由每次用户想要创建新任务或点击 添加新任务 按钮时,从服务器加载新的任务表单。它使用addTaskForm
模板来显示一个新的 HTML 表单,用于添加新任务。 - The
**POST /tasks**
路由: 这个路由调用addTask
处理器将新任务添加到数据库,并返回更新后的所有任务列表给客户端。 - The
**GET /gettaskupdateform/{id}**
路由: 使用任务的Id
加载任务到更新任务表单中,并使用updateTaskForm
模板返回此表单给客户端,当用户点击 编辑 按钮时。 - The
**PUT/POST /tasks/{id}**
路由: 使用任务Id
更新任务,使用updateTask
处理器。更新完成后,返回最新的任务列表(HTML 形式)。 - The
**DELETE /tasks/{id}**
路由: 使用deleteTask
处理器和任务Id
删除特定的任务(由Id
标识)。任务删除后,返回更新后的任务列表给客户端。
这就是应用程序中用到的所有路由和处理程序了。
你可能已经注意到我们除了在 main.go
文件中定义的路由处理程序之外还定义了一些其他函数。这些函数用于执行数据库操作,包括检索任务(getTasks
)、通过 Id
获取单个任务(getTaskByID
)、通过 Id
更新任务(updateTaskById
)以及通过任务的 Id
删除任务(deleteTaskById
)。
这些辅助函数被用于我们的路由处理程序,以帮助执行数据库操作并保持程序简洁。
创建我们的模板吧既然我们已经熟悉了超媒体API,让我们开始创建将在API调用响应中返回的HTML模板。
首先,我们在templates
文件夹里新建home.html
文件。这将加载我们任务管理软件的主页。创建文件后,添加以下代码到文件中,使之更符合中文表达习惯且简洁自然。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="<https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css>" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="<https://unpkg.com/[email protected]>"></script>
<title>待办事项应用</title>
</head>
<body>
<div class="row">
<div class="col">
<h2>任务</h2>
<div>
<a href="#" hx-get="/newtaskform" hx-target="#addTaskForm">新增任务</a>
</div>
<div id="taskList" hx-get="/tasks" hx-trigger="load" hx-swap="innerHTML">
</div>
</div>
<!-- <div class="col"> -->
<div class="col">
<h2>添加新任务</h2>
<div id="addTaskForm">
{{template "addTaskForm"}}
</div>
</div>
</div>
</body>
</html>
这个模板构成了整个应用程序的外壳和布局。我们有了基本的HTML结构,并且我还进行了一些基本的样式设置。并通过一个CDN链接也引入了HTMX库。
应用程序布局分为两部分。一部分用于显示任务,另一部分用于显示新任务和更新任务的表单。
第一个部分包含一个按钮,用于从超媒体API(即hypermedia API)请求新的任务表格。当表单返回时,我们则利用 hx-target
将表单加载至带有 id
为 addTaskForm
的 div
中,该 div
位于页面的表单区域。
<a href="#" hx-get="/newtaskform" hx-target="#addTaskForm">添加新条目</a>
接下来的部分是 div
,我们的任务将会被加载到这个 div
中。这个 div
会通过 hx-trigger
在页面加载时自动发送一个到 /tasks
的 GET
请求,从而加载任务到页面中。
<div id="taskList" hx-get="/tasks" hx-trigger="load" hx-swap="innerHTML"></div>
在第二部分,如前所述,我们有一个 id 为 addTaskForm
的 div
,用于加载新任务和更新任务表单。我们还利用 Go 模板语法预先将添加新任务的表单加载到该 div
中,以便默认显示一个表单。
现在让我们创建添加新任务的表单。在 templates
文件夹中,创建文件 addTaskForm.html
,并在其中添加以下代码:
{{define "addTaskForm"}}
<form>
<div>
<input type="text" class="form-control" name="task">
</div>
<div class="mt-2">
<button class="btn btn-primary" hx-post="/tasks" hx-target="#taskList" hx-target="innerHTML">
添加任务
</button>
</div>
</form>
{end}
此模板会在 UI 中加载一个空白表单,用于添加新任务。当你点击提交按钮时,它会通过 HTMX 发送一个 POST
请求到 /tasks
路由,以添加新任务。操作完成后,它会将更新的任务列表加载到具有 id 为 taskList
的 div
中。
下一个是我们更新任务表单模板。在 templates
文件夹中,创建文件 updateTaskForm.html
并在此文件中添加以下代码。
{{define "updateTaskForm"}}
<form>
<div>
<input type="text" class="form-control" name="task" value="{{.Task}}">
</div>
<div>
<input type="checkbox" name="done" {{if .Done}} checked {end}} id="">
</div>
<div class="mt-2">
<button class="btn btn-primary" hx-put="/tasks/{{.Id}}" hx-target="#taskList" hx-target="innerHTML">
更新任务
</button>
</div>
</form>
{end}
(Note: The suggested stylistic adjustment of "更新任务" to "更新此任务" was not applied as per expert suggestions.)
此模板接收要更新的任务,并使用它来预先填写更新任务表单,让用户了解之前的任务状态。
当点击更新任务按钮时,它会将更新后的值发送到任务的超媒体API进行更新。更新后,然后加载更新后的列表到页面。
最后,我们创建一个返回任务项列表的模板。在 templates
文件夹里,创建文件 todoList.html
并在其中添加以下代码:
{{define "todoList"}}
<ul>
{{range .}}
<li>
<span {{if .Done}} style="text-decoration:line-through" {end}>{{.Task}}</span>
[<a href="#" hx-get="/gettaskupdateform/{{.Id}}" hx-target="#addTaskForm" hx-swap="innerHTML">编辑任务</a>] |
<a href="#" hx-delete="/tasks/{{.Id}}"
hx-confirm="你真的要删除这个任务吗?"
hx-target="#taskList">[删]</a>
</li>
{end}
</ul>
{end}
这个模板内容挺多的,咱们一块儿分解一下吧。
首先,模板接受一个 Go slice
类型的 Task
切片,并用 range
函数遍历它,来生成一个 HTML 无序列表。
任务显示在每个列表项目中,并且 Done
属性用于检查任务是否完成。如果是这样的话,我们用 CSS 将已完成的任务划掉。
在任务文本之后,有一个编辑按钮。点击该按钮后会调用 /gettaskupdateform
端点,加载与所点击的任务对应的更新表单。用户可以更新任务,然后获取更新后的任务列表。
在编辑按钮之后,我们有一个删除按钮,该按钮使用hx-delete
来调用DELETE /tasks/{id}
端点以删除任务。但在我们发送删除请求之前,我们先向用户展示一个确认对话框,以确认他们是否真的想删除此任务项。一旦删除,系统将返回一个更新后的列表,任务将从列表中消失。
就这样我们结束了应用程序的部分,所以咱们进入有趣的部分,看看它。
运行应用现在代码都已经写好了,我们来测试一下这个应用。
确保所有文件已保存,然后在项目的根目录下运行以下命令:
# Command code remains unchanged
go run main.go
// 运行主程序 go run main.go
现在在浏览器中打开应用页面http://localhost:4000
。如果使用了其他端口,请确保使用正确的端口打开应用。
现在你应该看到你的应用程序,就像下面展示的那样。参见下方,我们将添加一个新任务,更新一个现有任务,然后从任务列表中删除一个任务。
如果你喜欢这篇文章,并想了解更多关于用 HTMX 建构项目的知识,我建议你看看 HTMX + Go:使用 Golang 和 HTMX 构建全栈应用程序 和 完整的 HTMX 课程:从零到精通 HTMX 以扩展你用 HTMX 构建超媒体驱动应用程序的知识。
编程愉快 :)
共同學習,寫下你的評論
評論加載中...
作者其他優質文章