ปรับ React Boilerplate ให้เป็น Micro Frontend

Putt Potsawee
LifeatRentSpree
Published in
6 min readJun 29, 2021

ในบทความก่อนหน้า เราได้เล่าถึงความน่าสนใจของ 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 เลย

นั่นก็คือ เราจะทำเว็บแอพ ง่ายๆ ที่สุ่มภาพของน้องหมาและน้องแมว

ขอบคุณโจทย์จาก Rumesh Hapuarachchi

โดยที่ในเว็บแอพนี้แบ่งส่วนย่อยๆ เป็นสามส่วน คือ

  1. Container App เป็นแอพหลักที่รวมแอพทั้งหมดไว้
  2. Dog App เป็นแอพที่สุ่มรูปน้องหมา
  3. Cat App เป็นแอพที่สุ่มรูปน้องแมว

เมื่อกดปุ่ม “New Cat” และ “New Doggo” จะได้สุ่มรูปน้องขึ้นมาใหม่

ส่วน Greet Me จะเอาข้อความใน Text Box ไปใส่หน้าน้องแมว และแสดงผลในอีก page นึง

ขอบคุณโจทย์จาก Rumesh Hapuarachchi

ขั้นที่ 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 ชื่อไฟล์อะไร

ตัวอย่างของ asset-manifest.json

ขั้นตอนต่อไปนี้ เราจะทำการแปลง 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 ออกไป

Comment ทั้งก้อนนี้ออกไปก่อนเลย

จากนั้นทำการปรับให้ตัวโครงมันเหมาะสำกับการถูกเรียกโดย 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 แอพย่อย

ขอบคุณโค้ดจาก Cam Jackson และ Rumesh Hapuarachchi

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

--

--

Putt Potsawee
LifeatRentSpree

Lead Developer at RentSpree. Passionate programmer who specialized in Backend and Infrastructure.