1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เพิ่มปฏิสัมพันธ์แบบไดนามิกด้วย HTMX โดยยังคง การเรนเดอร์ฝั่งเซิร์ฟเวอร์ ที่อิงกับ html/template ของ Go ไว้ และให้เรนเดอเรอร์ตัวเดียวสามารถเลือกส่งคืนได้ทั้งหน้าเต็มและชิ้นส่วน HTML
  • แยกมาร์กอัปออกเป็น base.tmpl, เทมเพลตหน้า และชิ้นส่วนที่นำกลับมาใช้ซ้ำได้ พร้อมใช้ embed.FS ของ Go 1.16 และ ชุดเทมเพลตร่วม เพื่อทำให้โครงสร้างการดีพลอยและการเรนเดอร์ง่ายขึ้น
  • ส่งคืนผลการค้นหาเป็นทั้งหน้าเต็มหรือแถวของตารางตามค่า HX-Request พร้อมใช้ Vary: HX-Request และ historyRestoreAsHxRequest: false เพื่อป้องกันปัญหาแคชและข้อผิดพลาดจากการกู้คืนเมื่อกดย้อนกลับ
  • เมื่อย้ายคำขอ HTMX ไปยังหน้าอื่น ให้ใช้ HX-Redirect และการตอบกลับ 2xx แทน 3xx ปกติ และใช้ responseHandling แยกตาม 204·422·4xx·5xx เพื่อควบคุมขอบเขตการแทนที่และการแสดงข้อผิดพลาด
  • ใช้การตั้งค่าที่ปิดแคช local storage และการสืบทอดแอตทริบิวต์ พร้อมกำหนดระยะเวลาหมดอายุของคำขอเป็นจุดเริ่มต้น และเมื่อระบบมีขนาดใหญ่ขึ้นก็สามารถเพิ่ม เทมเพลตเลย์เอาต์ระดับกลาง เพื่อรองรับโครงสร้างหน้าจอแยกต่างหาก เช่น พื้นที่ผู้ดูแลระบบ

เหตุผลที่ผสานการเรนเดอร์ฝั่งเซิร์ฟเวอร์ของ Go กับ HTMX

  • HTMX ช่วยเพิ่มปฏิสัมพันธ์แบบไดนามิกด้วย JavaScript เพียงเล็กน้อย โดยยังคงความสม่ำเสมอและความปลอดภัยจาก การเรนเดอร์ HTML ฝั่งเซิร์ฟเวอร์ ของ Go html/template
  • แกนหลักที่พูดถึงในการใช้งานมี 3 ส่วน
    • โครงสร้างเทมเพลตที่รองรับทั้งการตอบกลับเป็นทั้งหน้าเต็มและ HTML บางส่วน
    • การจัดการ redirect และข้อผิดพลาดในสภาพแวดล้อมของ HTMX
    • การตั้งค่าที่เปลี่ยนค่าเริ่มต้นของ HTMX และเหตุผลที่ต้องทำเช่นนั้น
  • เริ่มจากฟังก์ชันพื้นฐานที่เปลี่ยนปุ่มเป็นรูปภาพ แล้วขยายไปเป็นหน้าค้นหาที่กรองรายการระหว่างพิมพ์ชื่อผู้ใช้หรืออีเมล
  • แพตเทิร์นเทมเพลตเดียวกันนี้ใช้ได้ไม่เฉพาะกับ HTMX แต่ยังใช้ได้กับเครื่องมือแบบ HTML-over-the-wire อย่าง Unpoly และ Hotwire
  • หากยังไม่คุ้นกับวิธีใช้งานพื้นฐานของ HTMX ควรอ่านเอกสารทางการก่อน

โครงสร้างไดเรกทอรีโปรเจกต์และไฟล์สแตติก

  • โปรเจกต์แบ่งออกเป็นส่วนต่าง ๆ ตามหน้าที่ดังนี้
    • assets/html/base.tmpl: โครง HTML หลักที่ทุกหน้าใช้ร่วมกัน
    • assets/html/pages/: เนื้อหาของแต่ละหน้า
    • assets/html/partials/: ชิ้นส่วน HTML ที่นำกลับมาใช้ซ้ำในหลายตำแหน่ง
    • assets/static/css, assets/static/img, assets/static/js: แอสเซ็ตแบบสแตติก
    • cmd/web/: โค้ดเริ่มเซิร์ฟเวอร์, แฮนด์เลอร์ และตัวเรนเดอร์ HTML
  • โครงหลักเริ่มต้นสร้างได้ด้วยคำสั่งต่อไปนี้
go mod init example.com/htmx
mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages cmd/web
touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
  • จะติดตั้ง HTMX ผ่าน CDN หรือ NPM ก็ได้ แต่แนวทางที่ใช้บ่อยคือดาวน์โหลดสำเนามาแล้ว ให้บริการเป็นไฟล์สแตติกของแอปพลิเคชันโดยตรง
    • ตั้งค่าง่ายกว่าและหลีกเลี่ยงข้อเสียของการใช้ CDN
    • ใช้ htmx.org@2.0.10 และเฟรมเวิร์ก CSS แบบไม่มีคลาส bamboo.css@1.4.0
wget -P assets/static/js https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js
wget -P assets/static/css https://cdn.jsdelivr.net/npm/bamboo.css@1.4.0/dist/bamboo.min.css

การแยกเทมเพลตหลัก เทมเพลตหน้า และเทมเพลตบางส่วน

  • base.tmpl กำหนดโครงเอกสารทั้งหมด เช่น <head>, ชื่อเรื่องร่วม และ <main> แล้วแทรก page:title กับ page:content
  • เรียกใช้ Bamboo CSS และสคริปต์ HTMX จากเทมเพลตหลัก และกำหนด แอตทริบิวต์ defer ให้สคริปต์ HTMX
    • เบราว์เซอร์จะดึงสคริปต์แบบขนานระหว่างพาร์ส HTML
    • สคริปต์จะทำงานหลังจากพาร์ส HTML และสร้าง DOM เสร็จแล้ว
  • เทมเพลตทั้งหมดกำหนดชื่ออย่างชัดเจนด้วย {{define}}...{{end}} โดยไม่พึ่งชื่อไฟล์
    • โค้ด Go สามารถอ้างอิงเทมเพลตได้อย่างสม่ำเสมอด้วยชื่อที่กำหนดไว้เท่านั้น
    • : ใน page:title เป็นเพียงตัวคั่นตามสะดวก และจะใช้ชื่ออย่าง page_title, page-title, pageTitle, title ก็ได้
  • ปุ่มบนหน้าแรกกำหนดพฤติกรรมไว้ 2 อย่าง
    • hx-get="/gopher": ส่งคำขอ GET /gopher เมื่อคลิก
    • hx-swap="outerHTML": แทนที่ตัวปุ่มเองด้วย HTML ที่ตอบกลับมา
  • partial:image:gopher เป็นชิ้นส่วนรูปภาพที่นำกลับมาใช้ซ้ำได้ และรับความกว้างของรูปตอนเรียกใช้ผ่าน width="{{.}}"
  • ชิ้นส่วน HTML ที่ใช้เฉพาะในบางหน้า ไม่จำเป็นต้องอยู่ในไดเรกทอรี partials กลาง แต่สามารถวางไว้ในไฟล์ของหน้านั้นได้
    • กล่าวคือ ชิ้นส่วนที่ใช้หลายหน้าจะอยู่ในไดเรกทอรีกลาง ส่วนชิ้นส่วนเฉพาะหน้าจะวางไว้ข้างเนื้อหาที่เกี่ยวข้อง

ฝัง HTML และแอสเซ็ตสแตติกไว้ในไบนารีของ Go

  • หลังจาก Go 1.16 เพิ่มความสามารถในการฝังไฟล์ ก็สามารถ ฝัง HTML และแอสเซ็ตสแตติกไว้ในไบนารีของ Go แทนการอ่านจากดิสก์ตอนรันไทม์ได้
  • ใช้ directive //go:embed "html" "static" เพื่อใส่สองไดเรกทอรีลงใน embed.FS แล้วใช้ fs.Sub() แยกออกเป็นระบบไฟล์ย่อย 2 ชุด
    • HTMLFiles: ระบบไฟล์เทมเพลตที่ใช้ html เป็นราก
    • StaticFiles: ระบบไฟล์สแตติกที่ใช้ static เป็นราก
  • ข้อดีของการแยกเช่นนี้มี 2 อย่าง
    • โค้ดส่วน HTML และโค้ดส่วนไฟล์สแตติกจะไม่เข้าถึงไฟล์ของอีกฝั่งโดยไม่จำเป็น
    • เวลาเปิดไฟล์ไม่ต้องใส่คำนำหน้า html/ หรือ static/
  • หากไม่ต้องการใช้ panic() ใน sub() ก็สามารถคืนค่า error แล้วกำหนดค่าให้ตัวแปรทั้งสองใน main() ได้
  • ในการใช้งานปัจจุบัน หาก fs.Sub() จะล้มเหลวได้ ค่าของไดเรกทอรีต้องไม่ใช่พาธที่ถูกต้อง แต่สตริงคงที่ "html" และ "static" ผ่านการตรวจสอบนี้อยู่แล้ว จึงมี ความเสี่ยงต่ำมากที่จะเกิด runtime panic

ชุดเทมเพลตร่วมและ htmlRenderer

  • htmlRenderer จัดการลำดับการเรนเดอร์ดังนี้
    • พาร์ส ชุดเทมเพลตร่วม เพียงครั้งเดียวตอนเริ่มต้น
    • โคลนชุดร่วมทุกครั้งที่มีการเรนเดอร์
    • พาร์สเทมเพลตหน้าที่จำเป็นสำหรับคำขอนั้นเพิ่มเติม
    • รันเทมเพลตตามชื่อที่ระบุและส่งเป็นการตอบกลับ HTTP
  • newHTMLRenderer() ลงทะเบียน template.FuncMap ก่อน แล้วจึงพาร์สเทมเพลตร่วม
    • ผูก now เข้ากับ time.Now
    • สามารถเพิ่มฟังก์ชันเทมเพลตแบบกำหนดเองอื่น ๆ ได้ที่ตำแหน่งเดียวกัน
  • render() สร้างชุดเทมเพลตต่อคำขอด้วย sharedTemplates.Clone() และหากมี additionalTemplateFiles ก็ขยายต่อด้วย ParseFS()
  • เทมเพลตจะถูกรันลงใน bytes.Buffer ก่อน จากนั้นจึงเขียน status code และส่งออกเป็นเนื้อหาของ response
  • ตอนเริ่มต้นเซิร์ฟเวอร์ จะพาร์ส "base.tmpl" และ "partials/*.tmpl" เป็นชุดร่วม
    • ทำให้เทมเพลตหลักและชิ้นส่วนกลางทั้งหมดพร้อมใช้งานในเรนเดอเรอร์ตลอดเวลา
    • ส่วนเทมเพลตเฉพาะหน้าจะเพิ่มเข้ามาเมื่อเรียกแฮนด์เลอร์เท่านั้น
  • ให้บริการไฟล์สแตติกผ่าน http.FileServerFS(assets.StaticFiles) และพาธ /static/ โดยเซิร์ฟเวอร์รันอยู่ที่ :5051

การเรนเดอร์ทั้งหน้าเต็มและชิ้นส่วน HTML

  • แฮนด์เลอร์ของหน้าแรกส่งคืนเอกสารเต็มด้วยการเรียกดังนี้
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
  • นี่คือการเรียกที่เพิ่ม pages/home.tmpl เข้าไปในชุดร่วม แล้วรันเทมเพลต base ด้วย 200 OK
  • แฮนด์เลอร์ /gopher ไม่พาร์สไฟล์เพิ่มเติม แต่รันเฉพาะ partial:image:gopher ที่ถูกรวมอยู่ในชุดร่วมแล้ว
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
  • HTMX จะสลับปุ่มกับรูปภาพที่ได้รับจาก /gopher ตามการตั้งค่า hx-swap="outerHTML"
  • โครงสร้างนี้ให้ข้อดีดังต่อไปนี้
    • เทมเพลตและ static assets ถูกรวมอยู่ในไบนารี ทำให้ดีพลอยได้ง่าย
    • สามารถใช้ render() เดียวกันเพื่อส่งกลับได้ทั้งหน้า HTML แบบเต็มและชิ้นส่วน HTML เฉพาะส่วน
    • ลดความซ้ำซ้อนของมาร์กอัปด้วยเทมเพลตหลักและเทมเพลตย่อย
    • สามารถนำเทมเพลตย่อยกลับมาใช้ซ้ำได้ทั้งในเทมเพลตหลัก เนื้อหาหน้า และเทมเพลตย่อยอื่น ๆ

ค้นหาผู้ใช้แบบอัปเดตทันทีขณะพิมพ์

  • ฟังก์ชันค้นหาแบ่งออกเป็นสองเส้นทาง
    • GET /users: หน้า HTML แบบเต็มที่มีข้อมูลผู้ใช้ทั้งหมด
    • GET /users/search: ชิ้นส่วน HTML ที่ส่งกลับเฉพาะแถวตารางของผู้ใช้ที่ชื่อหรืออีเมลตรงกับคำค้น
  • ช่องค้นหาใช้ชุดแอตทริบิวต์ของ HTMX ดังนี้
    • ส่งคำขอค้นหาด้วย hx-get="/users/search"
    • ส่งคำขอเมื่อมีการเปลี่ยนแปลงอินพุตแล้วผ่านไป 500ms หรือเมื่อกด Enter ด้วย hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
    • คำค้นจะถูกส่งเป็น query string ในรูปแบบ GET /users/search?query=foo
    • ใส่ผลลัพธ์ลงใน <tbody id="search-results"> ด้วย hx-target="#search-results"
    • อัปเดตแถบที่อยู่และเพิ่มรายการในประวัติเบราว์เซอร์ทุกครั้งที่มีคำขอด้วย hx-push-url="true"
  • users:rows เป็นเทมเพลตแยกที่เรนเดอร์เฉพาะแถวของผู้ใช้
    • แต่ละแถวจะแสดงชื่อและอีเมล
    • หาก IsGopher เป็นจริง จะเรนเดอร์ partial:image:gopher ด้วยความกว้าง 24
  • ทั้ง listUsers และ searchUsers ต่างก็เพิ่ม pages/users.tmpl เข้าไปใน shared set แต่ใช้เทมเพลตที่รันต่างกัน
    • listUsers: รัน base เพื่อส่งกลับทั้งหน้า
    • searchUsers: รัน users:rows เพื่อส่งกลับเฉพาะแถวที่ตรงกัน
  • หากคำค้นว่างจะใช้รายชื่อผู้ใช้ทั้งหมด และถ้ามีค่าจะตรวจสอบชื่อกับอีเมลด้วย strings.Contains()
  • ในแอปพลิเคชันจริงอาจพิจารณาปรับปรุงเพิ่มเติมดังนี้
    • รวม list handler และ search handler เข้าด้วยกัน
    • รวมศูนย์ตัวช่วยสำหรับจัดการข้อผิดพลาดและ logging
    • เพิ่ม Content Security Policy header และ panic recovery ด้วย middleware
    • ตั้งค่า server timeout ให้เหมาะสม

แยกหน้าเต็มกับ partial response ด้วย HX-Request

  • ถ้าผู้ใช้เปิด /users/search?query=leo โดยตรงในเบราว์เซอร์หรือเข้าผ่านลิงก์ที่มีคนแชร์มา อาจเห็นเพียง partial HTML ของแถวตารางเท่านั้น
  • คำขอจาก HTMX จะมีเฮดเดอร์ HX-Request: true ติดมาด้วยเสมอ จึงใช้ค่านี้ในการตัดสินรูปแบบของ response
func isHTMXRequest(r *http.Request) bool {
    return r.Header.Get("HX-Request") == "true"
}
  • กำหนดให้เทมเพลตเริ่มต้นที่ search handler จะรันคือ base และเปลี่ยนเป็น users:rows เฉพาะเมื่อเป็นคำขอจาก HTMX
    • หากเข้าตรงจะได้รับทั้งหน้าที่มีผลการค้นหารวมอยู่ด้วย
    • คำขอจาก HTMX จะได้รับเฉพาะชิ้นส่วนแถวสำหรับใส่ใน <tbody>

เฮดเดอร์ Vary และการกู้คืนเมื่อกดย้อนกลับ

  • เนื่องจาก URL เดียวกันอาจส่งกลับ response คนละแบบตามค่า HX-Request จึงต้องแจ้งความแตกต่างนี้ให้แคชทราบด้วย
  • เพิ่ม Vary: HX-Request ให้กับทุก response ที่เรนเดอร์
    • หากตั้งค่าเฉพาะใน handler ที่จำเป็นจริง ๆ จะประหยัดกว่าเล็กน้อยในเชิงความเข้มงวด
    • แต่ถ้าตั้งครั้งเดียวใน renderer จะช่วยเลี่ยงบั๊กที่อาจเกิดจากการตกหล่น
  • เมื่อ hx-push-url หรือ hx-boost เพิ่มรายการในประวัติเบราว์เซอร์ HTMX จะเก็บแคช HTML ของหน้าที่สมบูรณ์ไว้ใน local storage ของเบราว์เซอร์
    • แคชเริ่มต้นเก็บได้สูงสุด 10 หน้า
    • หากย้อนกลับไปยังจุดที่ไม่มีในแคช HTMX จะร้องขอ URL นั้นจากเซิร์ฟเวอร์อีกครั้ง
  • โดยปกติคำขอกู้คืนจะมี HX-Request: true ติดมาด้วย ทำให้เซิร์ฟเวอร์อาจส่ง partial HTML แทนทั้งหน้า
  • หากใช้ HX-Request ในการเลือกชนิดของ response ให้ตั้งค่า historyRestoreAsHxRequest: false
    • จะตัด HX-Request: true ออกจากคำขอกู้คืนกรณี cache miss
    • ทำให้เซิร์ฟเวอร์ส่งกลับหน้า HTML แบบเต็มสำหรับคำขอนั้น
  • การตั้งค่า HTMX จะถูกใส่ไว้ในเมตาแท็ก htmx-config ของ base.tmpl

รีไดเร็กต์คำขอ HTMX ไปยังหน้าอื่น

  • HTMX สามารถสลับชิ้นส่วน HTML เช่นข้อความสำเร็จได้ทันที จึงลดความจำเป็นของ flow แบบ Post/Redirect/Get ทั่วไป
  • แต่เมื่อจำเป็นต้องพาไปยังอีกหน้าที่ต่างออกไปโดยสิ้นเชิง เช่นย้ายไปยังโปรไฟล์หลังล็อกอินสำเร็จ ก็ ไม่สามารถจัดการด้วย response 3xx ปกติอย่างเดียวได้
    • เบราว์เซอร์จะตาม response 3xx ไปก่อนที่ HTMX จะจัดการ
    • HTMX จะเห็นเพียง response สุดท้ายหลังรีไดเร็กต์ แล้วนำคอนเทนต์นั้นไปสลับแทนที่ target เดิม
  • สำหรับคำขอ HTMX ให้ใช้ response 2xx ร่วมกับเฮดเดอร์ HX-Redirect
    • HX-Redirect: /foo/bar จะพาเบราว์เซอร์ไปยัง URL นั้นและโหลดทั้งหน้าใหม่
    • คำขอไปยังปลายทางจะไม่มี HX-Request: true ติดไปด้วย
  • หากต้องการรองรับสภาพแวดล้อมที่ปิด JavaScript หรือไม่ได้โหลด HTMX ด้วย ก็ควรคง response 3xx ปกติไว้สำหรับคำขอที่ไม่ใช่ HTMX
func redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
    if isHTMXRequest(r) {
        w.Header().Set("HX-Redirect", url)
        w.WriteHeader(http.StatusNoContent)
        return
    }

    http.Redirect(w, r, url, code)
}
  • เมื่อต้องย้ายไป /profile หลังล็อกอิน สามารถเรียกใช้ได้ดังนี้
redirect(w, r, "/profile", http.StatusSeeOther)

ความต่างระหว่าง HX-Redirect และ HX-Location

  • HX-Location ทำงานคล้ายการรีไดเร็กต์โดยไม่โหลดทั้งหน้าใหม่
    • ดึง HTML ของ URL ปลายทาง
    • สลับ response เข้าไปแทนใน <body>
    • เพิ่มรายการใหม่ในประวัติเบราว์เซอร์
  • เมื่อร้องขอ URL ปลายทาง จะมี HX-Request: true ติดไปด้วย
  • หากเส้นทางปลายทางส่ง partial HTML ตามค่า HX-Request ก็อาจเกิดปัญหาได้
    • handler ไม่สามารถแยกคำขอที่ตามมาจาก HX-Location ออกจากคำขอ HTMX ปกติได้
    • แบบแรกต้องการทั้งหน้า แต่แบบหลังต้องการ partial response
  • ต่างจากการกู้คืนประวัติ HX-Location ไม่มีการตั้งค่าให้ปิด HX-Request: true ระหว่างการย้าย
  • โดยมากแล้วการยอมให้มีการโหลดหน้าใหม่ทั้งหมดและ ใช้ HX-Redirect จะง่ายและปลอดภัยกว่า
  • หากกำหนดให้เส้นทางปลายทางส่งกลับเฉพาะ HTML แบบเต็มเสมอ ก็สามารถใช้ HX-Location ได้เช่นกัน

การจัดการ response แบบ 204·422·4xx·5xx

  • โดยปกติ HTMX จะไม่สลับ response 4xx หรือ 5xx เข้าไปใน DOM
    • หน้าจอปัจจุบันจะคงเดิม
    • ข้อผิดพลาดจะถูกบันทึกไว้ในคอนโซลของ developer tools
  • แม้เซิร์ฟเวอร์จะส่ง 500 Internal Server Error เพราะชื่อเทมเพลตไม่มีอยู่จริง ภายใต้การตั้งค่าเริ่มต้นผู้ใช้ก็จะไม่เห็นข้อผิดพลาดบนหน้าจอ
  • เพื่อแสดงข้อความผิดพลาดให้ผู้ใช้เห็น จึงตั้งค่าให้ response 4xx และ 5xx สลับแทนที่ทั้ง <body>
  • ข้อยกเว้นคือ 422 Unprocessable Content
    • ใช้เมื่อส่งกลับฟอร์มที่มีข้อผิดพลาดในการตรวจสอบความถูกต้อง
    • response ควรถูกสลับเข้าไปใน target เดิมตามปกติ
  • responseHandling ถูกตั้งค่าตามสถานะดังนี้
    • 204 No Content: ไม่เปลี่ยน DOM
    • 422 Unprocessable Content: สลับเข้า target เดิมตามปกติ
    • 4xx และ 5xx อื่น ๆ: สลับ response เข้าไปแทนที่ <body>
    • response อื่นนอกเหนือจากนี้: สลับเข้า target เดิมตามปกติ
"responseHandling":[
    {"code":"204", "swap": false},
    {"code":"422", "swap": true},
    {"code":"[45]..", "swap": true, "target": "body"},
    {"code":"...", "swap": true}
]
  • หากต้องใส่ข้อผิดพลาดจากการโต้ตอบบางแบบไว้ในเป้าหมายอื่นที่ไม่ใช่ <body> สามารถใช้ ส่วนขยาย response target เพื่อเขียนทับค่าเริ่มต้นได้

ตรวจสอบ URL ปัจจุบันในแถบที่อยู่ของเบราว์เซอร์

  • HTMX จะไม่เปลี่ยน URL ของเบราว์เซอร์ระหว่างกระบวนการส่งคำขอ AJAX และแทนที่การตอบกลับ หากไม่ได้ใช้ hx-boost, hx-push-url, hx-replace-url
  • ด้วยเหตุนี้ r.URL ใน Go handler อาจไม่ตรงกับ URL ที่ผู้ใช้เห็นอยู่ในแถบที่อยู่
  • HTMX จะส่ง URL ที่เบราว์เซอร์กำลังแสดงอยู่ปัจจุบันมาในเฮดเดอร์ HX-Current-URL ของทุกคำขอ
  • ฟังก์ชันช่วยจะพาร์สเฮดเดอร์นี้เป็น url.URL และถ้าไม่มีเฮดเดอร์ก็จะคืนค่า r.URL
func browserURL(r *http.Request) (*url.URL, error) {
    cu := r.Header.Get("HX-Current-URL")
    if cu != "" {
        return url.Parse(cu)
    }

    return r.URL, nil
}

การตั้งค่าเพื่อเปลี่ยนค่าเริ่มต้นของ HTMX

  • ตั้ง historyCacheSize: 0 เพื่อปิด แคชหน้าบน local storage ของ HTMX ทั้งหมด
    • แคชบน local storage อาจเป็นต้นตอของบั๊กและปัญหาด้านความปลอดภัยได้
    • เมื่อกดย้อนกลับ ระบบจะดึง HTML จากเซิร์ฟเวอร์ใหม่แทนการใช้แคช
    • ใน HTMX เวอร์ชันอนาคต การแคชบน local storage ก็มีแผนจะถูกปิดไว้เป็นค่าเริ่มต้นด้วยเหตุผลเดียวกัน
  • ตั้ง disableInheritance: true เพื่อปิด การสืบทอดแอตทริบิวต์ของ HTMX
    • การประกาศแอตทริบิวต์อย่างชัดเจนทุกครั้งให้ความชัดเจนมากกว่า
    • ช่วยลดความเสี่ยงของบั๊กหรือพฤติกรรมที่ไม่ได้ตั้งใจ
    • ใน HTMX เวอร์ชันอนาคต การสืบทอดแอตทริบิวต์ก็มีแผนจะถูกปิดไว้เป็นค่าเริ่มต้นเช่นกัน
  • ตั้ง includeIndicatorStyles: false เพื่อไม่ให้ HTMX inject สไตล์ของ indicator
    • กำหนดสไตล์ของ indicator เองร่วมกับกฎ CSS อื่น ๆ เพื่อให้คงความสม่ำเสมอ
  • โดยปกติคำขอของ HTMX ไม่มีการกำหนดเวลาหมดอายุ จึงจะรอจนกว่าเซิร์ฟเวอร์จะตอบกลับ
    • สามารถกำหนด timeout เป็นมิลลิวินาทีได้ตามแนวทางของแอปพลิเคชัน Go ในการจัดการ timeout และ deadline
    • ใช้ timeout: 5000 เป็นจุดเริ่มต้น
  • การตั้งค่าเริ่มต้นยังมี historyRestoreAsHxRequest: false และ responseHandling ตามรหัสสถานะด้วย
<meta
    name="htmx-config"
    content='{
        "includeIndicatorStyles": false,
        "historyCacheSize": 0,
        "historyRestoreAsHxRequest": false,
        "responseHandling":[
            {"code":"204", "swap": false},
            {"code":"422", "swap": true},
            {"code":"[45]..", "swap": true, "target": "body"},
            {"code":"...", "swap": true}
        ],
        "timeout": 5000
    }'
>

เลย์เอาต์แยกตามหน้าสำหรับแอปพลิเคชันขนาดใหญ่

  • หากมีแค่เทมเพลตพื้นฐานและเทมเพลตแยกตามหน้าแล้วยังไม่พอ สามารถเพิ่ม เทมเพลตเลย์เอาต์ ระหว่างสองชั้นนี้ได้
  • เหมาะกับส่วนที่ต้องการชื่อเรื่อง เมนูนำทาง และโครงสร้างเนื้อหาที่ต่างจากหน้าทั่วไป เช่น หน้าผู้ดูแลระบบ
  • base จะเรียก layout แทนการเรียก page:content โดยตรง
<body>
    {{template "layout" .}}
</body>
  • layouts/admin.tmpl จะกำหนดโครงสร้างร่วมของส่วนผู้ดูแลระบบ และเรียก page:content ภายใน
    • ชื่อเรื่องของส่วนผู้ดูแลระบบ
    • เมนูนำทางไปยังผู้ใช้และคำสั่งซื้อ
    • <main> สำหรับใส่เนื้อหาเฉพาะหน้า
  • handler จะส่งทั้งเลย์เอาต์และเทมเพลตของหน้าที่จะใช้ให้กับ render()
app.html.render(
    w,
    200,
    nil,
    "base",
    "layouts/admin.tmpl",
    "pages/admin-orders.tmpl",
)
  • วิธีนี้ช่วยให้คงเทมเพลตพื้นฐานที่ใช้ร่วมกันไว้ พร้อมทั้งผสมเลย์เอาต์ตามเส้นทางกับเนื้อหาเฉพาะหน้าได้ จึงขยายใช้รูปแบบการเรนเดอร์เดียวกันกับหน้าที่ต้องมีโครงสร้างแยกต่างหากอย่างส่วนผู้ดูแลระบบได้

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความเห็นจาก Hacker News
  • ชอบ Go + HTMX และใช้ร่วมกับ a-h/templ เพื่อเพิ่ม type safety ของเทมเพลต คอมโพเนนต์ และ HTML บางส่วน เรียกชุดเครื่องมือทั้งหมดที่ประกอบด้วย Go·Unix·SQLite ว่า GUS stack และได้เผยแพร่ไว้: https://housecat.com/blog/the-gus-stack-go-unix-sqlite ได้แรงบันดาลใจอย่างมากจาก GUTS stack ของ exe.dev แต่ใช้ HTMX แทน TypeScript: https://exe.dev/docs/guts สำหรับ stack trace errors ใช้ cockroachdb/errors, สำหรับ HTML แบบ type-safe ใช้ templ, สำหรับการสร้าง OpenAPI spec ใช้ fuego, สำหรับการสร้างโค้ด SQL ใช้ sqlc, สำหรับ SQLite แบบ pure Go ใช้ modernc.org/sqlite, สำหรับ migration ใช้ goose, สำหรับ workflow แบบ persistent ใช้ dbos, และสำหรับการทดสอบกับระบบอัตโนมัติของ Chrome/CDP ใช้ rod ชุดนี้ให้ประสิทธิผลดีทั้งตอนเขียนโค้ดเองหรือตอนให้เอเจนต์ช่วยเขียน และในกระบวนการ build·deploy เป็น single binary

    • เวลาพูดถึง HTMX รู้สึกเสียดายที่ Hyperscript ไม่ค่อยได้รับความสนใจเท่าที่ควร มันสามารถจัดการ DOM ได้โดยไม่ต้องมี server round-trip หรือฟังก์ชัน JavaScript แยกต่างหาก จึงเข้ากับ Go + HTMX ได้ดีมาก และให้ความรู้สึกเหมือนเป็นส่วนขยายของ HTML ที่กำลังทำอยู่แล้ว บางคนอาจไม่ชอบการเขียนแบบ declarative แต่ใน Open Payment Host ที่ต้องรองรับหลาย payment gateway ก็สามารถทำ DOM manipulation ที่ซับซ้อนได้ด้วย Hyperscript เพียงอย่างเดียว https://hyperscript.org/, https://openpaymenthost.com/
    • อาจเปลี่ยนชื่อเป็น HUGS stack ได้เช่นกัน ถ้าจะรวม HTMX หรือ hypertext·Unix·Go·SQLite เข้าด้วยกัน
    • เคยเห็นคนเรียก Go·Templ·HTMX ว่า GoTH stack และถ้าเพิ่ม sqlc เข้าไปก็จะเป็น GoTHs หรือ GHosT ถ้ายอมรับ go generate ./... เป็นขั้นตอนหนึ่งของการ build ก็จะได้ประโยชน์มาก และยังใช้ goverter เพื่อสร้างตัวแปลงระหว่างโมเดลของ sqlc กับอ็อบเจ็กต์เทมเพลต·ค่าที่ส่งกลับ โค้ด boilerplate ราว 50% ถูกสร้างอัตโนมัติ ทำให้ type safety ดีมาก
    • ช่วงหลังชอบใช้ jet สำหรับ SQL แบบ type-safe มันต้องมีฐานข้อมูลที่กำลังรันอยู่สำหรับการสร้างโค้ด แต่ก็มองว่านี่เป็นข้อดีเพราะบังคับให้ต้อง apply migration ก่อนจะเขียนโค้ด https://github.com/go-jet/jet
    • สำหรับ reactive web app ขนาดเล็กอย่าง real-time API dashboard ที่อาจไม่จำเป็นต้องมีฐานข้อมูลของตัวเองด้วยซ้ำ ช่วงนี้มักใช้ PAHG template เป็นชุด Pico.css·Alpine.js·HTMX·Go และแน่นอนว่าสามารถต่อ SQLite เข้าไปได้ด้วย https://github.com/hiAndrewQuinn/pahg-template
  • HTMX ยอดเยี่ยมมากสำหรับหลายกรณีใช้งาน แต่ถ้าเพื่อนร่วมทีมไม่เห็นด้วย จะนำมาใช้ได้ยากมาก จะมีแรงต้านว่า มันไม่ใช่เทคโนโลยีจริงจัง และบั๊กสารพัดก็มักถูกโทษว่าเป็นความผิดของ HTMX ก่อน ทั้งที่ภายหลังจะพบว่าเป็นความเข้าใจผิด แต่ความเชื่อมั่นก็เสียไปแล้ว แม้แต่ในทีมที่ดีที่สุดที่เคยร่วมงานด้วยก็ยังเป็นแบบนั้น สุดท้ายจึงได้บทเรียนว่าต้องเลือกว่าจะสู้เรื่องไหน มองว่า html/template ของ Go ก็มีอินเทอร์เฟซที่ไม่เป็นธรรมชาติเป็นพิเศษเช่นกัน ตามที่ 《A Philosophy of Software Design》 แนะนำ ความซับซ้อนควรถูกผลักเข้าไปไว้ภายใน แต่โครงสร้างที่ต้องคอยกังวลเรื่องการคัดลอกเทมเพลตทุกครั้งเวลาจะเรนเดอร์ HTML นั้นซับซ้อนเกินจำเป็น

    • ฉันเองก็ชอบ HTMX แต่เวลาเอาเข้าทีมก็เจอความจริงแบบเดียวกัน เคยเสนอให้ใช้กับเว็บแอปง่าย ๆ แต่คนที่ทำงานด้วยซึ่งคุ้นกับการคืนค่า JSON แล้วเรนเดอร์ด้วย JS/jQuery มาหลายปี กลับลำบากทุกขั้นตอนเมื่อต้องแก้ปัญหาเดิมด้วยวิธีใหม่ ส่วนอีกคนก็ไม่ลองเลย และในทีมก็พูดถึงแต่ Blazor อยู่ตลอด ไม่ว่าแนวทางใหม่แบบไหนก็คงยากสำหรับพวกเขาอยู่แล้ว แต่สำหรับ HTMX แทบไม่มีโอกาสเลย ทีมเล็กแบบนี้น่าจะได้ประโยชน์จากการหลีกเลี่ยง JavaScript ไป 95% และสร้างโค้ดกับ HTML จากฝั่งเซิร์ฟเวอร์ ซึ่งจะทำให้โค้ดเบสเป็นระเบียบขึ้นและทดสอบง่ายขึ้น น่าเสียดายมาก ทุกวันนี้ในโปรเจกต์ส่วนตัวยังใช้ HTMX ต่อไป และพบ workflow ที่ดีจนรู้สึกว่าเว็บดีเวลอปเมนต์ง่ายขึ้นมาก
    • ในบทบาทที่ปรึกษา ฉันพยายามชี้นำทีมอย่างจริงจัง โดยอธิบายว่าการเรียนรู้ HTMX จะช่วยให้เข้าใจ เบราว์เซอร์ซึ่งเป็นสภาพแวดล้อมการทำงานจริง ได้ลึกขึ้น ในขณะที่ React และพวกเดียวกันกลับแยกนักพัฒนาออกจากเบราว์เซอร์ แต่ฉันก็เจอเรื่องแบบเดียวกัน: https://www.reddit.com/r/htmx/s/DuXyGgsCWK html/template มีข้อดีตรงความปลอดภัยพื้นฐานและการเป็นส่วนหนึ่งของมาตรฐาน แต่ด้วยความเป็นไดนามิกของมันจึงมีพื้นที่ที่ไม่มีคำตอบตายตัวมากเกินไป และแม้แต่ Templ เอง พอลองเขียนแอปเชิงพาณิชย์ขนาดใหญ่หลายตัวแล้วก็ยังรู้สึกว่าความสะดวกในการพัฒนาไม่ค่อยดีพอ เลยสร้าง gsx ขึ้นมา: https://github.com/gsxhq/gsx
    • ไม่จำเป็นต้องคัดลอกทุกครั้งตอนเรนเดอร์เทมเพลต แค่คอมไพล์ครั้งเดียวแล้วรันก็พอ ปัญหาในต้นฉบับเกิดจากการพยายามกำหนดชื่อหน้าจาก main template เอง หากส่งชื่อเรื่องผ่าน context แล้วแยก base.tmpl เป็น BASE_BEGIN กับ BASE_END เพื่อใช้ใน final template ก็ได้ ชื่อเรื่องของแต่ละหน้ายังต้องส่งตอนรันอยู่ดี แต่แนวทางในต้นฉบับก็จะพังอยู่แล้วเมื่อเริ่มรองรับ หลายภาษา
    • ที่บริษัทเราเป็นตรงกันข้ามเลย ต่อให้มีปัญหาสารพัดก็แทบเป็นไปไม่ได้ที่จะโน้มน้าวว่า React SPA ขยายต่อไม่ได้ เพราะมันถูกมองว่าเป็นเทคโนโลยีที่เป็นที่ยอมรับแล้ว แต่ละปัญหาถูกมองแยกกัน และไม่สามารถเชื่อมโยงภาพใหญ่ได้ว่าทั้งหมดมีต้นตอมาจากโครงสร้างเดียวกัน
  • ในโปรเจกต์ล่าสุด ใช้ HTMX แล้วสนุกมาก ในฐานะคนที่รู้จักเว็บก่อนยุค AngularJS และ React ชอบมากตรงที่สามารถสร้างหน้าเว็บจริง ๆ และลดปริมาณ JavaScript ให้เหลือน้อยที่สุดได้ จะทำด้วย JavaScript ล้วนก็ได้ แต่ HTMX ช่วยจัดการ boilerplate ของ event handling ที่ซ้ำ ๆ ให้ ถ้าไม่ชอบแนวคิดของฟรอนต์เอนด์สมัยใหม่ ก็น่าลองใช้ดู ตัวอย่างแรก ๆ บนเว็บไซต์ทางการอาจพื้นฐานมาก แต่พอเรียนรู้เพิ่มอีกนิดจะพบว่ามันทรงพลังกว่านั้นมาก

    • อยากแนะนำ SvelteKit แบบระมัดระวังด้วย ภายนอกมันอาจดูเหมือนเป็น JavaScript frontend framework อีกตัวหนึ่ง แต่พอได้ลองใช้จริงก็พบว่ามันรองรับการออกแบบแบบยึด hypertext เป็นศูนย์กลางที่สาย HTMX มุ่งหวังไว้ได้อย่างยอดเยี่ยม
  • เคยลองทำโปรเจ็กต์ขนาดค่อนข้างใหญ่ด้วย HTMX + Go แต่ก็ยังรู้สึกว่าไม่พอ และก็ไม่แน่ใจว่าต่อไปจะไปถึงระดับที่ต้องการได้หรือไม่ มันยอดเยี่ยมสำหรับแอป CRUD แบบง่ายและแอดมินแดชบอร์ด แต่พอมีคอมโพเนนต์ที่เชื่อมโยงกัน สถานะที่ใช้ร่วมกัน และปฏิสัมพันธ์ซับซ้อนจำนวนมาก การดูแลจัดการจะยากขึ้นอย่างรวดเร็ว ตอนแรกเลือก HTMX เพราะไม่ชอบ React แต่สุดท้ายก็ยังคงใช้ Go ในฝั่งแบ็กเอนด์ และเปลี่ยนฝั่งฟรอนต์เอนด์เป็น SvelteKit SPA mode ซึ่งทำให้แยกสองฝั่งออกจากกันได้อย่างสะอาด และพัฒนา/ดูแล UI ที่ซับซ้อนได้ง่ายกว่ามาก Svelte ให้ความรู้สึกเหมือนเป็นส่วนขยายตามธรรมชาติของ HTML มากกว่าจะเป็นภาษาอีกภาษาหนึ่งอย่าง JSX โมเดลคอมโพเนนต์และการจัดการสถานะก็เรียบง่าย และไวยากรณ์ $state แบบใหม่ก็ดีเป็นพิเศษ

    • ฝั่งเว็บของ HTMX ก็ระบุ จุดแลกเปลี่ยน ไว้ชัดเจนเหมือนกันว่าไม่เหมาะกับงานลักษณะนั้น แต่ถ้าจะหาบทความที่พูดเรื่องนี้ต้องขุดลึกพอสมควร
    • Hyperscript โปรเจ็กต์ฝั่งฟรอนต์เอนด์พี่น้องของทีม HTMX จัดการความสามารถอย่างสถานะฝั่งฟรอนต์เอนด์ ไม่แน่ใจว่าได้ลองใช้หรือยัง: https://hyperscript.org/docs/reactivity/
    • แก่นของแนวคิด HTMX อาจอยู่ที่การตั้งคำถามว่าแต่แรกเลยจำเป็นต้องมี คอมโพเนนต์ที่เชื่อมกันด้วยสถานะร่วม แบบนั้นจริงหรือไม่ ท้ายที่สุดแล้ว แอปส่วนใหญ่ก็แค่ทำ CRUD กับฐานข้อมูล
  • ช่วงนี้โปรเจ็กต์เล่นเกือบทั้งหมดเขียนด้วย Go + HTMX ไม่ว่าจะมี LLM ช่วยหรือไม่ก็ตาม ทั้ง Opus และ GPT ก็จัดการสแต็กนี้ได้ดีมาก สร้างและเริ่มต้นได้เร็ว แถมเป็นไบนารีเดียวจึงสะดวกต่อการดีพลอยและโฮสต์ เป็นสแต็กที่ดีมากสำหรับการพัฒนาแบบวนซ้ำอย่างรวดเร็ว

  • ถ้าจะใช้ HTMX ขอแนะนำอย่างยิ่งให้ใช้วิธีสร้าง HTML ในแบ็กเอนด์ที่ดึงชิ้นส่วน HTML ซ้ำๆ ออกมาเป็นฟังก์ชันเพื่อ ทำให้เป็นคอมโพเนนต์ ได้ง่าย แบบเดียวกับ React HTMX ต้องการความยืดหยุ่นใน HTML ที่แบ็กเอนด์สร้าง เพราะบางกรณีต้องเพิ่มหรือลบแท็กติดกับ HTML บางส่วนโดยตรง เช่น ถ้าเจอแท็ก `` ที่ระดับบนสุดก็จะอัปเดตชื่อหน้า งานแบบนี้ทำได้ยากในเทมเพลตเอนจินแบบสตริงดั้งเดิม แต่ใน ไลบรารี HTML แบบฝังในภาษา จะง่ายมาก และยังใช้ความสามารถด้านนามธรรมของภาษาทั้งหมดได้ จึงเข้ากับ HTMX ได้ดีเป็นพิเศษ ถ้าเป็นแบ็กเอนด์ JavaScript ก็ใช้ tagged template literal function อย่าง https://github.com/WebReflection/uhtml-ssr ได้ ถ้าเป็น Go ก็มี https://www.gomponents.com/ และถ้าเป็น Scala ก็มี ScalaTags

    • ในแบ็กเอนด์ Python ใช้ JinjaX ได้ผลดี: https://jinjax.scaletti.dev/
    • uhtml-ssr ดูเหมือนจะเก่าพอสมควร ถ้าต้องการไลบรารี tagged template literal สำหรับ HTML ที่ไม่มี dependency อาจดู implementation และเอกสารต่อไปนี้ได้ implementation: https://github.com/mastrojs/mastro/blob/main/src/core/html.t... เอกสาร: https://mastrojs.github.io/docs/html-components/
    • ในแบ็กเอนด์ก็ใช้ JSX ได้เช่นกัน React มี renderToStaticMarkup อยู่แล้ว แต่การเลือกไลบรารี JSX ที่เขียนมาเพื่อแบ็กเอนด์โดยเฉพาะน่าจะดีกว่า
  • รู้จัก Go ครั้งแรกจากหนังสือของ Alex Edwards และ Learn Go with Tests ซึ่งทุกวันนี้ก็ยังแนะนำอยู่ เลยอยากลองแปลงบางโปรเจ็กต์เก่าไปใช้ HTMX ดู

    • ถ้าสนใจการพัฒนาเว็บด้วย Go ไม่มากก็น้อย ควรอ่านทั้ง Let's Go และ Let's Go Further ของ Alex Edwards เป็นหนังสือเริ่มต้นภาษาโปรแกรมที่เข้าถึงง่าย สนุก และใช้งานได้จริงที่สุดเท่าที่เคยอ่านมา
  • ทำเฟรมเวิร์กส่วนตัวบนพื้นฐาน Kotlin + HTMX ไว้ใช้เอง: https://github.com/reubenfirmin/zoned เป้าหมายคือดูว่าจะทำเว็บแอปให้ type-safe แบบ end-to-end ได้หรือไม่ และใช้ Kotlinx.html ซึ่งเป็น DSL สำหรับเขียน HTML คล้าย JSX 90% แรกทำเองทั้งหมด และใช้ Claude ช่วยใน 10% สุดท้ายกับ README เพื่อปิดงานให้พร้อมเผยแพร่

  • ชอบทั้ง Go และ Alex Edwards แต่ผลลัพธ์ที่ได้จากการใช้ HTMX ทำให้ผิดหวังเสมอ รู้สึกว่าความซับซ้อนของโค้ดเบสโตเร็วกว่าแอปจริง 2 เท่า และมักเจอกรณีขอบที่หนีไม่พ้นหากไม่มีวิธีแก้แบบแปลกๆ เข้าใจว่าทำไมหลายคนถึงไม่ชอบแพ็กเกจ Node แต่ HTMX ดูเหมือนเป็นการชดเชยมากเกินไป เวลาที่ประหยัดจากการไม่ต้องต่อสู้กับ JSON น้อยกว่ามากเมื่อเทียบกับเวลาที่ต้องใช้เพิ่ม 3 เท่าเพื่อทำให้หน้าตาและประสบการณ์ใช้งานของแอปออกมาดี Mantine template ตั้งค่าเสร็จใน 2 นาทีแล้วใช้ UI component คุณภาพสูงได้ทันที และถ้ารวม static asset ที่ build แล้วเข้าไป ก็ยังทำเป็น Go binary เดียวได้เหมือนกัน: https://github.com/mantinedev/vite-min-template

    • ตอนเจอไฮเปอร์มีเดียครั้งแรกผ่าน Rails มันสะดวกและง่ายมาก เลยแปลกใจที่ Go/HTMX มีแรงเสียดทาน แต่หลังจากลองหลายครั้งก็ได้เรียนรู้ว่า นามธรรมสำคัญมาก ถ้าไม่มีตัวสร้างคอมโพเนนต์และ reverse router จะค่อนข้างเจ็บปวด ซึ่ง Rails มีมาให้โดยปริยายเลยไม่ต้องคิดเรื่องนี้ ข้อดีที่ใหญ่ที่สุดคือการคำนวณกับข้อมูลอยู่ที่เดียวกัน LLM ก็ใช้งานและทดสอบแอปไฮเปอร์มีเดียแบบครบในตัวได้ดี และรอบการทำซ้ำก็สั้นลงเพราะไม่ต้องรอ JavaScript แต่ก็เห็นด้วยว่าการเริ่มต้นยากเพราะไม่มีไลบรารีคอมโพเนนต์แบบหยิบใช้ได้ทันที
    • การจะใช้ HTMX ได้ ต้องเปลี่ยน ปรัชญาการออกแบบทั้งชุด มันอาจไม่ใช่ข้อเสียเสมอไป แถมอาจเป็นการเปลี่ยนที่ดีด้วยซ้ำ แต่ถือเป็นการเปลี่ยนใหญ่ และผลลัพธ์สุดท้ายจะใกล้กับความเป็นเว็บโดยธรรมชาติมากกว่าการพยายามเลียนแบบแอปมือถือ
    • อยากรู้ว่ามี ตัวอย่าง UI แบบเจาะจง อะไรบ้างที่ทำได้ด้วย Mantine แต่ทำด้วย HTMX ไม่ได้
  • ชอบ Go + Datastar มากกว่าเยอะเพราะเรียบง่ายกว่า

    • Go กับ Datastar เข้ากันได้ดีมากเหมือนถุงมือที่ตัดมาเฉพาะ อยากเพิ่ม a-h/templ เข้าไปในเทมเพลตด้วย ซึ่ง Datastar ก็รองรับ น่าแปลกใจที่ Datastar ยังไม่เป็นที่รู้จักมากกว่านี้
    • ฉันก็ชอบ Datastar มากกว่าเหมือนกัน แต่ ฟีเจอร์ใหม่ของ HTMX4 กำลังทำให้ช่องว่างแคบลงมาก ดีใจที่ได้เห็น ecosystem ค่อยๆ ลู่เข้าหาแพตเทิร์นที่ดี