微前端的溝通 - 發布/訂閱 vs 傳遞自訂事件 (Cross Micro Frontends Communication: Pub/Sub vs Custom Events)

pizza architecture vs pasta architecture

本文主要探討如何在微前端的跨應用程式間溝通,尤其針對兩種解法「發布/訂閱 (pub/sub)」與「傳遞自訂事件 (custom events)」說明和比較。

微前端與跨應用程式間的溝通

微前端 (micro frontends) 是一種前端架構,它將一個大型的 app 切分成多個小型、獨立的 app,這樣就能切分成不同單位而能可以獨立實作、運作和部署,適合由不同的團隊進行開發和維護。如果還是不了解「微前端」是什麼,這裡有些不錯的文章可以參考看看。

考量使用微前端架構的因素大多是由於 (1) 專案逐漸龐大,而解耦可維持高效開發;(2) 功能間的相依度低或技術線獨立,不需要綁在一起等。因此,若想使用微前端作為架構方式,儘管應用程式間溝通不是很頻繁,但還是需要些許元件、函式、資料和狀態的共享與傳遞,於是我們就需要一些機制來做跨應用程式間的溝通。

微前端在處理共享程式碼方面,就 function、component 和共用邏輯來說,有很多種共用的方法,像是做成 library 然後發佈成 npm package 或 git submodule 等,個別 app 要用的時候再自行 import 即可。若對共用程式碼相關有興趣,可以參考這篇文章

微前端在處理共享狀態方面,針對網站上各種不同的功能切分成多個微前端,而這些微前端可能是位於不同 domain 或由不同 framework 實作而成。這些微前端彼此間有些許關聯和連動性,像是必須共享資料或狀態,稍等會來看有哪些溝通方式;但若微前端彼此間需頻繁溝通,就考慮合併它們吧!

微前端的溝通方式有哪些

微前端的溝通方式大致上可分為 5 種:(1) web workers、(2) props + callbacks、(3) custom events、(4) pub/sub library 與 (5) 丟給後端處理。

在考量上手與設定的便利性後,選用兩種方式「發布/訂閱」(pub/sub, windowed-observable) 和「傳遞自訂事件」(custom events) 來實作和比較。以下範例是用 monorepo 的架構與 NX 來設定的,建立一個 container app 與 microfrontend app 來做溝通。

container app 與 microfrontend app

Web Workers

web workers 是指在 container app 建立 web worker 並存在 window 裡面給其他 app 共享,其他個別 app 監聽 window 的變化,以及更新 window 的內容。優點是希望能藉由 worker thead 加速效能,但要注意 main thread 與 worker thead 間溝通的複雜度與資料傳輸的成本。

Props + callbacks

props + callbacks 是指在 container app 建立 state 存放共享狀態來讓個別 app 存取。優點是簡單直覺、易於實作,但這解法很可能在整合不同框架時會遇到較大的風險。

發布/訂閱 (Pub/Sub, windowed-observable)

pub/sub library (windowed-observable) 是指在 container app 將狀態存到 window 並與其他 app 共享,其他個別 app 監聽 window 的變化,以及更新 window 的內容,類似 props + callbacks 的概念,只是細節由 library 周到地處理好了。

簡單範例如下。

在 container app 最一開始會丟出 Hello World!,點擊 Say Hi 則丟出 Hi!

import M1 from '@/components/M1';

const observable = new Observable('messages');
const observer = (item: any) => console.log(item);
observable.subscribe(observer)
observable.publish('Hello World!');

export default function App() {
  const [messages, setMessages] = useState([]);
  const handleNewMessage = (newMessage: any) => {

    console.log('messages', messages)
    setMessages((currentMessages) => currentMessages.concat(newMessage));
  };

  useEffect(() => {
    observable.subscribe(handleNewMessage);

    return () => {
      observable.unsubscribe(handleNewMessage)
    }
  }, [handleNewMessage]);

  return
    <>
      <M1 />
       <button onClick={() => { handleNewMessage('Hi!') }}>Say Hi</button>;
    </>
}

接著在 microfrontend app 點按鈕 Reply 則丟出 Say hi again!

import { Observable } from 'windowed-observable';
const observable = new Observable('messages');

export default M1() {
  return (<button onClick={() => { observable.publish('Say hi again!') }}>Reply!</button>);
}

選用 pub-sub library 的好處是容易上手、易於客製化、不綁死特定框架框架、API 足夠使用、細節都有被詳細處理到;而缺點是暴露在 window 易被修改、非原生 API 而有 (windowed-observable) package dependency 風險是需要考量的。

傳遞自訂事件 (Custom Events)

custom events 是指 app 彼此經由利用 eventListeners 監聽共享狀態的變化,經由 CustomEvent 更新狀態。

簡單範例如下。

在 container app 最一開始會丟出 Hello World!,點擊 Say Hi 則丟出 Hi!

import M1 from '@/components/M1';

export default function App() {
  const [messages, setMessages] = useState(['Hello World!']);

  const handleNewMessage = (event: any) => {
    console.log('messages', messages);

    if(event?.detail) {
      setMessages((currentMessages) => currentMessages.concat(event.detail));
    }
  };

  useEffect(() => {
    window.addEventListener('message', handleNewMessage);

    return () => {
      window.removeEventListener('message', handleNewMessage)
    }
  }, [handleNewMessage]);

  return (
    <>
      <M1 />
      <button onClick={() => { handleNewMessage('Hi!') }}>Say Hi</button>
    </>
  );
}

export default App;

接著在 microfrontend app 點按鈕 Reply 則丟出 Say hi again!

export default function M1() {
  const reply = (msg: any) => {
    const customEvent = new CustomEvent('message', { detail: msg });
    window.dispatchEvent(customEvent);
  }

  return (
    <>
      <button onClick={() => { reply('Say hi again!') }}>Reply</button>
    </>
  );
}

選用 custom events 的好處同樣有容易上手、易於客製化、不綁死特定框架框架、API 足夠使用,而且是原生 API,就沒有 package dependency 風險,只是有些細節需要自己來處理。

發布/訂閱與傳遞自訂事件兩種解法概念上大致相同,都是基於共享狀態的概念來實作,差異只在於 custom events 是用原生的 API,而 windowed-observable 則幫我們將細節處理好。

丟給後端處理

丟給後端處理是指 app 間要共享的資料,一律丟給後端處理,前端只要打 API 存取就好,推薦使用 GraphQL。但這解法的 UX 體驗也許沒這麼優。

總結

推薦閱讀


Micro Frontends front-end architecture 微前端 Monorepo NX Memori