PostgREST: ให้บริการคอนเทนต์ HTML ด้วย htmx
(postgrest.org)- หากฟังก์ชัน PostgreSQL ส่งคืน
text/htmlได้โดยตรง ก็สามารถสร้างแอป to-do ที่รับผลลัพธ์จากคำขอ AJAX และแทนที่บางส่วนของ DOM ได้ด้วยแค่ PostgREST และ htmx - ตัวอย่างนี้ให้สิทธิ์
web_anonกับapi.todosและapi.todos_id_seqโดยไม่ใช้การยืนยันตัวตน และเพิ่ม custom media type เพื่อรองรับคำขอAccept: text/html - การประกอบ HTML เป็นหน้าที่ของฟังก์ชัน SQL โดย
api.sanitize_html,api.html_todo,api.html_all_todosจะจัดการการ escape ค่าที่ป้อนเข้าและการเรนเดอร์รายการ - การทำงานบนหน้าจอประกอบด้วย
hx-post,hx-get,hx-target,hx-trigger,hx-vals,hx-headersและ HTML ที่ตอบกลับจะเข้ามาแทนที่#todo-list-areaหรือพื้นที่แก้ไขแต่ละรายการ - โครงสร้างนี้ใกล้เคียงกับ proof of concept และฝั่ง PostgREST ก็กำลังพัฒนา
plmustacheเพื่อปรับปรุงการจัดการ HTML
บทบาทของ PostgREST และ htmx
- PostgREST ส่งคืน คอนเทนต์ HTML และ htmx ใช้ HTML ที่ได้จากผลลัพธ์ของคำขอ AJAX เพื่อแทนที่องค์ประกอบภายใน DOM
- htmx คาดหวังการตอบกลับเป็น HTML จึงเข้ากันได้ดีกับรูปแบบที่เซิร์ฟเวอร์ส่งคืน HTML fragment แทน JSON
- ตัวอย่างนี้เป็น proof of concept ที่แสดงให้เห็นว่าการใช้สองเทคโนโลยีนี้ร่วมกันสามารถทำอะไรได้บ้าง
- PostgREST กำลังพัฒนา plmustache เพื่อปรับปรุงด้านการจัดการ HTML ให้ดียิ่งขึ้น
การเตรียมตั้งค่าแอป to-do
- ตัวอย่างนี้เป็นแอป to-do ที่อ้างอิงจาก Tutorial 0 - Get it Running
- เพื่อความเรียบง่าย ตัวอย่างนี้ไม่ใช้การยืนยันตัวตน และมอบสิทธิ์ที่จำเป็นให้กับผู้ใช้
web_anongrant allบนตารางapi.todosgrant usage, selectบนซีเควนซ์api.todos_id_seq
- หากต้องการให้ PostgREST มองคำขอ
Accept: text/htmlจากเบราว์เซอร์เป็น HTML ต้องเพิ่ม media type handler- สร้างโดเมน
"text/html"จากtext - การตั้งค่านี้ทำให้ PostgREST ส่งคืนเอกสาร HTML แบบดิบได้
- สร้างโดเมน
การส่งคืนหน้า HTML พื้นฐาน
- ฟังก์ชัน
api.index()ส่งคืน"text/html"และสร้างเอกสาร HTML พื้นฐาน - ภายในหน้ามีชื่อ
PostgREST + HTMX To-Do Listพร้อมทรัพยากรต่อไปนี้ - ในเบราว์เซอร์สามารถเปิดหน้านี้ได้ที่
http://localhost:3000/rpc/index
การเรนเดอร์รายการและการเพิ่ม to-do
- ใช้ฟังก์ชัน SQL สามตัวเพื่อสร้างรายการ to-do เดิมให้ออกมาเป็น HTML
api.sanitize_html(text): แทนที่อักขระ&,",>,<,'เป็น HTML entityapi.html_todo(api.todos): จัดรูปแบบรายการ to-do เดี่ยวเป็น HTML fragmentapi.html_all_todos(): รวมทุกรายการเป็นสตริง HTML เดียว และหากไม่มีรายการจะส่งข้อความThere is nothing else to do.
api.html_todoและapi.html_all_todosเป็นฟังก์ชันภายในสำหรับสร้างเทมเพลตรายการ จึงไม่ได้ใช้เป็น PostgREST endpoint โดยตรงapi.add_todo(_task text)แทรก to-do ใหม่ลงในapi.todosแล้วส่งคืน HTML ของรายการทั้งหมดที่อัปเดตแล้วapi.index()ที่อัปเดตแล้วจะมี htmx และฟอร์มรับข้อมูลรวมอยู่ด้วย- ใส่
hx-headers='{"Accept": "text/html"}'ไว้ที่องค์ประกอบระดับบน เพื่อให้คำขอ htmx ที่อยู่ด้านล่างร้องขอการตอบกลับแบบ HTML - หากไม่มี header นี้ PostgREST จะไม่มองคำขอเป็น HTML
- ใส่
- ฟอร์มเพิ่ม to-do ทำงานผ่านแอตทริบิวต์ของ htmx
hx-post="/rpc/add_todo": ส่งค่าอินพุต_taskแบบ POST ไปยัง/rpc/add_todohx-target="#todo-list-area": นำ HTML ที่ตอบกลับไปใส่ในพื้นที่รายการ to-dohx-trigger="submit": ส่งคำขอเมื่อมีการ submit ฟอร์มhx-on="htmx:afterRequest: this.reset()": ล้างฟอร์มหลังคำขอเสร็จสิ้น
- ต้องรีเฟรช schema cache เพื่อให้ฟังก์ชันใหม่และการเปลี่ยนแปลงต่าง ๆ มีผล
- หลังจากนั้นสามารถดูรายการและเพิ่ม to-do ใหม่ได้ที่
http://localhost:3000/rpc/index
การแก้ไข การลบ และการเปลี่ยนสถานะเสร็จสิ้น
api.html_todoถูกขยายให้มี UI สำหรับเปลี่ยนสถานะเสร็จสิ้น แก้ไข และลบในแต่ละรายการ- การเปลี่ยนสถานะเสร็จสิ้นจัดการด้วย
<form>และแอตทริบิวต์ของ htmxhx-post="/rpc/change_todo_state": ส่งคำขอ AJAX แบบ POST ไปยัง endpoint นั้นhx-vals='{"_id": ..., "_done": ...}': เพิ่มพารามิเตอร์ของคำขอแทนการใช้ hidden inputhx-trigger="click": เรียกคำขอเมื่อคลิกรายการ
- ปุ่มแก้ไขจะเปลี่ยนงานแต่ละรายการให้เป็นช่องอินพุต
hx-get="/rpc/html_editable_task": เรียก endpoint ที่ส่งคืนอินพุต HTML สำหรับแก้ไขได้hx-target="#todo-edit-area-...": แทนที่เฉพาะพื้นที่ของงานรายการนั้น ไม่ใช่ทั้งรายการทั้งหมดhx-valsใช้เพิ่มพารามิเตอร์ให้คำขอ GET และเมื่ออ้างถึงคอลัมน์ของตารางจะต้องใช้ตัวดำเนินการeq.
- ปุ่มลบจะส่งคำขอลบ to-do นั้นด้วย
hx-post="/rpc/delete_todo" api.html_editable_task(_id int)ส่งคืน HTML ของช่องอินพุตที่ใช้แก้ไข to-do ที่ระบุ- ส่งชื่องานที่แก้ไขแล้วด้วย
hx-post="/rpc/change_todo_task" - อัปเดตเมื่อ submit หรือเมื่อโฟกัสหลุดด้วย
hx-trigger="submit,focusout" - รวม
hx-headers='{"Accept": "text/html"}'ไว้ด้วย
- ส่งชื่องานที่แก้ไขแล้วด้วย
- endpoint ที่ใช้เปลี่ยนข้อมูลทั้งหมดจะส่งคืน HTML ของรายการทั้งหมดหลังแก้ไขเสร็จ
api.change_todo_state(_id int, _done boolean): อัปเดตคอลัมน์doneapi.change_todo_task(_id int, _task text): อัปเดตคอลัมน์taskapi.delete_todo(_id int): ลบ to-do ที่ตรงกับ_id
- หลังรีเฟรช schema cache แล้ว จะสามารถแก้ไข ลบ และทำเครื่องหมายเสร็จสิ้นให้ to-do ได้ที่
http://localhost:3000/rpc/index
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
PostgREST เป็นหนึ่งในโปรเจกต์โอเพนซอร์สที่ฉันชอบที่สุด มองว่าที่ Supabase กลายเป็นบริษัทระดับพันล้านดอลลาร์ได้ ส่วนใหญ่ก็เพราะการออกแบบที่ยอดเยี่ยมของ PostgREST และ Postgres
ไม่รู้ว่า Supabase สนับสนุนโปรเจกต์นี้อย่างไรบ้าง แต่หวังว่าน่าจะเป็นในระดับใหญ่มาก น่าเศร้ากับความเป็นจริงของการสนับสนุนทางการเงินในโอเพนซอร์ส เมื่อเห็นว่าโปรเจกต์ที่มีบริษัททำรายได้หลายร้อยแห่งใช้เป็น dependency หลัก กลับมีผู้สนับสนุนแบบเสียเงินเพียง 12 คน
https://www.patreon.com/postgrest/about
https://github.com/steve-chavez
เพราะแบบนี้ตอนนี้เลยใช้แต่ AGPLv3 หรือไลเซนส์คล้ายกัน ถ้าจะใช้เชิงพาณิชย์ก็จ่าย 1% ของรายได้รวมไปเลย
ในฐานะ proof of concept มันเท่มาก และการทำออกมาก็น่าชื่นชม แต่ถ้าต้องดูแลรักษาในเว็บแอประดับที่ไม่ใช่งานเล็ก ๆ ก็ดูเหมือน ฝันร้าย
https://sqitch.org/
สงสัยว่าความสามารถหรือแพตเทิร์นการเขียนโค้ดแบบนี้ยังถือว่าใหม่ หรือยังถูกใช้ในแอปพลิเคชันสมัยใหม่หรือไม่
CouchDB ซึ่งเป็นฐานข้อมูลเอกสาร JSON เคยมี API แบบ HTTP และมีเมธอด list/detail ในตัวที่สามารถตอบกลับได้ในแทบทุกรูปแบบที่สร้างได้ภายใน JavaScript interpreter นั่นหมายความว่าไคลเอนต์สามารถเรียกฐานข้อมูลโดยตรงเพื่อรับ HTML หรือ JSON ได้ และไม่ต้องมีเซิร์ฟเวอร์เลย
แต่หลังจาก v1 พวกเขาก็หยุดไปในทิศทางนั้น เพราะการดูแลรักษามันกลายเป็นฝันร้าย ใครที่จำยุคทองของ PHP หรือไฟล์ ASP แบบเก่าที่มีทั้ง SQL และ HTML ปนกันอยู่ในไฟล์เดียวได้ ก็คงจะรู้ว่านี่แทบไม่ต่างกันเลย
ราว 13 ปีก่อนฉันชอบฟีเจอร์ด้านเว็บของ CouchDB มากในโปรเจกต์ส่วนตัว แต่สำหรับการทำงานเป็นทีมมันค่อนข้างใช้งานลำบาก
แอป PostgREST เพียงตัวเดียวที่ฉันเคยทำด้วยนั้นแย่มาก เพราะเหมือนเฟรมเวิร์ก “เรียบง่าย” แบบนี้ส่วนใหญ่ มันจะเรียบง่ายอยู่แค่จนกว่าความต้องการจะเริ่มซับซ้อน
คนเขียนเดิมสุดท้ายต้องใช้ stored procedure ในฐานข้อมูลจำนวนมากเพื่อให้ได้ผลลัพธ์ที่ต้องการ และนั่นก็นำไปสู่ปัญหาด้าน scalability ตามมา อย่างที่มักเป็นกัน ทางออกก็คือกลับไปใช้ SQL
PostgREST ถูกสร้างมาสำหรับแอปแบบ CRUD และแอปจำนวนมากที่ฉันเจอก็เข้าข่ายนี้ ลอจิกแบ็กเอนด์เฉพาะทางที่จำเป็นเป็นครั้งคราวก็แยกไปทำในเซิร์ฟเวอร์อีกตัวหรือ edge function ได้
เครื่องมือแนวนี้ก็เหมือนกันหมด พอเจอความต้องการจริงในโลกความเป็นจริง ต้นทุนในการปรับตัวให้เข้ากับเครื่องมือกลับแย่กว่าสิ่งที่มันตั้งใจจะมาแทนที่อย่าง SQL + ภาษาและเฟรมเวิร์กสักตัวเสียอีก ทุกวันนี้ PostGREST เองก็เริ่มซับซ้อนขึ้นแล้ว และฟีเจอร์เพิ่มแบบนี้ยิ่งทำให้ฉันรู้สึกดึงดูดน้อยลง
ก่อนที่ Rails จะมาและทำให้ทุกคนเชื่อว่าการใส่ business logic ไว้ในภาษาเซิร์ฟเวอร์ที่ช้านั้นเป็นความคิดที่ดี ผู้คนก็สร้างแอปกันแบบนี้ทั้งนั้น
เป็นสแตกพัฒนาเว็บที่สะอาดมาก มีแค่ HTML กับฐานข้อมูล ไม่ต้องมีทั้งแบ็กเอนด์และฟรอนต์เอนด์
https://github.com/omnigres/omnigres
เมื่อความใหม่ของ HTMX จางลง ผู้ดูแลในตอนนั้นสุดท้ายก็จะยอมรับว่าต้องเขียนใหม่ทั้งหมดตั้งแต่ต้น
เคยใช้ PostgREST ในโปรเจกต์เก่าโดยไม่มี HTMX และก็ประทับใจว่ามันไปได้ไกลแค่ไหน
HTMX เองก็ดูเข้ากันดี แต่ไม่แน่ใจนักว่าอยากดูแลรักษาเทมเพลต HTMX มากแค่ไหนเมื่อมันอยู่ใน SQL function
เอาไว้ทำเล่น ๆ ได้ดี แต่ไม่เหมาะกับการสร้างของที่ต้องเสถียร
ถ้ามองจากมุม Haskell แล้ว PostgREST น่าสนใจมาก เพราะมันดูเป็นโปรเจกต์ที่ชัดเจนเสียจนเหมือนไม่ต้องคิดอะไร
แต่นั่นแหละที่ทำให้มันอัจฉริยะ เป็นไอเดียที่ Haskell มาก ๆ ซึ่งฉันชอบ
หากจะทำให้แนวคิดนี้กลายเป็น สแตกที่ดูแลรักษาได้ พร้อมประสบการณ์ผู้ใช้ที่ดีแม้ในแอปพลิเคชันขนาดกลางถึงใหญ่ คุณคิดว่าจำเป็นต้องมีเครื่องมืออะไรเพิ่มเติมบ้าง?
ผมเองก็เคยพยายามทำอะไรคล้ายกันบน SQL โดยใช้สแตกเทคโนโลยีเป็นเทมเพลต Jinja, SQL และชั้น OpenAPI ที่เขียนด้วย YML แต่ถึงอย่างนั้นก็คงจำกัดไว้ในขอบเขตของเครื่องมือภายในอยู่ดี ดูได้ที่: https://jinj.at
Show HN ที่เกี่ยวข้อง: เรนเดอร์ HTML จาก SQL ด้วย pg_render
https://news.ycombinator.com/item?id=38677852
อยากจดงานหนึ่งไว้ในแอปนี้กันลืม งานของผมคือสิ่งนี้:
ในกรณีนี้ดูเหมือนว่า คอลัมน์ task จะไม่ได้ถูก sanitize จากที่ไหนเลย
แต่ตอนนี้ฐานข้อมูลกลายเป็นวิวเอง งั้น XSS ก็กลับมาอีกแล้วหรือ? นี่เป็นปัญหาที่ป้องกันได้
HTML สามารถเรนเดอร์ด้วยภาษาเทมเพลตใด ๆ ที่จัดการเรื่องนี้ให้ได้