특히 라이브러리를 방금 사용해 본 개발자들 사이에서 HTMX에 관해 많이 돌아다니는 질문 중 하나는 "그런데 실제로 이 라이브러리로 무엇을 만들 수 있습니까?"입니다.
훌륭한 질문입니다. 이 기사에서는 HTMX와 Go를 백엔드 언어로 사용하여 데이터베이스 지원 CRUD 애플리케이션을 구축하는 간단한 단계부터 시작하겠습니다.
그런데 HTMX를 사용하여 풀스택 앱을 구축하는 방법에 대한 실용적인 프로젝트 기반 가이드를 정말로 원한다면 **HTMX + Go: Golang 및 HTMX를 사용하여 풀스택 애플리케이션 구축 [할인 포함] 과정을 확인하세요.**
시작해 보겠습니다.
저는 이것을 작업 관리 애플리케이션이라고 부르고 싶지만 여러분은 이미 그것이 다른 Todo 애플리케이션의 멋진 이름이라는 것을 알고 있습니다. 걱정하지 마십시오. Todo 앱은 언어, 라이브러리 및 프레임워크의 기본 작업을 학습하는 데 적합하므로 우리는 테스트되고 신뢰할 수 있는 동일한 전략을 사용할 것입니다.
우리 애플리케이션은 다음을 수행할 수 있습니다:
먼저 데이터베이스가 필요하며, 이 데모 프로젝트에서는 MySQL을 사용하겠습니다. 이 기사를 따라가면서 원하는 데이터베이스를 자유롭게 사용하고 필요한 코드를 변경하여 데이터베이스를 참조하세요.
복잡하지 않고 단순한 스키마 디자인을 유지하겠습니다. 먼저 testdb라는 이름의 데이터베이스를 생성하고 이 데이터베이스 내에 todos 테이블을 생성합니다(데이터베이스와 테이블에 원하는 이름을 자유롭게 지정하되 SQL 문에서 동일한 이름을 사용해야 합니다)
todos 테이블 내부에 아래 스키마를 구현합니다.
애플리케이션을 처음 로드할 때 일부 작업을 볼 수 있도록 데이터베이스 테이블에 일부 작업을 시드하도록 선택할 수 있습니다.
작은 애플리케이션 설정을 시작하려면 개발 컴퓨터의 편리한 위치에 프로젝트용 폴더를 만드세요.
mkdir task-management
프로젝트 폴더의 루트에서 다음 명령을 실행하여 Golang 프로젝트로 초기화하세요.
go mod init task-management
다음으로 몇 가지 종속 항목을 설치해야 합니다. 우리는 이미 MySQL을 데이터베이스로 사용하고 있다는 것을 알고 있으므로 Golang용 MySQL 드라이버를 설치해야 합니다.
또한 프로젝트의 라우팅 라이브러리가 될 Gorilla Mux Router를 설치해야 합니다. 프로젝트 루트에서 아래 두 명령을 실행하여 해당 라이브러리를 프로젝트에 설치하세요
MySQL:
go get -u github.com/go-sql-driver/mysql
고릴라 먹스:
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 // Initialize the db variable db, err = sql.Open("mysql", "root:root@(127.0.0.1:3333)/testdb?parseTime=true") if err != nil { log.Fatal(err) } // Check the database connection if err = db.Ping(); err != nil { log.Fatal(err) } } func main() { gRouter := mux.NewRouter() //Setup MySQL initDB() defer db.Close() gRouter.HandleFunc("/", Homepage) //Get Tasks gRouter.HandleFunc("/tasks", fetchTasks).Methods("GET") //Fetch Add Task Form gRouter.HandleFunc("/newtaskform", getTaskForm) //Add Task gRouter.HandleFunc("/tasks", addTask).Methods("POST") //Fetch Update Form gRouter.HandleFunc("/gettaskupdateform/{id}", getTaskUpdateForm).Methods("GET") //Update Task gRouter.HandleFunc("/tasks/{id}", updateTask).Methods("PUT", "POST") //Delete Task 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) //fmt.Println(todos) //If you used "define" to define the template, use the name you gave it here, not the filename 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) if err != nil { log.Fatal(err) } defer stmt.Close() _, executeErr := stmt.Exec(task, 0) if executeErr != nil { log.Fatal(executeErr) } // Return a new list of Todos todos, _ := getTasks(db) //You can also just send back the single task and append it //I like returning the whole list just to get everything fresh, but this might not be the best strategy tmpl.ExecuteTemplate(w, "todoList", todos) } func getTaskUpdateForm(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) //Convert string id from URL to integer 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, _ := strconv.ParseBool(r.FormValue("done")) var taskStatus bool fmt.Println(r.FormValue("done")) //Check the string value of the checkbox switch strings.ToLower(r.FormValue("done")) { case "yes", "on": taskStatus = true case "no", "off": taskStatus = false default: taskStatus = false } taskId, _ := strconv.Atoi(vars["id"]) task := Task{ taskId, taskItem, taskStatus, } updateErr := updateTaskById(db, task) if updateErr != nil { log.Fatal(updateErr) } //Refresh all Tasks 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) } //Return list 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) if err != nil { return nil, err } 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("No task was found with task %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) if err != nil { return err } rowsAffected, err := result.RowsAffected() if err != nil { return err } if rowsAffected == 0 { fmt.Println("No rows updated") } else { fmt.Printf("%d row(s) updated\n", rowsAffected) } return nil } func deleTaskWithID(dbPointer *sql.DB, id int) error { query := "DELETE FROM tasks WHERE id = ?" stmt, err := dbPointer.Prepare(query) if err != nil { return err } defer stmt.Close() result, err := stmt.Exec(id) if err != nil { return err } rowsAffected, err := result.RowsAffected() if err != nil { return err } if rowsAffected == 0 { return fmt.Errorf("no task found with id %d", id) } fmt.Printf("Deleted %d task(s)\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 변수를 만듭니다. 그런 다음 작업 유형을 정의하는 사용자 정의 작업 구조체를 생성합니다.
init() 함수 내에서 템플릿 폴더의 모든 템플릿을 로드합니다. HTMX가 HTML을 반환할 것으로 예상하므로 모든 템플릿에는 .html 확장자가 있어야 합니다. 이는 매우 의미 있는 일입니다.
모든 템플릿 로드를 시작할 수 있도록 프로젝트 루트에 템플릿 폴더를 생성하세요.
또한 데이터베이스에 대한 연결 설정을 관리하고 데이터베이스에 대한 포인터 참조를 반환하는 initDB() 함수도 있습니다. 데이터베이스의 문자열(자격 증명, 호스트, 포트, 데이터베이스 이름 등)과 일치하도록 연결 문자열을 변경해야 합니다
메인 함수 내에서 라우터를 초기화하고 initDB() 데이터베이스 함수를 호출하여 데이터베이스를 초기화합니다. 그런 다음 모든 경로와 경로 핸들러가 이어지며 마지막으로 애플리케이션을 제공할 포트 4000에서 수신 대기합니다.
이제 경로와 해당 핸들러를 분석해 보겠습니다.
And that’s all the routes and handlers used in this application.
You may have noticed some other functions asides the route handlers also defined in our main.go file. These are functions for performing database operations for fetching tasks (getTasks), getting a single task using its Id (getTaskByID), updating a task using its Id (updateTaskById), and deleting a task using the tasks’ Id (deleTaskWithID).
These helper functions are used within our route handlers to facilitate database operations and keep the handlers lean.
Now that we are familiar with our Hypermedia API, let’s begin creating the HTML templates that will be retuned in the response to our API calls.
First, we create home.html file in the templates folder. This will load the home page of our task management application. Add the following code to the file after creating it.
<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/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <script src="https://unpkg.com/htmx.org@1.9.12"></script> <title>To Do App</title> <div class="row"> <div class="col"> <h2>Tasks</h2> <div> <a href="#" hx-get="/newtaskform" hx-target="#addTaskForm">Add New Item</a> </div> <div id="taskList" hx-get="/tasks" hx-trigger="load" hx-swap="innerHTML"> </div> </div> <!-- <div class="col"> </div> --> <div class="col"> <h2>Add New Task</h2> <div id="addTaskForm"> {{template "addTaskForm"}} </div> </div> </div>
This templates forms the shell and layout of the entire application. We have the boilerplate HTML structure and I have also added the Bootstrap CSS library for some basic styling. The HTMX library has also been included through a CDN link.
The application layout contains two sections. One section for displaying tasks and the other for showing the new task and task update forms.
The first section contains a button for requesting a new task form from the hypermedia API. Once the form is returned, we then use hx-target to load the form into the div with an id of addTaskForm in the forms section of the page.
<a href="#" hx-get="/newtaskform" hx-target="#addTaskForm">Add New Item</a>
The next component in the first section is the div where our tasks will be loaded into. This div uses hx-trigger to initiate a GET request to the /tasks route once the page loads, thus immediately loading the tasks into the page.
<div id="taskList" hx-get="/tasks" hx-trigger="load" hx-swap="innerHTML"> </div>
In the second section, as mentioned earlier, we have a div with an id of addTaskForm for loading both our new task and update forms. We have also preloaded the form for adding a new task into this div using Go template import syntax so as to have a default form in place.
Now let’s create the form for adding a new task next. Inside the templates folder, create the file addTaskForm.html and add the following code inside it:
{{define "addTaskForm"}}
This templates loads a fresh form in the UI for adding a new task. When the submit button is clicked, it uses HTMX to send a POST request to the /tasks route to add a new task. When the operation is done, it uses HTMX once again to load the response, an updated list of tasks, into the div with an id of taskList.
Next is our update form template. Inside the templates folder, create the file updateTaskForm.html and add the following code:
{{define "updateTaskForm"}}
This template takes in a task to be updated and uses it to pre-populate the update form so that the user can see the previous state of the task to be updated.
When the Update Task button is clicked, it will send the updated values to the hypermedia API for the task to be updated. Once updated, it loads the updated list into the page.
Finally, we create the template the returns our list of task items. Inside the templates folder, create the file todoList.html and add the following code:
{{define "todoList"}}
Yeah, a lot is going on in this template, so let’s break it down.
First, the template takes in a Go slice of Task types and loops over it using the range function to create an HTML list of unordered items.
The task it displayed in each list item and the Done property is used to check if the task is completed. If so, we use CSS to strike the task as being completed.
Just after the task text, we have an Edit button. This button calls the /gettaskupdateform endpoint to load an update form using the id of the specific task that was clicked. The user can then update the task and get an updated list of task items.
After the Edit button, we have a Delete button that uses hx-delete to call the DELETE /tasks/{id} endpoint so that we can delete the task. But before we can send the delete request, we use hx-confirm to display a confirmation dialog to the user so that they can confirm if they really want to delete this task item. Once deleted, a new updated list is returned and the task will be gone.
And with that we wrap up our application, so let’s move on to the fun part, checking it out.
With all the code in place, now let’s test our application.
Ensure that all files are saved and run the following command at the root of your project:
go run main.go
Now go to your browser and load the application page at http://localhost:4000. If you have used a different port, ensure that you’re using that port to load the app.
Now you should see your application as displayed below. See below as we add a new task, update an existing task and delete a task from our task list
If you have enjoyed this article, and will like to learn more about building projects with HTMX, I’ll like you to check out HTMX + Go: Build Fullstack Applications with Golang and HTMX, and The Complete HTMX Course: Zero to Pro with HTMX to further expand your knowledge on building hypermedia-driven applications with HTMX.
Happy Coding :)
위 내용은 HTMX + Go: Golang 및 HTMX를 사용하여 CRUD 앱 빌드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!