ปรับ React Boilerplate ให้เป็น Micro Frontend
ในบทความก่อนหน้า เราได้เล่าถึงความน่าสนใจของ Micro Frontend ลักษณะข้อดีของมัน และข้อควรวิเคราะห์สำหรับทีมที่อยากเลือกเอาไปใช้
ในบทความนี้ เราจะนำเอา Micro Frontend มาลองเซ็ตอัพจริงๆ กันให้ทุกคนเห็นภาพ โดยเนื้อหาในบทความนี้เราเอาฐานแนวคิดมาจากบทความหลักของ Martin Fowler
และตัวอย่างโจทย์จาก Rumesh Hapuarachchi
โดยเราได้เอาวิธีการทำง่ายๆ มาทำให้ยากขึ้น ด้วยการทำลงไปบนโครงที่เป็น React Boilerplate
โดย React Boilerplate เป็นโครงสำหรับ React ที่เรียกว่าเป็น Production Ready คือมีการทุกอย่างเตรียมไว้ให้โครงพร้อมสำหรับเอาขึ้น Production ได้ โดยที่เราไม่ต้องเซ็ตอัพเพิ่มจากโครงง่ายๆ อย่าง Create React App
ตัวโครงมาให้พร้อมกับ React, React Router, Redux, Redux-Saga, Router หรือแม้กระทั่งไลบรารี่อย่าง Reselect, Immer และ Styled Component พร้อมด้วย Test Environment และ Lint ต่างๆ ไปจนถึง folder structure ที่จัดมาให้ นอกจากนี้ยังมี Script generator ที่เราสามารถเอาไว้ใช้สร้าง container และ component ได้
เรียกได้ว่าครบเครื่องสุดๆ
ข้อดีของมันคือ
เซ็ตอัพง่าย เอาไว้ใช้ใน Production ได้เลย มันมี config รองรับการจะตั้งค่าหลายๆ อย่างไว้ให้แล้ว เราแค่เซ็ตอัพเพิ่มเข้าไปนิดหน่อย ยกตัวอย่างเช่นการทำ Code Splitting, Lazy Loading ทำได้ง่ายมากๆ ที่ต้องทำเหมือนเราแค่ตั้ง Component ของเราให้ใช้งานฟังก์ชั่นนั้น ไม่ต้องไปปรับ Webpack config ให้เสียเวลา
ส่วนข้อเสีย
อาจจะ Overkill ไปสำหรับแอพที่มีขนาดเล็ก และถ้าต้องการเอา lib บางตัวออกจาก boilerplate เช่น Styled Component อาจจะต้องศึกษาโครงและการทำงานก่อน จึงจะเอาออกได้
เอาหละ มาเริ่มกันเลย
โจทย์
ตัวอย่างโจทย์ข้อนี้เราอ้างอิงมาจากของ Rumesh Hapuarachchi เลย
นั่นก็คือ เราจะทำเว็บแอพ ง่ายๆ ที่สุ่มภาพของน้องหมาและน้องแมว
โดยที่ในเว็บแอพนี้แบ่งส่วนย่อยๆ เป็นสามส่วน คือ
- Container App เป็นแอพหลักที่รวมแอพทั้งหมดไว้
- Dog App เป็นแอพที่สุ่มรูปน้องหมา
- Cat App เป็นแอพที่สุ่มรูปน้องแมว
เมื่อกดปุ่ม “New Cat” และ “New Doggo” จะได้สุ่มรูปน้องขึ้นมาใหม่
ส่วน Greet Me จะเอาข้อความใน Text Box ไปใส่หน้าน้องแมว และแสดงผลในอีก page นึง
ขั้นที่ 1: ขึ้นโครงแอพ
ขั้นตอนแรก เราจะขึ้นโครงแอพด้วย React Boilerplate เป็นแอพสามแอพด้วยกัน มี Dog จะรับผิดชอบส่วนน้องหมาทั้งหมด Cat จะรับผิดชอบส่วนน้องแมว และสุดท้าย Container จะรวมสองแอพเข้าด้วยกันเป็นหน้าเดียว
โดยเราจะขึ้นโครงด้วย React Boilerplate และจากนั้น จะทำการลง Lib ที่จำเป็น อัพเกรด version ของ react-redux
ให้เป็น 7.1.0
เพื่อให้รองรับการเซ็ตอัพที่เราต้องใช้
Script นี้ใช้วิธีการ copy folder แต่ถ้าใครจะ clone ไปทีละอันก็จัดการได้เลย ตามถนัด อย่าลืมอัพเกรด version ด้วย npm i react-redux@7.1.0
และลง cors
เพิ่มในทุกๆ แอพด้วย
ขั้นที่ 2: สร้าง Dog App แสดงรูปน้องหมากันหน่อย
ขั้นตอนนี้เราจะมาทำให้ dog app ใช้งานได้ก่อน เริ่มแรกเลย สร้าง Container ใหม่ขึ้นมาก่อน ด้วยคำสั่ง npm run generate container
- ไม่ต้องใช้ Loadable
- ตั้งชื่อ Container ว่า
Dog
ไปเลย - ตั้งให้ Main Route
/
ชี้มาที่ Container Dog ของเรา - ลบ Container อื่นๆ ใน Boilerplate ออกไป
ถ้าเราลองรัน เราจะได้แอพน้องหมาเดี่ยวๆ ที่ทำงานได้แล้ว
ขั้นที่ 2: สร้าง Cat App สำหรับน้องเหมียว
ในแอพน้องเหมียวจะคล้ายๆ Dog App แต่ต่างกันนิดหน่อยตรงที่ แอพนี้จะมีอีก Component ที่แสดงรูป Greeting Cat ด้วย เพราะฉะนั้น เราจะมี Component สองอันที่รองรับ feature เรานั่นก็คือ RandomCat
และ GreetingCat
Random Cat
สำหรับ Random Cat นี่จะใช้วิธีคล้ายๆ Dog นั่นก็คือการสร้าง container ใหม่ด้วย npm run generate
จากนั้นทำขั้นตอนเดียวกันได้เลย
- ลบ Loadable ออกไป
- ตั้งชื่อ Container ว่า
RandomCat
- ลบ Container อื่นๆ ที่มาจาก Boilerplate ออกไป
Greeting Cat
ต่อไป เพื่อให้เกิดความง่ายขึ้นหน่อย สำหรับ Greeting Cat เราจะขึ้นมันด้วย Component ง่ายๆ แบบไม่มี Redux จะได้ลดระยะเวลาในการสาธิต
สร้างไฟล์ชื่อ .app/containers/GreetingCat/index.js
จากนั้น สุดท้ายทำการแก้ Route ใน ./app/containers/App/index.js
โดยให้
- Root (
/
) ชี้มาที่RandomCat
/cat/:greeting
ไปที่GreetingCat
ขั้นที่ 4: Ground Up Container App
ตัว Container แอพของเราจะต้องเป็นตัวเชื่อมระหว่างสองแอพย่อย โดย Integration Layout ของเราจะต้องหน้าตาเป็นแบบนี้
เริ่มที่ Container โปรเจคที่เราขึ้นโครงไว้ เราลบ component ที่แถมมากับ boilerplate ให้หมดก่อน
จากนั้น เราเริ่มเขียน Component ของเรา
อันแรก Header Component แค่แสดง banner ด้านบน ง่ายๆ จากนั้นเป็น Home Component โดยในตอนนี้เราจะเอารูปตัวอย่างมาแปะให้เห็นรูปร่างของ Component ชิ้นนี้กันก่อน
ส่วน Global Style ก๊อปโค้ดไปใช้ได้เลย
ลองตรวจสอบดูว่า container app ของเรารันได้และทุกอย่างอยู่ถูกที่ถูกทางหรือไม่
ขั้นที่ 5: แปลงแอพย่อย ให้กลายเป็น Micro Frontend
ขั้นตอนนี้ถือเป็นขั้นตอนสำคัญในการรวมแอพของเราเลยก็ว่าได้ เพราะเราจะมาปรับแอพ Dog และ Cat ให้รองรับ Micro Frontend ให้ได้
โดยตอนนี้เราจะเห็นว่าเรามีแอพสามอันแยกขาดออกจากกันอยู่ เราต้องเอามันมารวมกัน แต่รันเป็นคนละ server โดยในเครื่องเราก็กำหนด port ให้แอพเราได้ดังนี้
เมื่อเอาไปขึ้น Production จริงตัว port อาจจะกลายเป็น URL คนละอันกัน ดังนั้นเราตั้งให้ Host ของ Dog และ Cat ที่ container เก็บค่าไว้เป็นตัวแปนไว้ก่อนน่าจะดีที่สุด
ใน container app ในไฟล์ ./app/containers/App/index.js
ให้เราตั้งค่าตัวแปรดังนี้
const DOG_HOST = "http://localhost:3001"
const CAT_HOST = "http://localhost:3002"
ซึ่งในตอนที่จะทำให้เป็น Production Environment จริงๆ ตรงส่วนนี้อาจจะถูกเปลี่ยนเป็น Service discovery หรือตัว config ที่ซับซ้อนกว่านี้ได้
วิธีในการจะรวมแอพ แบบ Micro Frontend ก็จะใช้วิธีตามที่ Cam Jackson ได้ลองทำไว้ในบทความเรื่อง Micro Frontend บนเว็บของ Martin Fowler เลย
ซึ่งวิธีนี้ก็คือการให้ Container แอพ ดึง Bundle จากแอพย่อยทั้ง Cat และ Dog App แล้วเอาฟังก์ชั่นนั้นมารันใน Container แอพ (รายละเอียดในขั้นที่ 6)
โดยวิธีการที่ทำให้ดึง bundle มาได้ ก็คือการใช้ asset-manifest.json
เป็นตัวในการบอกว่า src
js ชื่อไฟล์อะไร
ขั้นตอนต่อไปนี้ เราจะทำการแปลง Cat และ Dog ให้สามารถ serve ไฟล์ asset-manifest.json
ได้ และจากนั้นเราจะทำให้ Cat และ Dog ถูก render ผ่าน function bundle ได้
ซึ่งวิธีการทำ ให้เราเริ่มด้วยการลง Plugin manifest ของ webpack ลงใน Cat และ Dog project ของเราได้เลย
npm i react-dev-utils@11.0.4 webpack-manifest-plugin@2.2.0
จากนั้นแก้ไฟล์ helper ของ Boilerplate ใน /.internals/paths.js
จากนั้นก็ตั้งค่า Plugin ใน /.internals/webpack/webpack.base.babel.js
จากนั้น เราจะต้อง disable chunk ออกไปก่อน ซึ่งเมื่อเป็นแอพใน production จริงๆ คงต้องมาลองเซ็ตอัพส่วนนี้ให้มันได้ performance ที่ดีที่สุด
ในไฟล์ ./internals/webpack/webpack.prod.babel.js
และ ./internals/webpack/webpack.dev.babel.js
comment code ส่วนที่เป็น splitChunks
ออกไป
จากนั้นทำการปรับให้ตัวโครงมันเหมาะสำกับการถูกเรียกโดย Container แอพ นั่นก็คือ เพิ่ม cors
และ เปลี่ยน default port
โดยไปเปลี่ยนที่ไฟล์ ./server/index.js
และ ./server/port.js
จากนั้น ทำการปรับให้ App รับ History object ได้ด้วย ตัว history object นี้เองเป็นสิ่งที่จะเชื่อมและเป็นตัวกลางในการ communicate ระหว่าง Micro Frontend App Object ตัวนี้จะถูกสร้างใน Router ของ Container App แต่จะถูกส่งผ่านเข้าไปยังทุก Micro Frontend ที่ต่อเชื่อมกับ container
ในขั้นตอนนี้ เราปรับ ./app/containers/App/index.js
ของ Cat และ Dog
จากนั้น เราก็มาปรับ Entry point ให้ Cat กับ Dog สามารถถูก render ได้ใน container โดย concept ของมันก็คือ เมื่อมีการโหลดแอพของ cat และ dog ปกติ react จะทำการ hydrate html และใส่ลงใน body ที่เกิดการ render ขึ้น (ฟังก์ชั่น ReactDOM.render
ที่เราจะเห็นได้ในทุกไฟล์ entry ของ react นั่นเอง
ซึ่งถ้าจะให้พูดถึงโครง React boilerplat ไฟล์ ./app/app.js
นี้เองเป็นเสมือน entry point มองง่ายๆก็คือ เวลา npm start
หรือ bundle ถูกโหลด มันจะ execute ไฟล์นี้ ในแอพปกติ เราแค่ต้องการให้ไฟล์นี้ execute ReactDOM.render
เพื่อให้ UI ของเราประกฎขึ้นมาบน browser นั่นเอง
แต่ในกรณีของ Micro Frontend อย่างแรกเราต้องปรับ เงื่อนไขการ render ก่อน โดยต้องปรับ function ให้กลายเป็น function ที่รับ id ของ html element ที่จะให้ react ไป render และรับ history เข้ามาเพื่อส่งต่อให้กับ react-router
เป็น communication ระหว่างแอพด้วย
const render = (containerId, history) => {
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById(containerId);
ReactDOM.render(
<Provider store={store}>
<App history={history} />
</Provider>,
MOUNT_NODE,
);
};
ซึ่ง function render นี้ต้องรับ containerId
และนำไปเป็น MOUNT_NODE
ในการ render ใจขณะที่อีก parameter นึงที่ต้องรับก็คือ history
ซึ่งต้องทำการรับและส่งต่อไปให้ Component App
จะเห็นว่า function นี้เองเป็น function ที่เราจะนำไปให้ container app เรียกใช้เมื่อมันต้องการ render Micro Frontend แอพย่อยนี้
วิธีการที่เราจะนำไปให้ container app เรียกใช้ นั่นก็คือเราจะตกลงให้ แอพย่อยมี AppName
ซึ่งเป็นชื่อเฉพาะของแอพนั้นๆ และเราจะ assign function render นี้ขึ้นไปใน global variable ในตัวแปร window
อย่างเช่นแอพ Dog เราจะทำให้ function render กลายเป็น
renderDogs
unmountDogs
และสุดท้าย ถ้าเราไม่เจอ element ย่อยที่จะ render เราจะเดาว่า Entrypoint ถูกเรียกใน stand alone mode แปลว่ามันเป็น development ที่เอาไว้รันแอพเดี่ยวๆ
ไฟล์ที่ได้จะกลายเป็นแบบนี้
เมื่อเสร็จขั้นตอนนี้ ถือว่าเป็นอันเสร็จสิ้นสำหรับการแปลงแอพย่อย ให้เป็น Micro Frontend แล้ว
ลอง npm start
ในแอพย่อย เราต้องสามารถรันมันแบบ stand alone ได้
อย่าลืมทำขั้นตอนนี้ทั้ง Dogs และ Cat เลย
ขั้นที่ 6: ทำ Container แอพให้เรียก Micro Frontend App ได้
เมื่อ cat และ dog พร้อมแล้วเราจะทำการปรับ container ให้เรียกทั้งสองแอพ มา render ใน Container แอพกัน
อย่างแรก เราจะสร้าง MicroFrontend.js ซึ่งเป็น Component ที่จะจัดการทุกอย่างในการ render Micro Frontend แอพย่อย
Component นี้ทำอะไร สิ่งที่มันทำก็คือ ตอนที่ถูก mount มันจะประกาศ function อันนึงนั่นก็คือ renderMicroFrontend
ซึ่งฟังก์ชั่นตัวนี้ไม่ได้ทำอะไรเลยแค่เรียก global function ที่อยู่ใน window ซึ่ง name
นั้นก็จะเป็น AppName
ของแอพย่อยที่เราตั้งไว้ในขั้นตอนก่อนหน้า ในตัวอย่างนี้ ถ้าเป็นแอพ Dog มันก็จะเรียก window[renderDogs]
หรือเทียบเท่าการรัน renderDogs()
โดยส่ง containerId
กับ history
เข้าไปนั่นเอง
แต่ตัว global renderDogs
มาได้ยังไง? ก็ต้องดูตรง fetch
ซึ่งตัวนี้จะทำการโหลดไฟล์ asset-manifest.json
ลงมาแล้วสร้าง script
tag ขึ้นมาในหน้านี้ก่อนจะโหลด main.js
เข้าไป การประกาศแบบนี้มันก็คือสั่งให้ browser รัน entry point ของแอพ Dog ของเรานั่นเอง
ซึ่งถ้าย้อนกลับไปขั้นที่ 5 entry point ในแอพ Dog ของเรานั้นจะ assign renderDogs
ไปที่ global variables
การ assign render ไปที่ global variable และ MicroFrontend
เรียกจาก window[renderDogs]
ได้นั้น เป็นเหมือนข้อตกลงร่วมกันของแต่ละแอพ ว่าจะมีการ integrate กันแบบนี้ ซึ่งมันก็คือ Contract ในการ Integrate Micro Frontend ที่ทุกแอพที่จะมารวมกัน ต้องทำตาม
จากนั้น ขั้นตอนต่อไป เราก็จะปรับ container app ที่เราทำไว้ในขั้นที่ 4 ให้เรียก micro frontend จริงๆ ที่ ./app/containers/App/index.js
ซึ่งจาก โค้ดด้านบน จะมีการเรียก Component MicroFrontend
ที่รับ name
และ history
พร้อมกับ Host ของ Micro Frontend แอพนั้น
ส่วนใน App
นั้นก็ต้องมีการตั้งค่า Route ให้ตรงตาม Requirement ของแอพเราด้วย
เมื่อ start container app เราจะเห็น Dogs และ Cats ถูก render ขึ้นมาในหน้าเดียวกัน ถือว่าเป็น Micro Frontend แอพที่ใช้งานได้เรียบร้อย
สรุป
- เราสามาถปรับ React Boilerplate ให้กลายเป็น Micro Frontend แอพได้ไม่ยากนัก
- การ Integrate Micro Frontend ด้วยวิธีนี้ทำให้ได้ build dependency ที่แยกขาดจากกันชัดเจน Micro Frontend แต่ละแอพสามารถอยู่แยก project และใช้ CI/CD Pipeline แยกกันได้ชัดเจน
- เรื่องของ Contract ยังเป็นสิ่งสำคัญ วิธีการทำแบบนี้ไม่ใช่เพียงแบบเดียวที่เราสามารถทำ Micro Frontend ได้ ที่สำคัญคือ Contract ในการ Integrate ต้องมีการตกลงไว้ และบริหารจัดการง่าย
- Integration และ communication ในแอพนี้เราใช้ Route เป็น contract ในการส่งข้อมูลหากันระหว่าง Micro Frontend แอพ เราใช้วิธีส่ง History จาก Container App เพื่อให้แต่ละ Micro Frontend ใช้ History object เดียวกัน ซึ่งทำให้ Micro Frontend แต่ละอันสามารถเปลี่ยน URL ของเพจได้ผ่าน History และรับการเปลี่ยนแปลงของ URL ที่เกิดขึ้นผ่าน History เช่นกัน
- ถ้าอยากจะทำให้แอพเป็น Production Ready จริงๆ อาจจะต้องไปดูเรื่อง Performance ที่เป็น Code Splitting และข้อเสียอื่นๆ ของ Micro Frontend