ตัวอย่างการออกแบบ Frontend Architecture ในเว็บแอพพลิเคชั่นขนาดใหญ่

 

ttt_Software_Development_Process-venimo
Credit: ShutterStock.com

 

การออกแบบสถาปัตยกรรมในส่วนของ Frontend เป็นเรื่องที่หลายๆคนมองข้ามไป  ทั้งๆที่ Frontend มีบทบาทสำคัญมากในเว็บปัจจุบัน

สมัยก่อน เราอาจทนใช้เว็บที่โหลดช้าหรือใช้งานยากได้ เพราะไม่มีตัวเลือกมาก แต่ในปัจจุบัน คู่แข่งโผล่รายใหม่ๆขึ้นมาแทบทุกวัน ผู้ใช้เว็บมีตัวเลือกมากขึ้น

ส่วน Frontend ที่ผู้ใช้ต้องติดต่อด้วยตลอดเวลานั้นสามารถชี้เป็นชี้ตายให้กับธุรกิจได้เลย

ด้วยเหตุนี้ ความซับซ้อนของโค้ดเริ่มย้ายจากฝั่ง Backend มายังฝั่ง Frontend มากขึ้นเรื่อยๆ หาก Frontend Architecture ถูกออกแบบไว้ไม่ดี การจะขยับขยายเว็บหรือทีมจะทำได้ลำบากมาก

บทความนี้จะกล่าวยกตัวอย่างรูปแบบของสถาปัตยกรรม Frontend ของเว็บแอพพลิเคชั่นต่างๆ  โดยจะเน้นถึงความเป็นมา และชี้ให้เห็นถึงข้อดีข้อเสียของรูปแบบต่างๆ

ผู้อ่านควรมีประสบการณ์ในการทำเว็บแอพพลิเคชั่นขนาดกลาง – ใหญ่  และมีความเข้าใจในเรื่อง Continuous integration เบื้องต้น

1. Server-generated frontend

ย้อนกลับไปยังสัก 15-20 ปีที่แล้ว เว็บต่างๆมีแค่ HTML กับ CSS เป็นหลัก เว็บจำนวนมากเป็นแบบ Static กล่าวคือ ผู้ใช้ได้แต่กดเข้าไปดูข้อมูลอย่างเดียว ไม่สามารถทำอะไรกับมันได้ (เช่นโพสต์ข้อความ คอมเม้นต์ ตอบกระทู้ กดไลค์)

ส่วนเว็บที่เป็นแบบ Dynamic การประมวลผลทั้งหมดจะต้องทำที่ฝั่งเซอร์เวอร์ ทุกครั้งที่เราพิมพ์คอมเม้นต์แล้วกดส่ง หน้าเว็บทั้งเว็บต้องถูกโหลดใหม่อีกครั้ง เพื่อให้คอมเม้นต์ปรากฏขึ้นมา ข้อมูลทุกอย่างต้องกลับไปที่เซอร์เวอร์ก่อน โดยเซอร์เวอร์จะสร้างหน้า HTML ใหม่แล้วส่งกลับมา

การรวมโค้ดทั้งส่วน Frontend และ Backend เข้าด้วย เป็นลักษณะเด่นของรูปแบบนี้  หากไปดูลึกเข้าไปในโค้ด จะพบว่ามีการรวม HTML ไว้กับ processing tag ต่างๆ (เช่น กรณี JSP, PHP, ASP) และมี if/else/for/while logic แทรกอยู่ยุ่บยั่บเต็มไปหมด

สถาปัตยกรรมรูปแบบเหล่านี้จะพบเห็นได้บ่อยมากในเว็บเก่าๆ และไม่ค่อยมีทีท่าว่าจะเปลี่ยน เพราะเปลี่ยนยากมาก

พอเวลาผ่านไป JavaScript และ AJAX จะถูกนำมาใช้ด้วย แต่แนวทางยังคงเหมือนเดิม คือใช้  ในการสร้าง HTML ขึ้นมา ถ้าคนในทีมไม่มีวินัยพอ อาจมีการสร้าง JavaScript global variable ด้วย processing tag ทำให้ทุกอย่างพันกันเละเทะไปหมด

เมื่อมีปัญหา นักพัฒนาเริ่มเรียนรู้และปรับตัวโดยใช้ MVC Framework ต่างๆเข้ามาช่วย  และทำการแยกส่วน JavaScript กับ CSS ออกมาชัดเจน ทำให้การดูแลรักษาโค้ดทำได้ง่ายขึ้น

แต่แม้จะออกแบบดียังไงก็ตาม สถาปัตยกรรมรูปแบบนี้ก็มีขีดจำกัดของมันอยู่

  1. Backend กับ Frontend พัฒนาแยกจากกันไม่ได้ หากระหว่างพัฒนาอยู่ Backend พัง  คนที่พัฒนา Frontend จะทำงานต่อไม่ได้ ต้องย้อนไป เวอร์ชั่นเก่าๆ แล้วสร้าง Branch พัฒนาแยกต่อไป  ส่งผลให้มีโอกาสเกิด Long-live branch ซึ่งขัดกับแนวคิดของการทำ Continuous integration
  2. แม้นักพัฒนาจะพยายามแยกใช้ MVC Framework แยกส่วนต่างๆออกจากกัน แต่ด้วยความที่โค้ดของทั้งสองส่วนอยู่ใกล้กันมาก ทำให้เอื้อต่อการเอานำโค้ดมาปะปนกัน หากคนในทีมขาดวินัย หรือไม่มีกระบวนการ Code review ที่ดี โค้ดทั้งสองส่วนจะปนกันง่ายมาก
  3. ในบางเทคโนโลยี (เช่น Java ที่ deploy ด้วย WAR file) แม้เราจะแก้แค่ HTML หรือ JavaScript ไม่กี่บรรทัด เราต้อง Build ทั้งส่วน Frontend + Backend แล้ว Deploy ใหม่ทั้งหมด แทนที่จะทำการ Build แค่ในส่วนของ Frontend ทำให้การพัฒนาช้ากว่าที่ควรจะเป็น

ข้อจำกัดต่างๆเหล่านี้ ทำให้เกิดรูปแบบใหม่ขึ้นมา

 

2. Separated frontend — Monolith

จากรูปแบบที่แล้ว เราจะเห็นว่าให้รวมโค้ดของ Frontend กับ Backend เข้าด้วยเป็นเรื่องที่ไม่ค่อยดีเท่าไรนัก โค้ดจากแต่ละส่วนมักจะพันกันหมด จะมี interaction ทีก็ต้องโหลดหน้าเว็บใหม่ทั้งเว็บ

อย่ากระนั้นเลย เราแยก Frontend ออกมาเป็นเรื่องเป็นราวดีกว่า แยกมันออกมาใส่ใน Repository ต่างหากจาก Backend เลย

ส่วน Backend นั้นก็แปรสภาพเป็น REST API ให้ Frontend ดึงข้อมูลในรูปแบบของ XML/JSON แทน

jemmy_frontend_arch_02

มองในมุมของผู้ใช้  รูปดีแบบนี้ดีกว่า เพราะสามารถสร้างเว็บที่ตอบสนองผู้ใช้งานได้อย่างรวดเร็ว (ไม่ต้องโหลดเพจใหม่ทั้งหมด)

มองในมุมของผู้พัฒนา มีข้อดีอยู่มากเช่นกัน:

  • หากทีมต้องการพัฒนา Mobile app ด้วย ก็สามารถให้แอพเรียกใช้ API จาก Backend ได้เลย
  • การเขียน Unit test ง่ายขึ้น เพราะ Logic ทั้งสองฝั่งแยกกันชัดเจน
  • เราสามารถ mock ฺBackend API  เวลาเขียนโค้ดไม่ต้องพึ่งอีกส่วน หากโค้ด Backend พังขึ้นมาหรือกำลังปรับ API เราก็ทำงานต่อได้ด้วย Mock ของเรา
  • การทดสอบ Edge case (เช่น เว็บคืนค่า error 404/500, คืน error code) ก็สามารถทำได้ง่ายกว่าด้วย Mock
  • สามารถพัฒนาส่วนของ Frontend ได้เร็วกว่า เช่น
    • ไม่ต้อง Build ส่วนของ Backend  หากมีการแก้โค้ดแค่ใน Frontend
    • ใช้ Grunt เพื่อทำการ watch + autoreload  ในระหว่างการพัฒนา ไม่ต้องเสียเวลา redeploy หรือ สลับหน้าต่างกด F5
    • รัน Unit test แค่ในส่วนของ Frontend อย่างเดียวได้ โดยใช้เวลาในการรันน้อยกว่า ทำให้รันบ่อยๆได้

รูปแบบนี้ดีมาก แต่สำหรับเว็บแอพพลิเคชั่นที่มีขนาดใหญ่ รูปแบบนี้จำเป็นต้องมีการแยก Module ให้ดี ไม่เช่นนั้นแล้ว โอกาสเกิด Conflict จะสูงมาก ทำให้ทำ Continuous integration ได้ลำบาก

ลองนึกภาพโค้ดทั้งหมดของ Frontend อยู่ใน repository เดียวและมีนักพัฒนาสัก 100 คนทำงานพร้อมกัน จะทำยังไงให้ Integration branch เสถียรได้เกือบตลอดเวลา เพราะถ้ามีใครพลาดขึ้นมา ทำ Build พัง  ต้อง revert กลับทันที เพื่อไม่ให้กระทบคนอื่น

ในทางปฏิบัติ นี่เป็นเรื่องที่ยากพอสมควร  ตัวอย่างเช่น อาจมีนักพัฒนาสามคน Commit ไล่เลี่ยกันในหนึ่ง Build window แล้ว Build ก็เกิดพังขึ้นมา เราจะรู้ได้ไงว่ามันพังที่ Commit ไหน (ทั้งสามคนอ้างว่า Run build local หลัง merge แล้วผ่านหมด)  ถ้าสามคนอยู่ทีมเดียวกันก็คุยกันได้ง่าย แต่ถ้าไม่ แล้วแต่ละคนนั่งอยู่กันคนละประเทศล่ะ?

เมื่อ Integration build พังบ่อยๆ เราจะเริ่มเห็น Long-live branch โผล่ขึ้นมาจากแต่ละทีม พอใกล้ release ก็จะ Merge กันที Conflict กันกระจาย ไม่ได้หลับไม่ได้นอน

พอถึงจุดที่จะทำ Continuous delivery ก็จะมีปัญหาเรื่องการ promote ขึ้น environment ต่างๆ เพราะทุกส่วนต้องพร้อมหมด หากส่วนใดส่วนหนึ่งโยน JavaScript error ขึ้นมา โค้ดส่วนอื่นอาจจะพินาศไปด้วย

เพื่อลดปัญหานี้ให้น้อยที่สุด Frontend จะมักจะถูกแยกเป็น Module ย่อยๆ โดยนักพัฒนาก็จะแบ่งเป็นทีมย่อยๆที่ทำงานแยกจากกันได้โดยเกือบจะอิสระ  โดยพยายามทำให้ Dependency ระหว่าง Module มีน้อยที่สุด

jemmy_frontend_arch_02-2

จะเห็นได้ว่า แม้รูปแบบนี้จะดีกว่าการทำทุกอย่างบน Backend  แต่ก็ยังมีข้อจำกัดอยู่มาก โดยสองสิ่งที่สำคัญที่สุดสำหรับรูปแบบนี้คือ :

  • การออกแบบ Module ต้องทำให้ดี และมี Dependency ต่อกันน้อย
  • ทีมต้องมีวินัย รักษา Integration branch ให้เสถียรอยู่ตลอดเวลา

ที่เล่ามาทั้งหมดนี้ไม่ได้หมายความว่ารูปแบบนี้ไม่ดีนะครับ เพราะหากทำถูกต้อง

  • เราจะได้โค้ดที่ทำ Continuous integration  อยู่ตลอดเวลา
  • โอกาสเกิด Library conflict (เช่น module นึงใช้ jQuery 2.1.x ส่วนอีกอันใช้ 1.9.x)  จะน้อยมาก เพราะโค้ดทั้งหมดอยู่บน repository เดียวกัน ซึ่งแชร์ library ด้วยกัน

 

3. Separated frontend –Totally separated

วิธีแก้ข้อจำกัดของรูปแบบที่แล้ว คือการแยกทุก Module ขาดออกจากกัน นำมาซึ่งรูปแบบนี้

jemmy_frontend_arch_03

แบบนี้ Scale ได้แน่นอน แต่ละทีมมีอิสระอย่างเต็มที่ เพราะแต่ละ Module นั้นทำงานอยู่บน HTML page(s) ของตัวเอง อยากใช้ Library อันไหนก็จัดเต็มไปเลย ไม่ต้องห่วงว่าจะไปมี Conflict กับใคร

บางที่จะแยก Backend หรือแม้กระทั่ง Database ออกจากกันไปด้วยเลย แล้วทำเป็น Microservices แทนก็ได้ โดยให้ทีมเดียวกันดูแลตั้งแต่ Frontend Module ไปจนถึง Service (Backend API + Database)

หากเรามีโค้ดส่วนที่ทุกทีมต้องแชร์กัน ก็แยกออกมาเป็น Shared module(s) ซะ แล้วโหลดไปใช้งานด้วย Bower  หรือ Dependency management tool อื่นๆ

แต่ไม่มีรูปแบบไหนที่สมบูรณ์แบบครับ วิธีนี้ก็ยังมีข้อจำกัดของมันอยู่

  • การสื่อสารระหว่าง Frontend ข้าม Module นั้นลำบากกว่า
  • เวลาข้าม Module จะต้องโหลด HTML/JavaScript ใหม่  ผู้ใช้จะรู้สึกได้ว่ามีรอยต่อระหว่างกันอยู่
  • ต้องมีการแชร์ CSS และ Basic components (button, modal, navigation menu) ไม่อย่างนั้นหน้าตา UI ของแต่ละ module อาจแตกต่างกันมากจนทำให้ User experience เสีย

เรื่องของการสื่อสารข้าม Frontend module  เป็นข้อจำกัดที่แก้ไขไม่ได้ เนื่องจากทั้งสอง Module นั้นทำงานบนคนละเพจกัน  การส่งข้อมูลต้องส่งผ่าน URL parameters, query, POST หรือผ่านทาง ฺฺBackend แทน

ในเว็บแอพพลิเคชั่นบางประเภท โอกาสที่จะต้องใช้ข้อมูลจากสอง Module พร้อมกันอาจมีน้อย และไม่ต้องกังวลเรื่องหน้าตาของเว็บว่าจะต้องเหมือนกันเป๊ะ (ตัวอย่างเช่น Module หนึ่งเป็น Admin console, อีก Module เป็นของ Manager, และอีกส่วนของพนักงานคีย์ข้อมูล)  การแยก Module ออกจากกันโดยสิ้นเชิงแบบนี้จะเหมาะมาก

ในอีกมุมหนึ่ง แอพพลิเคชั่นบางประเภท ทุกๆ Module จะต้องทำงานแบบเป็นเนื้อเดียวกัน เช่น

  • Header, Footer, Menu ต้องเหมือนกันหมด
  • การแสดง page loading indicator ต้องเหมือนกันหมด อยู่ในตำแหน่งเดียวกัน
  • Language translation /locale setting ต้องสอดคล้องกัน
  • หากเกิด Timeout ระหว่างการทำงาน จะต้องแสดงข้อความแบบเดียวกัน หรือ redirect ไปยัง Login page เหมือนกัน

การแยกแต่ละ Module ขาดออกจากกันและให้อิสระอย่างเต็มที่ มักจะทำให้แต่ละทีมสร้างโค้ดเพื่อจัดการกรณีเหล่านี้กันเอง แม้จะมี Shared module ให้เรียกใช้ แต่ทีมก็อาจจะเลือกสร้างทุกอย่างเอง เพราะจะได้ไม่ต้องรอทีมอื่นในการทำงาน ทำให้เกิด Duplication ได้ง่าย

 

4. Separated frontend — Portal and widgets

เพื่อแก้ข้อจำกัดของรูปแบบที่แล้ว เราสามารถสร้าง Portal module ซึ่งทำหน้าที่สร้างส่วนหลักๆของเว็บที่ทุก Module ต้องมีเหมือนกัน (Header, menu, error handling) แล้วทำการโหลด Module ย่อยๆเข้ามาแสดงผลตรงกลาง

บางที่อาจจะเรียกเจ้า Module ย่อยๆเหล่านี้ว่า Widget  โดยที่แต่ละ Widget สามารถถูกพัฒนาโดยใครก็ได้ (อาจจะเป็น External party)  ตราบเท่าที่ Widget เหล่านี้อยู่ในรูปแบบที่ Central module สามารถโหลดเข้ามาได้

jemmy_frontend_arch_04

รูปแบบนี้จะช่วยลดความซ้ำซ้อนของโค้ดระหว่าง Module ได้ โดยให้ Portal จัดการส่วนที่ซ้ำกัน (เช่น menu, header, loading indicator, notification box)

jemmy_frontend_arch_04-2

แต่รูปแบบนี้ก็มีความยากลำบากของมันเองอยู่

ความยากลำบากนี้คือการโหลด Widget อื่นๆเข้ามาแสดงนั่นเอง โดยตัว Portal ต้องมีข้อตกลงกับ Widget ว่าจะเรียกใช้งานโค้ดของอีกฝั่งอย่างไร ซึ่งแต่ละที่ก็อาจจะมีข้อตกลงไม่เหมือนกัน เช่น

4.1 ใช้ <iframe> ในการดึง Standalone HTML page มาแสดงผล

วิธีแรกคือการให้ Widget อื่นๆสร้าง Standalone HTML page ขึ้นมาให้ Central Module ดึงไปแสดงผลใน <iframe>

คำว่า Standalone นี้หมายความว่า HTML page นั้นๆจะต้องมี CSS, JavaScript libraries เป็นของตัวเอง

วิธีนี้จะเริ่มมีปัญหา หากมีหน้าใดหน้าหนึ่งที่ต้องการแสดง HTML จากหลายๆ Module มาแสดงผลร่วมกัน

  1. ช้ามากๆ เพราะทุก <iframe> ต้องโหลด library/css มา render ใหม่หมด ถ้าใช้ Angular ก็ต้องโหลดและ bootstrap ใหม่ทุกๆ <iframe>
    บางคนอาจแย้งว่า cache หรือใช้ CDN เอาก็ได้  แต่เวลา render ใน browser ยังไงก็แก้ไขไม่ได้
  2. หากต้องมีการส่งข้อมูลระหว่าง Module (ข้าม <iframe>) จะทำได้ยาก

4.2 โหลด HTML มาแทรกลงใน <div>  และโหลด JavaScript แล้ว bootstrap แยกต่างหาก

รูปแบบนี้มีวิธี Implement ที่ค่อนข้างหลากหลาย

ที่ผมเคยเห็นมีตั้งแต่โหลด HTML ด้วย xhr แล้วเอามา append ใน div เอง  แล้วดึง JavaScript มาด้วย requireJS

แต่วิธีที่ผมเห็นแล้วเวิร์คมาก คือการใช้ AngularJS + ui-router ในการโหลด  โดยโค้ดที่ใช้ในการใส่ ui-router กับ Controller จะแยกออกจากไปอยู่อีก repository นึง

 

สรุปแล้วรูปไหนดีล่ะ?

ผมขอตอบด้วยคำตอบคลาสสิคละกันครับว่า “It depends”

เอาเข้าจริง ไม่มีเว็บแอพพลิเคชั่นไหนไหนที่ออกแบบมาแล้วตรงตามที่ผมบอกเป๊ะๆหรอกครับ ส่วนใหญ่จะผสมๆกัน

ตัวอย่างเช่น แบ่งในระดับแรกเป็นแบบ Totally separated  5 modules ใหญ่ๆ  แล้วให้อิสระในการออกแบบ Module ย่อยข้างใน โดย บาง Modules ก็เลือกที่จะทำ Server-generated frontend  และบาง Modules ก็มาแนว Portal and widgets

หรืออีกกรณี ทีมทำเป็น Totally separated ในระดับของ Git repository แต่ใช้ Integration server ในการนำโค้ดของทุกๆ Module มา Build รวมกันและทำ Integration test แบบ Monolith

ดังนั้น หากคุณต้องอยู่ในสถานการณ์ที่ต้องออกแบบจริง  แทนที่จะเลือกว่าใช้รูปแบบไหน ให้มองย้อนกลับไปยังปัจจัยเหล่านี้ แล้วสร้างรูปแบบที่เหมาะสมกับแอพพลิเคชั่นของคุณเอง

  1. User experience – เราต้องการให้ทุก Module เชื่อมต่อแบบเป็นเนื้อเดียวกันหมดหรือเปล่า ทำอย่างไรให้ Menu, Header และ UI ต่างๆ consistent โดยให้มี overhead น้อยที่สุด
  2. Scalability – เว็บแอพพลิเคชั่นตัวนี้จะใหญ่แค่ไหนในอนาคต สถาปัตยกรรมจะสามารถรองรับทีมจำนวนมากได้หรือเปล่า โครงสร้างของทีมในองค์กรเป็นอย่างไร เหมาะสมกับการแบ่ง module ออกจากกันหรือไม่
  3. Ease of continuous integration – จะรักษา Integration branch ให้เสถียรอยู่ตลอดเวลาได้หรือไม่ หากทำได้ยาก อาจจะต้องหั่น Frontend ออกเป็นแบบ Totally separated แทน
  4. Productivity – การแยก Frontend ออกจะทำให้พัฒนาได้เร็วขึ้น แต่คนในทีมไม่มีประสบการณ์ในการแยก Frontend ออกมา อาจจะทำให้ Productivity ตกลงมากในช่วงแรก เราจะยอมรับได้หรือไม่
  5. Communication between module – แต่ละ Module ต้องส่งข้อมูลหากันในระดับ Frontend มากแค่ไหน หากแยก Module ออกจากกันโดยสิ้นเชิง (Totally separated) จะส่งข้อมูลแบบไหนดี
  6. Mobile support – หากต้องการรองรับ Mobile application การใช้ Server-generated อาจไม่เหมาะนัก เพราะต้องสร้าง API ให้ Mobile ซ้ำอีกที หากมีการเปลี่ยนแปลงก็จะกระเทือนโค้ดทั้งสองส่วน
  7. Performance – การแยก Frontend ออกมาจะทำให้ Frontend code ทั้งหมดเป็น static resource ทำให้ latency น้อยกว่า

แน่นอนครับ ไม่รูปแบบใดที่ออกมาแล้วดีครบ 100% หมด  ดังนั้น ผู้ออกแบบต้องคิดให้ละเอียด ว่าเรื่องใดที่สำคัญมาก สำคัญน้อย แล้วเลือก Trade-off ให้ดี

 

Chadchapol V.

ผู้เลือกผิดมาเยอะแล้ว



About Chadchapol V.

A software engineer who strayed to study management - I am passionate about software design, code quality, technical leadership, and Siberian Husky.

Check Also

[Video Webinar] Cyber Threat Alliance – Making the Impossible Possible and WAN Edge Transformation

สำหรับผู้ที่ไม่ได้เข้าฟังการบรรยาย Webinar เรื่อง “Cyber Threat Alliance – Making the Impossible Possible and WAN Edge Transformation” …

[Video Webinar] Detecting Ransomware with Veeam by CSL

สำหรับผู้ที่ไม่ได้เข้าฟังการบรรยาย CSL Webinar เรื่อง “Detecting Ransomware with Veeam” เพื่อเรียนรู้การปกป้องข้อมูลจาก Ransomware และการปรับปรุงแผน Business Continuity ให้รองรับกับปริมาณข้อมูลที่เพิ่มมากขึ้น และสอดคล้องกับ …