Xử lý bài toán multi bundlers khi làm việc với Microfrontend

0 0 0

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

Theo Viblo Asia

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

Mỗi đợt bận việc trên cty nhưng ngày nào cũng lên Viblo nhìn thông báo nhảy tưng tưng thấy mọi người vẫn đọc blog của mình đều, nhìn thôi là thấy có động lực viết tiếp liền. Nhưng khổ cũng bắt đầu già rồi nên càng ngày càng lười 😂😂

Kể từ khi viết bài Microfrontend, Module Federation - đưa microservices đến với frontend đến giờ, có khá là nhiều bạn nhắn hỏi mình các vấn đề xoay quanh nó. Nhận thấy MFE hiện tại (ở VN) có vẻ vẫn còn mới và nhiều người gặp khó khăn trong việc tiếp cận nó nên mình sẽ viết thành 1 series với tất cả những gì mà mình đã từng làm, hi vọng rằng có thể đưa đến cho các bạn nhiều giải pháp trong cách vận hành nó.

Ở bài ngày hôm nay ta sẽ cùng nhau setup để kiến trúc MFE của chúng ta có thể support nhiều bundlers thay vì chỉ mỗi Webpack nhé.

Lên thuyền thôi anh em ơiiiiiiiiii 🚢🚢

Review kiến thức

Mình rất hi vọng các bạn đã đọc bài trước của mình và làm theo ví dụ ở bài đó: Microfrontend, Module Federation - đưa microservices đến với frontend, để có thể có các kiến thức và hiểu cơ bản về MFE.

Một số tiêu chí mà mình nghĩ 1 kiến trúc MFE tốt cần có như sau:

  • Các MFE và app shell là độc lập, các team dev riêng, deploy riêng, pipeline riêng...Không ai block ai
  • Thay đổi 1 MFE thì chỉ MFE đó cần deploy lại
  • Có thể áp dụng authentication/authorization để control khi nào load MFE nào
  • Mỗi MFE có thể tuỳ ý setup tech của riêng nó: bundler nào, framework nào,...Đều là do team dev cái MFE đó quyết định
  • Việc giao tiếp giữa các MFE phải đơn giản hết sức có thể
  • Developers người mà code cái MFE thì nên có cái trải nghiệm như kiểu họ đang code một project bình thường chứ không phải đang dùng 1 cái inhouse framework nào đó

Chúng ta sẽ dùng những tiêu chí trên đi xuyên suốt series này nhé

Vấn đề

Như các bạn thấy, một trong những tiêu chí ở trên đó là kiến trúc MFE của chúng ta có thể support nhiều bundlers nhất có thể

Ở ví dụ trong bài trước: Microfrontend, Module Federation - đưa microservices đến với frontend. Thì mình dùng hết Webpack Module Federation.

Nhưng như trong bài mình có nói, Module federation giờ đã được phát triển thành 1 chuẩn (standard), miễn là ta bundle nó với những tiêu chí cụ thể, thì nó có thể hoạt động như 1 MFE

Trong thực tế khi ta phải support nhiều framework, nhiều team, ta sẽ thấy rằng mỗi team họ sẽ chọn 1 framework riêng, đi kèm với đó là bundler riêng: khi thì Vite, lúc thì Rollup, Esbuild, giờ thì hot những cái JS bundler viết bằng Rust như Rolldown hay Rspack,...

Và một kiến trúc MFE tốt thì không nên hạn chế các team MFE được chọn tech stack của họ 😊

Những gì ta sẽ làm

Ở ví dụ ngày hôm nay ta sẽ bắt đầu với app shell (Webpack) + 1 MFE (Webpack).

Sau đó ta sẽ setup để có thể support thêm Vitersbuild (build tool dựa trên rspack) nhé

Lên nhạc anh em ơiiiiii 🕺🕺

Setup

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

Sau khi clone về ta sẽ thấy trong folder có app shell và 1 mfe như sau:

Tiếp đó ở mỗi folder ta chạy npm install nhé

Và ta start từng thứ lên nha:

# app-shell
npm start # angular-app
npm start

Và giờ ta mở trình duyệt ở địa chỉ http://localhost:4200 sẽ thấy như sau:

Ta login với user username=userpassword=user:

Và ta zô bên trong sẽ thấy MFE Angular được load lên thành công:

Vì hiện tại cả app shell và MFE dùng Webpack nên chúng tương thích khá dễ dàng vì code bundler cho ra cùng 1 chuẩn. Để xem khi làm với các bundler khác sẽ thế nào nhé

Zô món chính thôiiiiiii

Rsbuild - React

Ở phần này ta sẽ setup 1 một React MFE dùng Rsbuild và https://module-federation.io/ nhé

À giới thiệu chút là cái https://module-federation.io/ nó là 1 cái lib opensource tích hợp với rsbuild để làm MFE cực kì mượt, support cả typecheck luôn, nếu các bạn mà bắt đầu làm việc với MFE thì mình khuyên là nên dùng cái đó, nó abstract cho ta rất nhiều, không phải setup gì loằng ngoằng, support nhiều framework (nhưng chưa có Angular 😂)

Ta bắt đầu nhé 💪💪

Ở folder viblo-mfe-multi-bundlers ta chạy command sau để tạo project rsbuild mới

npm create rsbuild@latest

Ta đặt tên project là react-app và chọn framework là React nhé, setting chọn như sau:

Sau khi tạo xong ta có như sau:

Ta chạy npm install ở folder react-app nhé, sau đó ta thử start nó lên coi xem nó như thế nào:

# react-app
npm run dev

Oke ngon rồi, giờ ta install module federation cho nó nhé, vẫn ở folder react-app ta chạy:

npm add @module-federation/enhanced

Tiếp theo ta sẽ tạo asynchronous boundary bằng cách tạo file src/bootstrap.tsx sau đó copy content từ file src/index.tsx sang nhé:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'; const rootEl = document.getElementById('root');
if (rootEl) { const root = ReactDOM.createRoot(rootEl); root.render( <React.StrictMode> <App /> </React.StrictMode>, );
}

Sau đó sửa file src/index.tsx thành như sau:


import('./bootstrap')

Tiếp theo ở file rsbuild.config.ts ta update lại như sau nhé:

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack"; export default defineConfig({ plugins: [pluginReact()], server: { port: 3002, }, dev: { assetPrefix: true, }, tools: { rspack: { output: { uniqueName: "react_mfe_app", // cái này phải unique cho mỗi mfe nhé }, plugins: [ new ModuleFederationPlugin({ name: "react_mfe_app", exposes: { ReactAppLoader: "./src/loader.ts", }, shared: { react: { singleton: true, }, "react-dom/client": { singleton: true, }, }, }), ], }, },
});

Phần setup này rất cơ bản mình lấy từ Gettings Started của module federation thôi

Ở trên mình fix cứng port là 3002 để tí app shell còn gọi tới

Như các bạn thấy thì đoạn setup ModuleFederationPlugin các options mà họ support cũng giống y như bên Webpack vậy (check ở file webpack.config.js)

Cuối cùng là ta tạo file src/loader.ts nhé

import App from './App.tsx'; export default { framework: 'react', component: App
}

Nếu các bạn thắc mắc file loader này để làm gì, thì mình dùng nó để khai báo các thuộc tính cần thiết để lát nữa app shell load cái MFE này lên thì nó biết cần làm gì, dùng framework nào để render MFE

Ổn roài, sau khi setup thì cấu trúc thư mục ta có như sau:

Giờ ta quay lại app shell và khai báo MFE này ở file app-shell/src/app/app.service.ts nhé:

//....... const remoteModules = [ { remoteEntry: 'http://localhost:3001/remoteEntry.js', remoteName: 'angular_mfe_app', exposedModule: 'AngularAppLoader', }, { remoteEntry: 'http://localhost:3002/remoteEntry.js', remoteName: 'react_mfe_app', exposedModule: 'ReactAppLoader', },
]; 

Oke ngon rồi, ta restart lại react-app nhé:

npm run dev

Sau đó ta thấy terminal sẽ in ra như sau tức là module federation đã được setup cho MFE nhé:

Ở trên các bạn thấy có dòng Federated types created correctly tức là nó gen cả type check cho MFE xịn luôn, nhưng mà nó chỉ tích hợp nếu ta dùng toàn bộ cả app-shell và MFE bằng https://module-federation.io/ thôi

Sau khi React MFE đã lên và được khai báo với app shell thì giờ ta quay lại app shell, F5 và login lại xem React lên chưa nhé:

Ủa mới có mỗi Angular???? React đâu??????? 🧐🧐🧐

Mở console thì thấy lỗi sau:

Mở tiếp tab Network để xem file remoteEntry.js của React MFE nó có được load không:

Ầu sết, lỗi 😢😢

Ta thử mở đường dẫn tới file remoteEntry từ trình duyệt xem nhé:

Ầuuuuu, nó trả về trang web (HTML) chứ không phải file JS, ủa lạ nhẩy.....

Kiểm tra lại Terminal thấy rằng có vẻ nó gen ra file mf-manifest.json chứ không phải remoteEntry.js:

Check trên Website của Module Federation: https://module-federation.io/configure/index.html thì có vẻ đúng như vậy thật, mặc định họ dùng file manifest để giao tiếp giữa app-shell và MFE vì file đó nó chưa nhiều thông tin hơn và cho phép linh hoạt hơn trong việc triển khai MFE, nhưng nó chỉ phù hợp nếu ta dùng toàn bộ App shell và MFE với setup của họ thôi, còn trường hợp của chúng ta là mỗi thành phần thoải mái chọn tech stack

May quá có một option để ta có thể generate remoteEntry.js, ta sửa lại lại file rsbuild.config.ts cho react-app như sau:

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack"; export default defineConfig({ plugins: [pluginReact()], server: { port: 3002, }, dev: { assetPrefix: true, }, tools: { rspack: { output: { uniqueName: "react_mfe_app", // cái này phải unique cho mỗi mfe nhé }, plugins: [ new ModuleFederationPlugin({ name: "react_mfe_app", exposes: { ReactAppLoader: "./src/loader.ts", }, shared: { react: { singleton: true, }, "react-dom/client": { singleton: true, }, }, filename: "remoteEntry.js", //-----> Ở đây }), ], }, },
});

Ở trên các bạn thấy rằng ta chỉ thêm vào đúng 1 dòng filename: "remoteEntry.js", cái đó sẽ nói với rsbuild rằng generate thêm cho tôi file remoteEntry.js nữa, và để tuỳ tôi dùng, thích dùng manifest.js hay remoteEntry.js thì kệ tôi 😂😂

Sau đó ta lưu lại và sẽ thấy react-app tự restart.

Ngay sau đó ta quay lại App shell, F5 và login lại lần nữa sẽ thấy như sau:

Pòm pòm, React MFE lên rồi 🎉🎉🎉🎉

Mà style hơi to chiếm nhiều chỗ quá, ta sửa lại chút ở file react-app/src/App.css comment mấy chỗ này nhé:

/* body { margin: 0; color: #fff; font-family: Inter, Avenir, Helvetica, Arial, sans-serif; background-image: linear-gradient(to bottom, #020917, #101725);
} */ .content { display: flex; /* min-height: 100vh; */ line-height: 1.1; text-align: center; flex-direction: column; justify-content: center;
} .content h1 { font-size: 3.6rem; font-weight: 700;
} .content p { font-size: 1.2rem; font-weight: 400; opacity: 0.5;
}

Lưu lại và quay lại App shell F5 ta sẽ thấy nó hiển thị gọn gàng hơn roàiiii:

Cũng đơn giản nhẩy 😍😍

Thực tế là cũng may là https://module-federation.io/ họ support tốt, bundle ra file remoteEntry.js theo đúng chuẩn của Module federation mà mọi người đang dùng, cộng thêm đó là cấu hình như webpack luôn nên là một phát ăn ngay không cần setup thêm nhiều. 10 điểm 👍️👍️

Nhưng cuộc đời sẽ không dễ như vậy mãi đâu các bạn ạ 🤣🤣

Vite - Vue

Tiếp theo ta sẽ setup tiếp 1 MFE với Vite nhé, ở folder viblo-mfe-multi-bundlers ta tạo mới project Vite:

npm create vite@latest

Ta đặt tên là vue-app và chọn framework là Vue nhé:

Sau đó ở folder vue-app ta chạy npm install, và trước khi làm ta phải chạy nó lên test xem đầu đuôi nó trông như thế nào đã chứ nhỉ

Các bạn mở terminal ở vue-app chạy:

npm run dev

Sau đó ta mở http://localhost:5173, thấy như sau là oke rồi nhé:

Giờ ta setup Module federation cho Vite nha, các bạn cài package sau @originjs/vite-plugin-federation:

npm i -D @originjs/vite-plugin-federation

Sau đó ở file vite.config.ts ta sửa lại như sau:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation"; // https://vitejs.dev/config/
export default defineConfig({ build: { target: "esnext", }, preview: { port: 3003, }, plugins: [ vue(), federation({ name: "vue_mfe_app", filename: "remoteEntry.js", exposes: { "VueAppLoader": "./src/loader.ts", }, shared: ["vue"], }), ],
});

Cấu hình Module federation thì vẫn với các options tương tự như ta vẫn làm thôi nhỉ 😁

Chú ý rằng ta có thêm build-> target: "esnext", vì nếu khi tí nữa ta build nó sẽ báo lỗi Top-level await is not available in the configured target environment đó nhé

Ở trên mình cũng fix port=3003 khi preview luôn để tí khai báo với bên app-shell

Giờ ta build Vue MFE nhé (cái này ta phải build chứ chạy dev với Vite thì nó sẽ không gen ra cho ta file remoteEntry.js đâu):

npm run build

Sau đó ta start preview nhé:

npm run preview

Thấy terminal in ra như sau là oke nhé:

Ta mở thử ở trình duyệt kiểm tra xem lên chưa nữa nha:

Tiện kiểm tra luôn đường dẫn của file remoteEntry.js xem oke ko, chú ý rằng với Vite thì JS bundle files nằm ở trong dist/assets nhé:

Do vậy đường dẫn chính xác của remoteEntry.js là: http://localhost:3003/assets/remoteEntry.js. Mở ở trình duyệt ta thấy như sau::

Giờ ta khai báo Vue MFE với app-shell nhé, ở file app-shell/src/app/app.service.ts ta thêm Vue vào:

//......
const remoteModules = [ { remoteEntry: 'http://localhost:3001/remoteEntry.js', remoteName: 'angular_mfe_app', exposedModule: 'AngularAppLoader', }, { remoteEntry: 'http://localhost:3002/remoteEntry.js', remoteName: 'react_mfe_app', exposedModule: 'ReactAppLoader', }, { remoteEntry: 'http://localhost:3003/assets/remoteEntry.js', remoteName: 'vue_mfe_app', exposedModule: 'VueAppLoader', },
];

Sau đó ở app-shell ta F5 login lại:

Ầu, Vue không thấy lên, kiểm tra console thì thấy lỗi gì đó:

Kiểm tra network thì thấy load ngon nghẻ rồi:

Hầy... lỗi gì ta???????? 🧐🧐

Nếu ta mở http://localhost:3003/assets/remoteEntry.js để ý sẽ thấy ta có xíu code thôi à:

So với MFE khác, ví dụ React thì code siêu dài:

Quay lại cái lỗi ở console ta chú ý lỗi này Uncaught SyntaxError: Cannot use 'import.meta' outside a module (at remoteEntry.js:1:293)

Từ đây ta nhận định rằng, có thể là cái remoteEntry.js của Vue nó đang là JS Module

Cái JS module nó khác với JS truyền thống mà ta vẫn dùng, JS truyền thống nó là global, import vào phát là nó chạy luôn và global, còn JS module thì scope của nó nhỏ hơn

Đó là lí do vì sao với React MFE ta thấy có dòng var react_mfe_app này:

Còn với Vue thì ta không có mà nó là import các thứ. Hơn nữa ở app-shell/src/app/utils/federation-utils.ts logics để load MFE của ta hiện tại chưa support cho JS module, mà mới chỉ support cho global JS thôi

Để khẳng định cho giả định này thì ta test thử local xem nhé. Các bạn tạo cho mình file index.html ở bất kì đâu, ngoài folder làm việc nhé, vì ta chỉ để test thôi, với nội dung như sau:

<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> const url = "http://localhost:3003/assets/remoteEntry.js"; import(url).then((module) => { console.log("module", module); }); </script> </body>
</html>

Sau đó ta lưu lại (ví dụ lưu ra Desktop), sau đó kéo thả thẳng file đó vào trình duyệt, và kiểm tra console ta sẽ thấy như sau:

Ồ, load thành công module 🥸

Vậy từ đây ta có thể khẳng định là remoteEntry.js cho Vue MFE (Vite) nó được build thành JS module và do vậy ta cần update federation-utils.ts ở app-shell để support cho dạng này.

Ta update lại function loadRemoteModule như sau:

export async function loadRemoteModule( options: LoadRemoteModuleOptions
): Promise<any> { if (options.remoteEntry.startsWith('vite:')) { const module = await import(options.remoteEntry.split('vite:')[1]); (window as any)[options.remoteName] = module; } else { await loadRemoteEntry(options.remoteEntry); } return await lookupExposedModule<any>( options.remoteName, options.exposedModule );
}

Ở trên ta khai báo thêm support cho Vite, nếu url của remoteEntry bắt đầu bằng vite thì ta sẽ load nó như JS Module sau đó gán vào biến window (như ta làm với các mfe khác)

Sau đó ta quay lại app.service.ts update lại url của Vue:

//.....
const remoteModules = [ { remoteEntry: 'http://localhost:3001/remoteEntry.js', remoteName: 'angular_mfe_app', exposedModule: 'AngularAppLoader', }, { remoteEntry: 'http://localhost:3002/remoteEntry.js', remoteName: 'react_mfe_app', exposedModule: 'ReactAppLoader', }, { remoteEntry: 'vite:http://localhost:3003/assets/remoteEntry.js', remoteName: 'vue_mfe_app', exposedModule: 'VueAppLoader', },
];

Lưu lại và quay lại trình duyệt test thôi nào 💪💪

Mế, lại lỗi nữaaaaaaaaaaaaaaaaa:

Lý do ở đây là do webpack (app-shell đang dùng webpack), nó parse cái dynamic import và thay thế bằng 1 url khác dựa vào cái base url (url của app shell). Xem thêm ở đây: https://webpack.js.org/api/module-methods/#webpackignore

Do vậy ở đây ta cần nói với webpack là ignore đi đừng có parse gì cả:

export async function loadRemoteModule( options: LoadRemoteModuleOptions
): Promise<any> { if (options.remoteEntry.startsWith('vite:')) { const module = await import( /* webpackIgnore: true */ options.remoteEntry.split('vite:')[1] ); (window as any)[options.remoteName] = module; } else { await loadRemoteEntry(options.remoteEntry); } return await lookupExposedModule<any>( options.remoteName, options.exposedModule );
}

Sau đó ta lưu lại, F5 và login lại app-shell:

Lại lỗi nữaaaaaaaaaaaaaaaa 😭😭

Cơ mà may, lần này là lỗi khác 😂🤣🤣

Ở trên các bạn thấy rằng nó đang báo là không đọc được property framework (mà ta khai báo ở file loader.ts phía MFE). Ở đây thì ta cần check lại ngAfterViewInitapp-shell/src/app/main/main.component.ts:

async ngAfterViewInit() { for (const m of this.appService.authorized_modules) { loadRemoteModule(m).then((module) => { this.loaders.push(module.default); }); }
}

Ở trên các bạn thấy rằng, sau khi load được module lên thì ta truy cập vào thuộc tính default của module đó (vì module được build với webpack thì nó được export default ). Nhưng ở đây do cách Vite và cái plugin ta dùng mà ta chỉ cần truy cập thẳng vào module luôn thôi (các bạn có thể kiểm chứng bằng cách console.log(module)).

Do vậy ta cần sửa lại chút, nếu có .default ở module thì dùng nó, không thì thôi:

async ngAfterViewInit() { for (const m of this.appService.authorized_modules) { loadRemoteModule(m).then((module) => { if (module.default) { this.loaders.push(module.default); } else { this.loaders.push(module); } }); }
}

Cuối cùng ta lưu lại, quay lại app-shell F5 login:

Pòm pòm chíu chíu, Vue lên rồiiiiiiiii 🎉🎉🎉🎉🎉🎉

Cái logo Vite đang không load được, mình sẽ nói về vấn đề đó ở bài xử lý assets khi làm việc với MFE nhé 😉

Review và kết bài

Như các bạn thấy để support nhiều bundler hơn cho 1 kiến trúc frontend thì ta cần phải hiểu rõ từng thứ hoạt động như thế nào, cách xử lý ra sao với các bundler cho ra output ở các dạng khác nhau, đôi khi cũng chuối phết á 😆

Ở trong bài url của Vue mình dùng tiền tố vite:, ví dụ ta có nhiều bundler hơn thì ta có thể dùng kiểu esbuild: / rollup:...

Việc support được nhiều bundler hơn giúp cho kiến trúc MFE của chúng ta mạnh hơn, bằng cách cho developers (người dev MFE) nhiều sự lựa chọn hơn trong việc dùng tech stack mà họ muốn.

Chúc các bạn buổi tối vui vẻ và hẹn gặp các bạn vào những bài sau về Microfrontend 👋👋

Bình luận

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

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

Tại sao Rails lại dùng cả Webpack lẫn Sprocket?

Khi Rails 6 được ra mắt, có thể bạn đã từng tự hỏi. WTF, sao Webpack đã được add vào rồi, mà Sprocket vẫn tồn tại thế kia . Chẳng phải Webpack và Sprocket được dùng để giải quyết chung một công việc hay sao. Hoặc cả đây:.

0 0 56

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

Xây dựng Server-Side Rendering trong React theo phong cách của tôi

Chào bạn, sau một khoảng thời gian nghỉ tết ngắn, chúng ta hầu hết đã trở lại với công việc và mình cũng không ngoại lệ. Dư âm tết vẫn còn đấy nhưng cũng không thể quên nhiệm vụ, thế là hôm nay mình đã trở lại với một bài viết mới với một chủ đề tuy cũ nhưng lại mới với bản thân mình, đó là xây dựng

0 0 62

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

Tìm hiểu về JavaScript Module

Chắc hẳn ai trong chúng ta cũng đã từng sử dụng nhiều công cụ như là webpack, rollup, grunt, browserify,...; sử dụng những cú pháp module quen thuộc của CommonJS, AMD hay là ES6, nhưng có lẽ là chưa thực sự nhiều người đã nắm rõ về quá trình hình thành và mục đích tại sao chúng ta có những công cụ n

0 0 41

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

Webpack từ A đến Á: Clean Webpack Plugin

. Nếu các bạn đã làm qua các bài ví dụ trước ở trong series này thì sẽ thấy folder dist của chúng ta có rất nhiều file và trở nên khá lộn xộn. Webpack đã tạo các file và đặt chúng vào folder dist giúp bạn nhưng nó không theo dõi những file nào thực sự được sử dụng, sẽ có những file thừa file rác sin

0 0 29

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

1 số tool webpack hay ho bạn có thể sử dụng được trong dự án

Giới thiệu. Từ khóa webpack chắc không còn xa lạ với mọi người nữa, sức mạnh của nó đã được thể hiện rõ ràng qua số dự án đang chạy hay số sao (hiện tại đang là 56,8k lượt vote trên github, đó là chỉ là core của nó mà thôi).

0 0 27

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

Webpack từ A đến Á: HTML Webpack Plugin

Bài hôm nay chúng ta sẽ học về plugin html-webpack-plugin được dùng để sắp xếp các file html theo một trật tự nhất định, giúp tối ưu nội dung file html hơn. Link plugin: https://github.

0 0 54