- vừa được xem lúc

Xử lý một số vấn đề hóc búa với Routing trong kiến trúc Microfrontend

0 0 2

Người đăng: Mai Trung Đức

Theo Viblo Asia

Cập nhật gần nhất 29/12/2024

Hello các bạn lại là mình đây 👋👋

Tiếp tục với series Chập chững làm quen với Microfrontend, hôm nay ta sẽ xử lý bài toán tiếp theo mà khả năng rất cao ta sẽ gặp phải khi làm việc với kiến trúc MFE đó là: xử lý Routing

Mặc áo phao tay cầm chèo và lên thuyền với mình nhé 🛥️🛥️

Vấn đề hiện tại

Từ đầu series tới giờ ta cũng điểm qua 1 số vấn đề nổi cộm với MFE, nhưng nếu các bạn để ý thì ở các ví dụ mình mới chỉ làm tới phần Widget View cho MFE, tức là các MFE được hiển thị như 1 Widget, share chung 1 màn hình, có thể kéo kéo xung quanh, resize các thứ:

Tất cả chúng mới chỉ nằm ở 1 route là /main - MainComponent

Nhưng thực tế, ta thường có yêu cầu là app cần phải có routing:

  • Widget phải chạy như kiểu nó là 1 standalone app luôn, kiểu Single page App (SPA) mà ta vẫn dùng, cho user cảm giác về navigation giữa các màn hình các kiểu
  • hoặc ta đã có sẵn SPA app rồi từ trước rồi, ví dụ React app, có react-router đang chạy ngon và giờ ta muốn đưa nó vào kiến trúc MFE

Giờ ta phải làm gì để support routing?? và có những vấn đề nào ta có thể gặp phải?? 🧐🧐

Bởi vì routing system giữa các framework/library có thể sẽ rất khác nhau, có thể đụng độ route hoặc thậm chí không tương thích.

Ở bài này ta sẽ cùng điểm qua các vấn đề đó và một số hướng giải quyết nhé. Zô phần chính thoai 🚀🚀

Setup

Đầu tiên các bạn clone source code của mình về ở đây: https://github.com/maitrungduc1410/viblo-mfe-routing

Sau khi clone thì ta chạy:

npm install npm start

Và nó sẽ tự động mở trình duyệt ở địa chỉ http://localhost:4200 khi app ready, mình tinh gọn mấy bước setup hết rồi cho nhàn 😎😎

Mở lên ta vẫn thấy có mấy MFE như các bài trước, kéo kéo xung quanh, nhưng để ý phần header ta có thêm button để switch giữa Widget ViewApp View

Khi bấm vào App View, mặc định ta có màn hình trắng, click vào Dropdown show list các MFE có thể được render ở App View

Click vào từng MFE và ta thấy là chúng có routing được setup sẵn luôn 🤩🤩🤩🤩🤩:

Tổng quan

Bởi vì setup Routing cũng khá lằng nhằng nên mình đã làm luôn cho các bạn, ở bài này ta sẽ chủ yếu là hiểu các setup trong bài, sau đó là thảo luận các vấn đề xung quanh nha, không phải code thêm mấy nữa chứ ngồi mà code implement phần routing này thì cũng khá dài đó 😝😝

Thay đổi về loader.ts

Cấu trúc folder của bài này thì cũng xêm xêm như các bài trước:

Ở đó ta có App Shell và 3 MFE là Angular, React, Vue

Nhưng có 1 chút đặc biệt là ở loader.ts của mỗi MFE thì thay vì ta chỉ export cấu hình của 1 "Widget" thì giờ đây đã được update thành nhiều widget:

import App from "./App.vue";
import AppView from "./components/appview/AppView.vue";
import router from "./router"; const widget = { id: "VUE_WIDGET", // must be globally unique framework: "vue", component: App, title: "My Vue Widget",
}; const app = { id: "VUE_APP", // must be globally unique framework: "vue", component: AppView, routeName: "vue-app", appView: true, plugins: [router], title: "Vue App",
}; export { widget, app };

Mỗi Widget sẽ có 1 ID global unique, với Widget muốn được render ở App View thì nó cần phải set appView=true sau đó define thêm routeName, và tuỳ vào framework thì ta cần khai báo thêm plugins với Vue hoặc module với Angular, React thì không cần, sau đó ta export ra tất cả các Widget mà MFE có.

Với update này ở loader.ts sẽ cho phép mỗi MFE có thể expose bao nhiêu Widget tuỳ ý, nhớ đảm bảo id unique là được, điều này sẽ rất tiện vì MFE team có thể chủ động thêm bớt widget mà không cần phụ thuộc vào ai 😘, ví dụ:

// loader.ts
const widget1 = {};
const widget2 = {};
const widget3 = {}; const app1 = {};
const app3 = {}; export { widget1, widget2, widget3, app1, app3 };

Còn về ID thì bởi vì giờ ta đã có nhiều widget, có thể trùng lặp framework, nên ta cần có 1 ID cụ thể để App shell có thể phân biệt chúng.

Thậm chí là khi làm thực tế thì khả năng cao ta còn cần support trường hợp 1 widget ID được mở nhiều lần trên màn hình, lúc đó ta cũng cần maintain thêm 1 ID khác, ví dụ _ID để phân biệt giữa các instance của cùng 1 loại widget

App-shell

Phía App shell thì mình update chút để nó có thể đọc được nhiều Widget được export từ cùng 1 MFE như ta nói ở trên, các bạn xem thêm ở app-shell/src/app/main/main.component.ts > loadModules

Có một số chỗ ta cần chú ý là ở routing mặc định của App Shell được cấu hình ở app-shell/src/app/app.routes.ts:

export const routes: Routes = [ { path: 'login', component: LoginComponent, }, { path: 'main', component: MainComponent, canActivate: [AuthGuard], children: [] }, { path: '**', redirectTo: 'main', },
];

Chú ý phần children của main là nơi mà ta sẽ thêm các route từ các Widget của MFE vào

Quay lại loadModules ở MainComponent, ở đó ta chú ý đoạn sau:

mainRoute.children?.push({ path: l.routeName, ...(l.framework === 'angular' ? { loadChildren: () => l.module } : l.framework === 'react' ? { component: ReactWrapperComponent } : { component: VueWrapperComponent }), data: { component: l.component, plugins: l.plugins, },
});

Ở đây logic của ta là như sau:

  • nếu widget là Angular -> dùng loadChildren để lazyload routing module của widget, ta dùng lazyload chứ không load trực tiếp sẽ tối ưu hơn cho user, họ chỉ load JS file của widget đó khi cần
  • còn nếu widget là React/Vue thì ta vẫn dùng Wrapper component như thường, như kiểu bên Widget View vậy
  • với mỗi route thì ta truyền thêm componentplugins cho nó nếu có (nếu được khai báo ở loader.ts ấy)

main.component.html ta có đoạn:

<ng-template #appView> <router-outlet></router-outlet>
</ng-template>

Đoạn này là ta check nếu là App View thì render cái router-outlet, nó giống như <router-view> phía vue-router<Outlet> phía react-router. Ở đó ta sẽ render component theo cấu hình routing

Sơ sơ thì sự khác biệt so với các bài trước cũng có vậy thôi, giờ ta tới phần chính nè 💪💪

Các vấn đề hiện tại

Ở đây vì để implement giải pháp ngay tại bài blog này sẽ dài nên chúng ta sẽ chỉ thảo luận về solution và tuỳ các bạn implement nha 😘

Cấu hình route của MFE bị phụ thuộc vào app-shell

Nếu ta để ý ở /vue-app/src/router.ts ta có:

const router = createRouter({ history: createWebHistory('/main/vue-app'), routes,
});

Hay ở /react-app/src/appview/AppView.tsx:

<BrowserRouter basename="/main/react-app">

Ở đây ta thấy rằng mỗi MFE kia (Vue/React) phần cấu hình router ta phải set base cho nó là /main/..., phần đuôi vue-app/react-app phải trùng với routeName ta khai báo ở loader.ts.

Vấn đề ở đây là cái /main, cái đó là của app-shell mà, sao MFE lại hardcode ở đây? Nhỡ sau này app-shell thay đổi thì toàn bộ MFE cũng phải đổi à????

Lí do là vì nếu không có cái đó thì khi Vue/React navigate lúc runtime nó sẽ là /react-app hay /vue-app mà không có tiền tố /main ở trước, kiểu này thì phía App Shell không biết là nó ở route nào, và không render ra được

Các bạn có thể nghĩ là làm như thế này thì sao:

export const routes: Routes = [ { path: 'login', component: LoginComponent, }, { path: 'main', component: MainComponent, canActivate: [AuthGuard], }, // ----------------------- { path: 'react-app', component: ReactWrapperComponent }, // ----------------------- { path: '**', redirectTo: 'main', },
];

Tức là thêm route của MFE trực tiếp vào ngang level với /main, ừa thì kiểu này được nhưng khả năng đụng độ và override giữa app-shell với MFE hoặc giữa các MFE với nhau là rất cao

Solution: ta có thể truyền window.location.pathname vào cho MFE luôn, vì tại thời điểm ta select option ở dropdown thì app-shell sẽ tiến hành navigate đến url của MFE trước, ví dụ /main/react-app, sau đó mới tiến hành render react component, lúc này ta lấy ra window.location.pathname là được. Ví dụ ở /react-app/src/appview/AppView.tsx ta sửa lại như sau:

import { BrowserRouter, Route, Routes, Navigate } from "react-router";
import Main from "./Main";
import Foo from "./Foo";
import Bar from "./Bar"; const AppView = () => { return ( <BrowserRouter basename={window.location.pathname}> <Routes> <Route element={<Main />}> <Route path="/foo" element={<Foo />} /> <Route path="/bar" element={<Bar />} /> </Route> <Route path="/" element={<Navigate replace to="/foo" />} /> </Routes> </BrowserRouter> );
}; export default AppView;

Chú ý ở trên ta có <BrowserRouter basename={window.location.pathname}>

Sau đó quay lại app shell ta sẽ thấy React MFE hoạt động như thường:

Ta có thể làm tương tự với Vue ở /vue-app/src/router.ts nha 😉

Một solution khác là ta cũng có thể tính toán basename từ phía app shell và truyền xuống MFE nha:

export class ReactWrapperComponent { @Input() component: any; root!: Root; constructor(private readonly host: ElementRef) {} ngAfterViewInit() { this.root = createRoot(this.host.nativeElement); // ===== truyền props vào React component ===== this.root.render(createElement(this.component, { basename: '/main/react-app' })); } ngOnDestroy() { this.root.unmount() }
}

Ủa vậy đến Angular thì sao???? 🤔🤔

Thì Angular là trường hợp đặc biệt, vì App shell của ta cũng là Angular, nên Angular MFE sẽ được "kết nối" như là 1 phần của App shell luôn, cụ thể hơn thì ta sẽ chỉ có 1 Angular root app. Khác với React/Vue, mỗi 1 component sẽ là 1 root app riêng biệt, do vậy phần routing của Angular MFE sẽ tự động có basename chính xác luôn theo những gì ta set ở main.component.ts phía app-shell luôn 💪

Sử dụng URL trực tiếp tới MFE

Nếu các bạn để ý, các URL của app view của các MFE sẽ là kiểu http://localhost:4200/main/react-app/foo

Nhưng nếu ta copy cái URL đó, mở tab mới để truy cập trực tiếp vào App View React thì sẽ không được, sẽ toàn thấy nó show màn login (vì ban đầu ta chưa authenticated), login xong nó luôn luôn show màn Widget view mặc định

Vấn đề này cũng hơi chuối chút và phụ thuộc khá nhiều vào business của các bạn, xem là các bạn muốn đi theo hướng nào, ví dụ ta có thể chọn hướng sau:

  1. Khi truy cập thẳng vào URL -> đẩy ra màn Login -> update URL để lưu URL "tiếp theo" ta cần chuyển hướng vào sau khi login, ví dụ: http://localhost:4200/login?next=/main/react-app/foo
  2. Sau khi zô bên trong thì phải check xem có next ở URL không, và nó có hợp lệ không, có bắt đầu bằng main không, nhỡ nó lại là next=/dummy/react-app/foo thì ta làm gì có route nào bắt đầu bằng dummy đâu 😂
  3. Đoạn tiếp theo chuối này: ở bước này App Shell không hề biết là react-app/foo nó xuất phát từ MFE nào, do vậy App shell cần phải load toàn bộ loader.ts của các MFE lên, hoặc load từng cái lên và đọc ra để tìm routeName=react-app, dù là kiểu nào thì cũng sẽ hơi bị không tối ưu chút vì ta vẫn phải load nhiều hơn cần thiết. Cái này thì ta có thể làm kiểu cache routeName chẳng hạn, load lần đầu thì ghi nhớ lại (ở phía trình duyệt hoặc phía server), rồi từ lần sau lấy từ cache ra để biết nó tới từ MFE

Thêm một vấn đề nữa là trong lúc ta load MFE ta cũng nên show cho user thấy là MFE đang được load, ví dụ:

Nhưng nhỡ mà load MFE rất lâu thì sao? hay có lỗi khi load thì sao?? 😅😅 User mất công ngồi chờ một đống xong tự nhiên chả có kết quả gì 🥹

Cái này thì tuỳ vào business ta cần implement như thế nào sao cho phù hợp: có thể là set loading timeout = 3 giây, hết 3 giây load không xong thì prompt báo user có lỗi và redirect về trang chủ, hoặc cho phép User Retry load lại...

Bài toán này khi làm thật có vẻ cũng chuối phết đới vì ta cần phải cover nhiều cases 😂😂😂

Cấu hình loader.ts bị phân hoá

Các bạn có để ý thấy rằng ở loader.ts của Angular thì ta cần khai báo thêm module, của Vue thì ta cần khai báo thêm plugins không? trong khi React thì không cần

Cái này nó đến từ sự khác nhau giữa các library/framework, cách cấu hình routing cho chúng khác nhau:

  • với React thì rất tiện, dynamic, lúc nào cũng Component là xong
  • Vue thì phải có thêm tí app.use(routerPlugins)
  • Angular thì ta lazyload cả Module routing của MFE
  • ...

Tương lai thêm nhiều framework nữa thì lại thêm một lố options à??? 😂😂

Cái này thì cũng lại tuỳ business của chúng ta làm sao cho nó phù hợp nhất có thể thôi nè ☺️☺️

Nếu các bạn thấy việc thêm nhiều options như kia mỗi khi ta muốn support thêm một feature mới có vẻ không cần thiết và dài dòng thì ta có thể dùng 1 cái callback, khi init component App shell sẽ gọi cái callback đó, còn lại thì MFE muốn làm gì thì làm, ví dụ ở loader.ts cho Vue ta khai báo như sau:

import type { App as VApp } from 'vue'; const app = { id: "VUE_APP", framework: "vue", component: AppView, routeName: "vue-app", appView: true, title: "Vue App", onInit: (app: VApp) => { app.use(router); }
};

Xong ở phía App Shell ta có:

export class VueWrapperComponent { @Input() component: any; @Input() onInit: any; app!: App<Element>; constructor(private readonly host: ElementRef) {} ngAfterViewInit() { this.app = createApp(this.component); this.onInit(this.app); this.app.mount(this.host.nativeElement); } ngOnDestroy() { this.app.unmount(); }
}

Ở trên ta thấy là MFE đã tự chủ động register plugin tuỳ ý, bên cạnh đó thì với việc có quyền truy cập vào root App thì MFE cũng có thể làm nhiều thứ khác trên instance của root App luôn. App-Shell sẽ chỉ gọi this.onInit(this.app); còn lại bên trong onInit có gì thì không cần quan tâm

Làm kiểu này mặt tốt là cho MFE khả năng linh hoạt nhưng cũng có vấn đề khác về việc MFE có thể tác động làm thay đổi root instance và sau này khi App shell đụng vô thì lại xảy ra các vấn đề không mong muốn (lỗi runtime), sẽ tương đối bất định và khó đoán. Ta cân nhắc lựa chọn sao cho phù hợp nha 😘

Routing của Angular MFE đang không được lazyload

Nếu các bạn để ý thì phía App shell, khi load Angular ta dùng loadChildren (xem ở MainComponent), đây là kĩ thuật dùng để Lazy-loading feature modules trong Angular, ở đó ta chỉ load các file JS bundle của 1 module khi ta thật sự truy cập vào module đó

Nhưng mà hiện tại khi ta load loader.ts lên thì nó đã load luôn cả routing module của MFE lên cả rồi, dẫn tới việc size của loader.ts của Angular lớn hơn, và lãng phí nếu user chả bao giờ xem tới Angular App View

Để fix điều này thì ở loader.ts của angular-app ta sửa lại cấu hình của app view như sau:

const app = { id: 'ANGULAR_APP', framework: 'angular', module: () => import('./app-view/app-view.module').then((m) => m.AppViewModule), routeName: 'angular-app', appView: true, title: 'Angular App',
};

Ở trên ta đã sửa module thành 1 function và return về dynamic import

Sau đó ta quay lại App Shell -> main.component.ts, sửa lại đoạn loadChildren như trả trực tiếp về l.module:

mainRoute.children?.push({ path: l.routeName, ...(l.framework === 'angular' ? { loadChildren: l.module } : l.framework === 'react' ? { component: ReactWrapperComponent } : { component: VueWrapperComponent }), data: { component: l.component, plugins: l.plugins, },
});

Chú ý rằng ở trên loadChildren đã không còn là arrow function nữa mà ta đã set nó trực tiếp bằng l.module

Sau đó ta quay lại app shell, login, chọn App View và mở Angular app:

20241228-183159.jpeg

Ta thấy rằng giờ đây chỉ khi ta mở Angular App lên thì routing module của nó mới được load theo, size của loader.ts cũng giảm đi một nửa khi ta vừa login xong mà chưa truy cập vào Angular app.

Ban đầu 15.6KB:

Sau đó 7KB + 9KB:

Cũng được đó nhờ, vậy thì ta cũng có thể áp dụng lazyload trong trường hợp là Widget view đúng không?? 🧐🧐

Chuẩn rồiiiiii 😂, ta thử coi nha

Vẫn ở loader.ts của Angular app ta sửa cấu hình của widget thành:

const widget = { id: 'ANGULAR_WIDGET', framework: 'angular', component: () => import('./app.component').then((m) => m.AppComponent), title: 'My Vue Widget',
};

Ở trên ta cũng thay component thành function trả về dynamic import tương tự khi nãy

Sau đó ta quay lại App shell > angular-wrapper.component.ts ta sửa lại chút:

export class AngularWrapperComponent { @Input() component: any; @Input() module: any; resolvedComponent: any = null; ngOnInit() { console.log('AngularWrapperComponent ngOnInit'); this.component().then((m: any) => { this.resolvedComponent = m; }); }
}

Ở trên ta phải thêm resolvedComponent để lưu component được resolved từ this.component(), bởi vì Dynamic import nó return về Promise tức là cái this.component() nó chính là Promise và ta cần gọi .then để resolve nó và lấy về component ta muốn rồi lưu vào resolvedComponent

Ở phía App shell > angular-wrapper.component.html ta update lại chút:

<ng-container *ngIf="resolvedComponent; else loading"> <ng-container *ngComponentOutlet="resolvedComponent"></ng-container>
</ng-container>
<ng-template #loading> <p>Loading...</p>
</ng-template>

Ở trên mặc định ta show loading khi component đang được load, load xong thì show resolvedComponent

Sau đó ta quay lại App shell, Login và để ý rằng ở Widget view có 1 request cuối cùng để load JS file chính là của Angular App để lazyload cái component được khai báo ở loader.ts của Angular app:

Và vì giờ nó là lazyload nên size của loader.ts cũng được giảm đi đáng kể đó 😘😘

Tại sao lazyload lại đóng vai trò quan trọng trong kiến trúc MFE?

Vấn đề của ta ở đây là sau khi login xong, không phải lúc nào ta cũng mở hết tất cả các Widget View/App View của mọi MFE lên và xem, do vậy, ban đầu ta chỉ cần load file loader.ts để biết 1 số thông tin cơ bản của MFE thôi, khi nào user thật sự click vào thì hẵng load.

Theo mình ta nên xem file loader.ts giống như 1 file metadata ấy, ở đó define các cấu hình là chính, size của nó nên xêm xêm size của vài JSON objects thôi, chứ ta không nên import thẳng các component/module vào. Bởi khi khi truy cập vào app shell chắc gì user sẽ load hết các MFE lên, thực tế chủ yếu họ sẽ chỉ load vài MFE mà họ hay sử dụng 😘

App shell re-render vì cấu hình router thay đổi

Nếu ta để ý, ở app-shell/src/app/app.config.ts, mình có CustomRouteReuseStrategy, giờ ta thử comment nó lại trước và tí mình giải thích nhé:

export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes, withComponentInputBinding()), // bind route data to component inputs provideAnimationsAsync(), // { // provide: RouteReuseStrategy, // useClass: CustomRouteReuseStrategy, // }, ],
};

Sau khi quay lại App shell, thử truy cập vào các MFE, ví dụ Angular ta sẽ thấy như sau:

1.jpg

Ta để ý thấy rằng khi mở được Angular MFE -> Bấm navigate vào Foo/Bar thì MainComponent bị re-render -> đang từ App View bị nhảy về Widget View 😵‍💫

Việc này dẫn tới UX cực kì lởm, và rất không tối ưu, vì MainComponent mà bị re-render thì toàn bộ children của nó tức là các MFE cũng bị re-render theo 🥹

Vấn đề là vì ở main.component.ts ta có đoạn:

this.router.resetConfig(this.router.config);

Bởi vì bên trên đoạn đó ta có phần đọc routing từ loader.ts của các MFE và xử lý, sau khi xử lý xong ta cần resetConfig của router lại để nó ăn. Nhưng với việc làm này thì Angular sẽ coi như ta vừa có một routing config mới hoàn toàn và coi như cái cũ không làm gì được nữa, nên là lần tiếp theo navigate nó sẽ reset lại toàn bộ component có ở routing config, bao gồm cả MainComponent.

Kể cả ta có dùng một biến global nào đó để control lưu lại state của main component thì ít nhất 1 lần tiếp theo nó cũng sẽ bị reset (mình đã thử rồi 😉). Do vậy là ở app.config.ts mình mới cần thêm CustomRouteReuseStrategy, để nói với Angular là "yo bro, đừng re-render lại component nếu path hiện tại vẫn là main trong quá trình navigate nhé 😘":

shouldDetach(route: ActivatedRouteSnapshot): boolean { return route.routeConfig?.path === 'main'; // Cache MainComponent
}

Vậy nên khi làm thực tế ta cũng phải rất để ý tới vấn đề này nha, nó ảnh hưởng trực tiếp tới UX và performance của cả kiến trúc

Routing không tương thích giữa các library/framework

Giờ ở app-shell > main.component.html, đoạn xử lý appView ta sửa lại như sau:

<ng-template #appView> <a routerLink="/main/react-app/foo">React Foo</a>&nbsp; <a routerLink="/main/react-app/bar">React Bar</a> <hr> <router-outlet></router-outlet>
</ng-template>

Ở trên ta đơn giản là thêm 2 cái router link để nhảy trực tiếp tới React app, để chuẩn bị đây ta test xem từ Angular/Vue MFE có nhảy sang React được không. Lưu lại và F5 app shell sau đó ta test coi nè:

Ầu, vậy là nó đang không ăn được vào React App, lí do là bởi vì routerLink kia là của App Shell ta dùng Angular, khi click vào đó App Shell sẽ check ở trong config của nó trước và không thấy /main/react-app/foo được khai báo ở đâu cả, mà cái đó chỉ React App mới biết 🥹

Giờ ta update lại App shell > main.component.ts một chút nha:

//...... if (!existingRoute) { mainRoute.children?.push({ path: l.routeName, ...(l.framework === 'angular' ? { loadChildren: l.module } : l.framework === 'react' ? { component: ReactWrapperComponent, children: [{ path: '**', component: ReactWrapperComponent }] } : { component: VueWrapperComponent }), data: { component: l.component, plugins: l.plugins, }, });
}

Ở trên ta thêm vào children: [{ path: '**', component: ReactWrapperComponent }] ở chỗ React, để nói với App shell rằng, với tất cả các children routes mà bắt đầu bằng /react thì cũng dùng ReactWrapperComponent luôn nha. Ta lưu lại và F5 app shell check tiếp nè:

Ngon rồi, nhảy được vào React App rồi nè 🎉🎉🎉

Ầu, mà có một vấn đề, sau khi nhảy được vào React App rồi, click tiếp mấy cái Router Link đó thì không thấy nó chạy:

Lúc nào nó cũng chỉ là Foo thôi

Check thử basename của React Router cái coi:

Ầu nó đang là /main/react-app/foo, lí do là ban đầu click vào Router Link -> Angular điều hướng tới path đó trước, sau đó mới tiến hành render React component, dẫn tới việc basename bị sai, và cách implement của ta với pathname hiện tại có vẻ không ổn rồi 🥹

Thôi để cho chắc chắn nhất ta vẫn phải tính toán basename từ app shell và truyền xuống nha

Ở App shell > main.component.ts đoạn xử lý routing ta truyền thêm basename vào `data:

if (!existingRoute) { mainRoute.children?.push({ path: l.routeName, ...(l.framework === 'angular' ? { loadChildren: l.module } : l.framework === 'react' ? { component: ReactWrapperComponent, children: [{ path: '**', component: ReactWrapperComponent }] } : { component: VueWrapperComponent }), data: { component: l.component, plugins: l.plugins, basename: l.routeName, }, });
}

Sau đó vẫn ở App shell > react-wrapper.component.ts, ta sửa lại như sau:

export class ReactWrapperComponent { @Input() component: any; @Input() basename: any; root!: Root; constructor(private readonly host: ElementRef) {} ngAfterViewInit() { this.root = createRoot(this.host.nativeElement); this.root.render(createElement(this.component, { basename: `/main/${this.basename}`, })); } ngOnDestroy() { this.root.unmount() }
}

Ở trên ta nhận vào Input basename sau đó truyền nó vào làm props lúc render React component

Quay lại phía bên react-app/src/appview/AppView.tsx, ta sửa lại chút:

const AppView = ({ basename }: { basename?: string }) => { return ( <BrowserRouter basename={basename}> <Routes> <Route element={<Main />}> <Route path="/foo" element={<Foo />} /> <Route path="/bar" element={<Bar />} /> </Route> <Route path="/" element={<Navigate replace to="/foo" />} /> </Routes> </BrowserRouter> );
};

Ở trên ta đã nhận vào basename từ props và truyền vào BrowserRouter

Âu cây ta lưu lại tất cả và quay lại App shell F5 check:

Thì thấy rằng khi đang từ Angular App MFE, ở lần đầu tiên click nó đã nhảy đúng vào route Foo hoặc Bar phía React, chứ không chỉ lúc nào cũng show Foo như vừa nãy nữa

Nhưng từ những lần sau click tiếp vào Router Link của Angular thì nó vẫn không navigate phía React:

Bài toàn này cũng khá nan giải đây... 🙁🙁🙁

Vì giữa React và Angular cách mà routing nó hoạt động khác nhau, và React nó không lắng nghe "listen" được khi ta click vào router Link của Angular, mặc dù thanh URL của trình duyệt thay đổi, nhưng React vẫn không biết được là route đã thay đổi để tiến hành render lại React Component

Vấn đề này cũng khá là củ chuối nếu ta gặp phải, giải pháp thì ta có thể dùng API của browser để listen on route change (dùng cái on route change của React router không được đâu nha vì URL change xảy ra ở bên ngoài React app)

Ví dụ ở react-app/src/appview/Main.tsx ta sửa lại như sau:

import "./Main.css";
import reactLogo from "../../public/react.svg?inline";
import { Outlet, NavLink } from "react-router";
import { useEffect } from "react"; const Main = () => { useEffect(() => { const handleNavigate = (event) => { console.log('URL changed:', event.destination.url); }; if (window.navigation && window.navigation.addEventListener) { window.navigation.addEventListener('navigate', handleNavigate); } return () => { if (window.navigation && window.navigation.removeEventListener) { window.navigation.removeEventListener('navigate', handleNavigate); } }; }, []); return ( <div className="react-app-view"> <NavLink to="/foo">Foo</NavLink> <NavLink to="/bar">Bar</NavLink> <div> <img src={reactLogo} alt="React Logo" width={100} style={{ marginTop: 16 }} /> </div> <Outlet /> </div> );
}; export default Main;

Ở trên ta dùng API mới của trình duyệt đó là window.navigation (các bạn cứ kệ lỗi type check nha 😁

Sau đó lưu lại và ta quay lại app shell và test:

Ta thấy rằng giờ thì React App đã listen được route change, từ đây ta có thể viết thêm chút custom logic để xử lý việc navigate trong nội bộ React Router 😘

Đây chắc là một trong những vấn đề củ chuối nhất ta có thể gặp phải khi xử lý bài toán routing, và nó rất phụ thuộc vào business của từng người là gì để chọn giải pháp implement cho phù hợp

Các MFE dùng History Mode khác nhau

Thêm một vấn đề nữa là việc các MFE có thể dùng các History mode khác nhau cũng đau đầu phết 😂, theo mình thấy có 3 loại History mode phổ biến:

  • Web History: dùng history của trình duyệt, các library/framework router sẽ push/pop state vào đó. Cái này là cái phổ biến nhất và trông "tự nhiên" nhất. Như ở bài này là ta dùng toàn bộ cách này đây 😘
  • WebHash History: library/framework router sẽ thêm # vào trước các route, ví dụ https://example.com/folder/#/app/, dùng cái này có thể fix được 1 số trường hợp ngoại lệ kiểu server không xử lý được "any route" chẳng hạn. Cũng... không phổ biến cho lắm.
  • Memory History: cái này thì toàn bộ history sẽ được lưu in memory, áp dụng trong một số trường hợp với Server side rendering, kiểu ta có thể bắt đầu app từ bất kì một nơi nào đó. Mình thấy mọi người khá ít dùng cách này.

Ví dụ với WebHash History: ta để ý thanh URL có dấu #:

Ví dụ với Memory history: thanh URL trình duyệt không hề thay đổi, cũng không có history trình duyệt -> không Go back được về đúng route trước đó

Việc các MFE sử dụng các history mode khác nhau đôi khi sẽ gây khó khăn trong việc conflict giữa app shell và các MFE với nhau, hoặc cross navigation (navigate từ MFE này sang MFE kia). Vấn đề này ta cũng cần phải lưu tâm và chọn cách implement phù hợp hoặc "force" yêu cầu bắt buộc các MFE follow theo 1 tiêu chuẩn cụ thể

Tổng kết

Hi vọng rằng qua bài này mình đã mang tới cho các bạn góc nhìn về cách giải quyết một số vấn đề có thể gặp phải khi xử lý Routing trong kiến trúc Microfrontend.

Khi thực chiến sẽ có vô vàn use cases khác có thể xảy ra tuỳ thuộc vào business của từng người, ta nên linh hoạt và chọn cách phù hợp nhất nha 🥰

Chúc các bạn cuối tuần vui vẻ, hẹn gặp lại các bạn vào những bài sau 👋👋

Bình luận

Bài viết tương tự

- vừa được xem lúc

Học React Router cho bớt ngu nger

1. Giới thiệu. Ngày nay việc sử dụng SPA (Single Page Application) đã khá phổ biến so hơn trước. So với các ứng dụng SSR(Server-Side Rendering) thì một trong những ưu điểm vượt trội hơn của SPA chính là khi chúng ta làm việc với routing.

0 0 163

- vừa được xem lúc

Dynamic Routing trong NGINX

Tiếp nối bài viết trước, Cấu hình Nginx Server như thế nào?. Trong bài viết này, mình sẽ giới thiệu về Dynamic Routing trong NGINX.

0 0 42

- vừa được xem lúc

Blog#28: Routing trong Nodejs Express - [Express Tutorial - Part 1/10] 😊 (Series: Bí kíp Javascript - PHẦN 23)

Mình là TUẤN hiện đang là một Full-stack Developer tại Tokyo . Route đề cập đến cách các endpoint (URI) của ứng dụng phản hồi các request của client.

0 0 32

- vừa được xem lúc

Routing

Basic Routing. use Illuminateupportacadesoute;. . Route::get('/greeting', function () {.

0 0 18

- vừa được xem lúc

Các Nguyên Tắc Cơ Bản Về Routing (Định Tuyến) trong NextJS - Bài 4

Các Nguyên Tắc Cơ Bản Về Routing (Định Tuyến) trong NextJS. Bộ khung của mỗi ứng dụng là định tuyến.

0 0 8

- vừa được xem lúc

Traffic mirroring là gì? Cấu hình Traffic mirroring cho Nginx và Apache

Hệ thống thông tin ngày càng trở nên phức tạp và tiềm ẩn nhiều môi đe dọa hơn đòi hỏi các hệ thống cần có các phương pháp quản lý lưu lương truy cập một cách phù hợp hơn. Điển hình những năm gần đây đ

0 0 2