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>&lt;script&gt;alert(&#34;hello&#34;)&lt;/script&gt;</strong> wrote:</p>
<p class="text">&lt;script&gt;alert(&#34;hello&#34;)&lt;/script&gt;</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 field Name dan Text.
  • NewMessage(name, text), yang memangkas, memeriksa, dan mengembalikan Message dan error. Pesan error-nya ditulis untuk pengunjung, karena main.go menampilkannya di halaman.
  • Book, struct dengan sync.Mutex, nama file, dan slice berisi pesan.
  • Open(filename), yang mengembalikan *Book dan 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 menulis b.mu.Lock(). Metodenya butuh receiver pointer. Dengan receiver nilai, setiap pemanggilan akan menyalin kuncinya, dan go vet melaporkan Messages passes lock by value.
  • Message punya field yang diekspor, yang sedikit melanggar saran di Bab 30. encoding/json dan html/template hanya bisa melihat field yang diekspor, jadi tidak ada pilihan lain. Book menjaga field-nya tetap privat, jadi slice hanya bisa dicapai lewat metodenya, dan di sanalah aturannya berada.
  • NewMessage tidak menyentuh buku, jadi tidak butuh kunci. main.go memanggilnya 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 mengatur white-space: pre-wrap pada kelas text, 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.Reverse pada salinan di Messages.
  • Batas. Tolak pesan baru begitu buku memuat 1000, karena seluruh file ditulis ulang setiap kali menyimpan.
  • Hitungan. Tampilkan 3 messages di 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.