實作 - 從零開始用 Golang 寫網頁 - 11 透過 AJAX 請求資料

[實作] 從零開始用 Golang 寫網頁 : 11 透過 AJAX 請求資料

上一章我們使用了 Form 表單的傳統方式來向後端請求數據,而此方式會刷新整個頁面導致消耗不必要的寬頻

這裡我們要透過 AJAX 來請求資料,這樣並不會刷新整個頁面,而是只刷新需要變更的地方,此技術比較注重於前端 JavaScript 的開發

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{{ block "content" . }}

<h1 class='text-center'>{{ .Title }}</h1>
<form action='/ajaxtodo/' method='POST'>
<div class='row'>
<div class='offset-lg-1 col-lg-8 offset-md-1 col-md-7' style='margin-bottom: 5pt;'>

<input type='text' class='form-control' name='todo' placeholder='Something to do.'>
</div>

<div class='col-lg-3 col-md-4'>
<button class='btn btn-primary'>Add</button>
</div>
</div>
</form>

<div id='todos'></div>

<div class='row'>
<div class='offset-lg-2 col-lg-8 offset-md-1 col-md-10 col-sm-12'>
<div id='message'></div>
</div>
</div>

{{ end }}

{{ block "script" . }}
<script src='/js/appAJAX.js'></script>
{{ end }}

HTML

  • HTML 這邊將原本處理後端輸出資料的程式碼縮減到剩 <div> 元素,錯誤訊息的部分也是

    • 因為我們不在由後端輸出項目,改由 AJAX 呼叫取得,再由前端 JavaScript 動態生成
  • 在外層的 layout 會載入 SuperAgent,後面會使用到與介紹

1
<script src="https://cdnjs.cloudflare.com/ajax/libs/superagent/4.1.0/superagent.min.js" integrity="sha256-XwGIb0dW2d+hM8XPl9RcTbaNJoTfQ/xKV1n5EBYH0n4=" crossorigin="anonymous"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
let baseURL = 'http://localhost:8080';

document.addEventListener('DOMContentLoaded', function () {
/* Event listeners related to TODO creation. */
(function () {

let form = document.querySelector('form');

let input = form.querySelector('input');

/* The `Add` button listens to click event. */
let btn = form.querySelector('button');

btn.addEventListener('click', function (ev) {
ev.preventDefault();

createTODO();
}, false);

function createTODO() {
let item = input.value;

superagent
.post(`${baseURL}/ajaxtodo/`)
.send({
item: item,
index: 0
})
.set('accept', 'json')
.then(function (res) {
clearMessage();

console.log(res.body);

addTODO(res.body);
input.value = '';
})
.catch(function (err) {
if (err.response) {
showMessage(err.response.message);
}
});
}
})();

/* Load initial TODO items */
superagent
.get(`${baseURL}/gettodo/`)
.set('accept', 'json')
.then(function (res) {
clearMessage();

let ts = res.body.todos;

console.log(ts);

for (let i = 0; i < ts.length; i++) {
addTODO(ts[i]);
}
})
.catch(function (err) {
if (err.reponse) {
showMessage(err.reponse.message);
}
});
});

function addTODO(todo) {
let item = todo.item;
let index = todo.index;

/* Create a label element for item text. */
let label = document.createElement('label');

label.classList.add('col-form-label');
label.innerText = item;

/* Create a hidden input element for item index. */
let input = document.createElement('input');

input.name = 'index';
input.setAttribute('value', index);
input.setAttribute('hidden', true);

/* A holder for the label and input elements. */
let row = document.createElement('div');

row.classList.add('offset-lg-1');
row.classList.add('col-lg-8');
row.classList.add('offset-md-1');
row.classList.add('col-md-7');
row.classList.add('todo');

row.style.marginTop = '5pt';
row.style.marginBottom = '5pt';

row.appendChild(label);
row.appendChild(input);

/* Create a new HTML form. */
let form = document.createElement('form');

let div = document.createElement('div');

div.classList.add('row');

div.appendChild(row);

form.appendChild(div);

/* Append the created form to the index page. */
let todoList = document.getElementById('todos');
todoList.appendChild(form);
}

function showMessage(msg) {
let div = document.createElement('div');

div.classList.add('alert');
div.classList.add('alert-warning');
div.setAttribute('role', 'alert');

div.innerText = msg;

let message = document.getElementById('message');

message.innerHTML = '';
message.appendChild(div);
}

function clearMessage() {
let message = document.getElementById('message');

message.innerHTML = '';
}

載入頁面事件處理

  • 新增一個按下 Add 的事件處理,並新增 createTODO 函式

  • 在載入頁面時,會使用 AJAX 來向後端請求並載入所有的待辦事項項目

    • 這邊使用 SuperAgent 來撰寫 AJAX,相較於原生的 XMLHttpRequest 較為簡潔

      • SuperAgent 是一個較輕量、具可讀性、易學習的 AJAX 請求函式庫,也可在Node.js 中使用

      • 這邊主要用 GET 的方式向後端 http://localhost:8080/gettodo/ 呼叫,接收 JSON 格式資料

      • .then() 為請求成功的處理

        • 呼叫 clearMessage() 將頁面的錯誤訊息清空

        • 並呼叫 addTODO() 將項目一個一個帶入

      • .catch() 則為請求失敗的處理

        • 呼叫 showMessage() 將錯誤訊息顯示出來

按下 Add 事件處理

  • 使用 preventDefault() 函式來避免觸發原本的 HTML 表單請求

  • 呼叫 createTODO() 來新增待辦事項

createTODO

  • 用於將使用者新增的待辦事項傳送到後端,並將接收到的回應用 addTODO() 顯示於頁面

    • 使用 SuperAgent 用 POST 的方式向後端 http://localhost:8080/ajaxtodo/ 呼叫,傳送並接收 JSON 格式資料

      • .then() 呼叫 addTODO() 將新增的項目帶入

      • .catch() 將錯誤訊息顯示出來

addTODO

  • 用於在 HTML 頁面生成區塊來放置傳來的待辦事項

showMessage

  • 用於生成區塊來顯示傳來的錯誤訊息

clearMessage

  • 用於將顯示於頁面的錯誤訊息清空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package main

import (
"fmt"
"net/http"
"os"

"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"github.com/urfave/negroni"

negronilogrus "github.com/meatballhat/negroni-logrus"

"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)

// AJAXTODO represents single TODO item.
type AJAXTODO struct {
Item string `json:"item"`
Index uint `json:"index"`
}

// AJAXTODOs represents TODO items.
type AJAXTODOs struct {
Todos []AJAXTODO `json:"todos"`
}

// ResponseMessage represents message json for response
type ResponseMessage struct {
Message string `json:"message"`
}

// TODOModel represents the model of a TODO list.
type TODOModel struct {
ID uint `gorm:"PRIMARY_KEY,AUTO_INCREMENT"`
Todo string
}

// TableName set the name of the table.
func (TODOModel) TableName() string {
return "todos"
}

var db *gorm.DB

func init() {
var err error

db, err = gorm.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}

if !db.HasTable(&TODOModel{}) {
db.CreateTable(&TODOModel{})
}
}

func main() {

defer db.Close()

host := "localhost"
port := "8080"
output := "D:\\Desktop\\GO\\log\\log.txt"

args := os.Args[1:]

for {
if len(args) < 2 {
break
} else if args[0] == "-h" || args[0] == "--host" {
host = args[1]
args = args[2:]
} else if args[0] == "-p" || args[0] == "--port" {
port = args[1]
args = args[2:]
} else if args[0] == "-l" || args[0] == "--log" {
output = args[1]
args = args[2:]
} else {
log.Fatal(fmt.Sprintf("Unknown parameter : %s", args[0]))
}
}

mux := httprouter.New()

mux.ServeFiles("/js/*filepath", http.Dir("static/js"))
mux.ServeFiles("/css/*filepath", http.Dir("static/css"))

mux.GET("/myexercise/ajaxtodolist", ajaxtodolistindexHandler)
mux.GET("/gettodo/", ajaxgetTODOHandler)
mux.POST("/ajaxtodo/", ajaxaddTODOHandler)

mux.NotFound = http.HandlerFunc(notFound)

mux.PanicHandler = errHandler

l := log.New()

var f *os.File
var err error

if output != "" {
f, err = os.Create(output)
if err != nil {
log.Fatal(err)
}

defer f.Close()
l.SetOutput(f)
}

n := negroni.Classic()
n.Use(negronilogrus.NewMiddlewareFromLogger(l, "web"))
n.UseHandler(mux)

server := http.Server{
Addr: fmt.Sprintf("%s:%s", host, port),
Handler: n,
}

fmt.Printf("服務器即將開啟, 訪問地址 http://%s:%s\n", host, port)
l.Println(fmt.Sprintf("服務器即將開啟, 訪問地址 http://%s:%s", host, port))
l.Fatal(server.ListenAndServe())
}

宣告一些用於 JSON 資料格式的結構

main

  • 將對應的路徑分配給對應的函式

    • ajaxtodolistindexHandler 處理頁面的顯示
    • ajaxgetTODOHandler 處理傳送給請求端目前待辦事項所有的資料
    • ajaxtodo 處理傳來的 JSON 資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
func ajaxtodolistindexHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
t := template.Must(template.ParseFiles("./views/todolistAJAX/layout.html", "./views/head.html", "./views/todolistAJAX/todolisteasy.html"))

err := t.ExecuteTemplate(w, "layout", struct {
Title string
}{
Title: "AJAX待辦事項",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func ajaxgetTODOHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
rows, err := db.Table("todos").Select("*").Rows()
if err != nil {
ErrorMessage(w, http.StatusBadGateway, "Unable to retrieve database")
return
}

var todos []AJAXTODO

todos = make([]AJAXTODO, 0)

for rows.Next() {
var todo struct {
ID uint
Todo string `gorm:"todo"`
}

db.ScanRows(rows, &todo)

todos = append(todos, AJAXTODO{
Index: todo.ID,
Item: todo.Todo,
})
}

data := AJAXTODOs{
todos,
}

json, _ := json.Marshal(data)

w.Header().Set("Content-Type", "application/json")
w.Write(json)
}

func ajaxaddTODOHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
decoder := json.NewDecoder(r.Body)

var t AJAXTODO
err := decoder.Decode(&t)
if err != nil {
ErrorMessage(w, http.StatusUnprocessableEntity, "Failed to parse input")
return
}

db.Table("todos").Create(struct {
Todo string `gorm:"todo"`
}{
Todo: t.Item,
})

var rec struct {
ID uint
Todo string `gorm:"todo"`
}

db.Table("todos").Last(&rec)

data := AJAXTODO{
Index: rec.ID,
Item: rec.Todo,
}

json, _ := json.Marshal(data)

w.Header().Set("Content-Type", "application/json")
w.Write(json)
}

ajaxtodolistindexHandler

  • 用於載入 HTML 顯示頁面

ajaxgetTODOHandler

  • 首先查詢資料庫取得目前所有的待辦事項

  • 接著使用 json.Marshal() 將資料轉換為 JSON 格式

  • 最後將 Header 的 Content-Type 設為 application/json

ajaxtodo

  • 使用 json.NewDecoder().Decode() 將傳來的資料解析

  • 接著用 .Create() 將新的資料寫入資料庫

  • 最後查詢資料庫中最後一筆資料,並將其用 JSON 格式傳回

結果

  • 此時新增項目後就不會再刷新整個頁面了

tags: 實作 Golang 網站 AJAX SuperAgent
Author: Kenny Li
Link: https://kennyliblog.nctu.me/2020/09/25/Golang-Web11/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.