Mini-proyek Bagian 7: Buku Tamu
Bab 01 menjanjikan tiga program: game menebak angka, petualangan teks, dan halaman web buku tamu kecil. Kamu sudah membuat dua yang pertama. Inilah yang ketiga: pengunjung meninggalkan pesan, programmu menyimpannya, dan menampilkannya kepada pengunjung berikutnya. Ini proyek pertama yang kamu buka di browser.
Apa yang harus dibuat
Server di http://localhost:8080 dengan dua rute:
| Rute | Yang dilakukan |
|---|---|
GET / |
Menampilkan semua pesan, yang terlama lebih dulu, dan formulir. |
POST /messages |
Memeriksa formulir, menyimpan pesan, dan melakukan redirect ke
/. |
Aturannya:
- Pesan punya nama dan teks. Spasi di sekitarnya dipangkas.
- Nama paling banyak 40 karakter, dan teks paling banyak 500. Keduanya tidak boleh kosong.
- Formulir yang buruk mendapat status 400. Halaman menampilkan formulir lagi, dengan pesan yang mengatakan apa yang salah, dan dengan isian pengunjung tetap ada di kotaknya.
- Formulir yang baik mendapat status 303, lalu browser meminta
/. - Pesan disimpan ke
messages.json, dan masih ada setelah server dimulai ulang. <script>di dalam pesan tampil sebagai teks.
Server yang sudah selesai, dari jalan yang sebenarnya. Baris-baris ini dari halaman sesudah tiga pesan, dan yang terakhir adalah percobaan menyerangnya:
<p><strong>Keiko</strong> wrote:</p>
<p class="text">Hello there</p>
<p><strong>Arif</strong> wrote:</p>
<p class="text">Nice page</p>
<p><strong><script>alert("hello")</script></strong> wrote:</p>
<p class="text"><script>alert("hello")</script></p>
Response untuk formulir yang baik dan beberapa yang buruk:
curl.exe -i -d "name=Keiko&text=Hello there" http://localhost:8080/messages
HTTP/1.1 303 See Other
Location: /
curl.exe -i -d "name=&text=x" http://localhost:8080/messages
HTTP/1.1 400 Bad Request
<p class="error">Please enter your name.</p>
curl.exe -i -d "name=Keiko&text=" http://localhost:8080/messages
HTTP/1.1 400 Bad Request
<p class="error">Please enter a message.</p>
curl.exe -i http://localhost:8080/messages
HTTP/1.1 405 Method Not Allowed
Allow: POST
Method Not Allowed
Teks sepanjang 501 karakter dan nama sepanjang 41 karakter memberikan baris-baris ini:
<p class="error">The message can have at most 500 characters.</p>
<p class="error">The name can have at most 40 characters.</p>
messages.json yang rusak menghentikan server di
awal:
Could not load the messages: messages.json is not valid: invalid character 'o' looking for beginning of object key string
Fix or remove the file, then start the server again.
Susunan file
Proyek ini berupa modul dengan satu package buatanmu sendiri, seperti
di Bagian 6. Kedua versi ada di projects/06-guestbook/:
starter/
go.mod
main.go
guestbook/
guestbook.go
templates/
home.html
finished/
go.mod
main.go
guestbook/
guestbook.go
templates/
home.html
Untuk mengerjakan starter, masuk ke foldernya dan pakai
go run .:
cd projects/06-guestbook/starter
go run .
Untuk menjalankan versi yang sudah selesai:
cd projects/06-guestbook/finished
go run .
Untuk kembali ke go-work sesudahnya:
cd ../../..
Kedua path di program ini relatif terhadap folder tempat dia
berjalan: dia membaca templates/home.html, serta membaca
dan menulis messages.json. File data muncul di sebelah
main.go saat pesan pertama disimpan. Hapus untuk memulai
dengan buku kosong. Menjalankan dari folder lain memberikan ini:
Could not read the template: open templates/home.html: no such file or directory
Run this program from its own folder, and check the template.
Menyebut nama file dari go-work gagal lebih awal, karena
alasan di Bab 25. Program berhenti dengan error yang memuat
package guestbookapp/guestbook is not in std.
Starter bisa langsung dijalankan. main.go sudah selesai,
jadi baca dulu. Halamannya menampilkan formulir dan baris
TODO di tempat pesan-pesan seharusnya berada. Mengirim
formulir mendapat status 400, dengan baris
TODO: NewMessage is not written yet. di halaman. TODO-nya
ada di guestbook/guestbook.go dan
templates/home.html. Jalankan go vet ./... di
folder yang sama untuk memeriksa kedua package.
Bahan untuk membuatnya
main.go mengetahui soal web dan tidak tahu apa-apa
tentang aturan. Package guestbook mengetahui aturan dan
file, dan tidak tahu apa-apa tentang web. Package itu bisa dipakai oleh
program terminal tanpa perubahan. Ini finished/main.go,
yang juga dimiliki starter:
// Guestbook -- main (finished).
//
// Run from this folder, because the template and the data file are relative to
// it:
// cd projects/06-guestbook/finished
// go run .
package main
import (
"bytes"
"fmt"
"html/template"
"net/http"
"guestbookapp/guestbook"
)
const templateFile = "templates/home.html"
const dataFile = "messages.json"
// homeData is everything the template needs.
type homeData struct {
Error string
Name string
Text string
Messages []guestbook.Message
}
var homeTemplate *template.Template
var book *guestbook.Book
// showHome builds the page in memory, and sends it only if that worked.
func showHome(w http.ResponseWriter, status int, data homeData) {
data.Messages = book.Messages()
var page bytes.Buffer
err := homeTemplate.Execute(&page, data)
if err != nil {
fmt.Println("Template error:", err)
http.Error(w, "The page could not be built.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
page.WriteTo(w)
}
func handleHome(w http.ResponseWriter, r *http.Request) {
showHome(w, http.StatusOK, homeData{})
}
func handleAdd(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
text := r.FormValue("text")
message, err := guestbook.NewMessage(name, text)
if err != nil {
// Show the form again, with what the visitor typed.
data := homeData{Error: err.Error(), Name: name, Text: text}
showHome(w, http.StatusBadRequest, data)
return
}
err = book.Add(message)
if err != nil {
fmt.Println("Save error:", err)
http.Error(w, "The message could not be saved.", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func main() {
parsed, err := template.ParseFiles(templateFile)
if err != nil {
fmt.Println("Could not read the template:", err)
fmt.Println("Run this program from its own folder, and check the template.")
return
}
homeTemplate = parsed
opened, err := guestbook.Open(dataFile)
if err != nil {
fmt.Println("Could not load the messages:", err)
fmt.Println("Fix or remove the file, then start the server again.")
return
}
book = opened
http.HandleFunc("GET /{$}", handleHome)
http.HandleFunc("POST /messages", handleAdd)
fmt.Println("Guestbook running at http://localhost:8080")
fmt.Println("Press Ctrl+C to stop it.")
err = http.ListenAndServe("localhost:8080", nil)
if err != nil {
fmt.Println("Server stopped:", err)
}
}Package guestbook membutuhkan:
Message, struct dengan fieldNamedanText.NewMessage(name, text), yang memangkas, memeriksa, dan mengembalikanMessagedan error. Pesan error-nya ditulis untuk pengunjung, karenamain.gomenampilkannya di halaman.Book, struct dengansync.Mutex, nama file, dan slice berisi pesan.Open(filename), yang mengembalikan*Bookdan error, dengan file yang tidak ada memberikan buku kosong.(b *Book) Messages(), yang mengembalikan salinan pesan-pesan.(b *Book) Add(message), yang menyimpan lebih dulu dan mempertahankan slice baru hanya kalau penyimpanan berhasil.
Petunjuk
- Mutex sekarang menjadi field dari
Book, di sebelah data yang dilindunginya, dan kamu menulisb.mu.Lock(). Metodenya butuh receiver pointer. Dengan receiver nilai, setiap pemanggilan akan menyalin kuncinya, dango vetmelaporkanMessages passes lock by value. Messagepunya field yang diekspor, yang sedikit melanggar saran di Bab 30.encoding/jsondanhtml/templatehanya bisa melihat field yang diekspor, jadi tidak ada pilihan lain.Bookmenjaga field-nya tetap privat, jadi slice hanya bisa dicapai lewat metodenya, dan di sanalah aturannya berada.NewMessagetidak menyentuh buku, jadi tidak butuh kunci.main.gomemanggilnya lebih dulu, dan begitulah dia bisa membedakan kesalahan pengunjung (400) dari masalah disk (500).- Hitung karakter dengan
utf8.RuneCountInString, supaya nama dalam alfabet lain tidak diukur dalam byte. - Di template,
{{range .Messages}}punya{{else}}untuk kasus kosong. Pesan di dalam<p>akan kehilangan akhir barisnya, jadi blok<style>kecil di template mengaturwhite-space: pre-wrappada kelastext, yang mempertahankannya. Semuanya ada di halaman itu sendiri, tanpa stylesheet dari luar. - Nilai formulir kembali ke halaman saat ada error. Itu aman, karena template juga meng-escape-nya di sana.
Tantangan lebih besar (opsional)
- Terbaru lebih dulu. Pakai
slices.Reversepada salinan diMessages. - Batas. Tolak pesan baru begitu buku memuat 1000, karena seluruh file ditulis ulang setiap kali menyimpan.
- Hitungan. Tampilkan
3 messagesdi atas, memakai{{len .Messages}}di template. - Uji package dengan tangan. Tulis program terminal
kecil yang mengimpor
guestbook, untuk menunjukkan bahwa package itu tidak butuh web.
Tidak ada yang wajib supaya proyek ini selesai.
Selesai?
Ketika sebuah pesan bertahan setelah server dimulai ulang, formulir
kosong mendapat error yang berguna, memuat ulang halaman sesudah
mengirim tidak mengirim pesan dua kali, dan <script>
tampil sebagai teks, buku tamumu selesai. Itu yang terakhir dari tiga
program yang dijanjikan Bab 01. Proyek terakhir adalah Papan Skor Web, yang menyatukan
seluruh buku.