+18

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

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:

Screenshot 2024-09-22 at 5.09.09 PM.png

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:

Screenshot 2024-09-22 at 5.11.43 PM.png

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:

Screenshot 2024-09-22 at 5.12.31 PM.png

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:

Screenshot 2024-09-22 at 5.20.39 PM.png

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

Screenshot 2024-09-22 at 5.21.41 PM.png

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

Screenshot 2024-09-22 at 5.22.56 PM.png

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:

Screenshot 2024-09-22 at 5.31.34 PM.png

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é:

Screenshot 2024-09-22 at 5.34.57 PM.png

Ở 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é:

Screenshot 2024-09-22 at 5.37.14 PM.png

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

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

Screenshot 2024-09-22 at 5.38.01 PM.png

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

Screenshot 2024-09-22 at 5.38.35 PM.png

Ầu sết, lỗi 😢😢

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

Screenshot 2024-09-22 at 5.40.56 PM.png

Ầ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:

Screenshot 2024-09-22 at 5.43.13 PM.png

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:

Screenshot 2024-09-22 at 5.51.16 PM.png

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:

Screenshot 2024-09-22 at 5.53.26 PM.png

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é:

Screenshot 2024-09-22 at 6.04.50 PM.png

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é:

Screenshot 2024-09-22 at 6.07.35 PM.png

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

Cuối cùng là ta tạo file app-vue/src/loader.ts với nội dung như sau:

import App from './App.vue';

export default {
  framework: 'vue',
  component: App
}

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é:

Screenshot 2024-09-22 at 6.26.11 PM.png

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

Screenshot 2024-09-22 at 6.25.38 PM.png

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é:

Screenshot 2024-09-22 at 6.27.16 PM.png

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::

Screenshot 2024-09-22 at 6.28.49 PM.png

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:

Screenshot 2024-09-22 at 6.30.27 PM.png

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

Screenshot 2024-09-22 at 6.31.51 PM.png

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

Screenshot 2024-09-22 at 6.32.04 PM.png

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 à:

Screenshot 2024-09-22 at 6.33.36 PM.png

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

Screenshot 2024-09-22 at 6.34.11 PM.png

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:

Screenshot 2024-09-22 at 6.38.23 PM.png

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:

Screenshot 2024-09-22 at 6.43.41 PM.png

Ồ, 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:

Screenshot 2024-09-22 at 6.49.03 PM.png

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:

Screenshot 2024-09-22 at 6.51.53 PM.png

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:

Screenshot 2024-09-22 at 6.56.27 PM.png

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 👋👋


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.