+6

Làm việc với static assets trong kiến trúc Microfrontend

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

Tiếp tục với series Chập chững làm quen với Microfrontend, ở bài hôm nay ta sẽ cùng nhau tìm hiểu cách xử lý static assets: images, files, SVG,...trong kiến trúc microfrontend nhé

Mặc áo phao rồi lên thuyền với mình thôiii ⛵️⛵️

Setup

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

Ở đó ta có 4 folder (1 app shell, 3 MFE):

Screenshot 2024-09-24 at 10.54.32 PM.png

Các bạn chạy npm install ở từng folder để cài dependencies nhé

Vọc vạch

Đầu tiên ta start react-app lên coi thử nhé, các bạn chạy command sau ở folder react-app:

npm run dev

Sau đó ta có thể truy cập từ trình duyệt ở địa chỉ http://localhost:3002 sẽ thấy như sau:

Screenshot 2024-09-25 at 10.12.17 PM.png

Giờ các bạn để ý ở folder public mình có để 2 file svg và png:

Screenshot 2024-09-25 at 10.14.44 PM.png

Giờ ta thử display 2 file ảnh theo như cách ta vẫn làm ở project bình thường nhé 😂, a sửa lại file src/App.tsx như sau:

import "./App.css";

const App = () => {
  return (
    <div className="content">
      <h1>Rsbuild with React</h1>
      <p>Start building amazing things with Rsbuild.</p>
      <div>
        <img src="/react.svg" alt="React Logo" width={50} />
      </div>
      <div>
        <img src="/rspack.png" alt="React Logo" height={100} />
      </div>
    </div>
  );
};

export default App;

Sau đó ta lưu lại, quay lại trình duyệt sẽ thấy nó tự refresh:

Screenshot 2024-09-25 at 10.21.39 PM.png

Rất oke không có vấn đề gì nhỉ 😁😁 Giờ ta lắp nó vào kiến trúc MFE xem nhé.

Ta start app-shell lên nha:

npm start

Sau đó truy cập ở địa chỉ http://localhost:4200, login với user/user:

Screenshot 2024-09-25 at 10.25.12 PM.png

Ủa???? 2 ảnh của mình đâu rồi ta???? 🧐🧐

Ta Inspect check Elements xem nhé:

Screenshot 2024-09-25 at 10.27.04 PM.png

Ảnh ở đây rồi mà???

Thử nháy chuột phải copy link xem link ảnh thật sự nó là gì nhé:

Screenshot 2024-09-25 at 10.28.06 PM.png

http://localhost:4200/react.svg

Ầuuuu, vậy là nó đang trỏ tới app-shell, mà hiển nhiên ta làm gì có lưu ảnh ở app-shell đâu, nó ở react-app cơ mà 🤔🤔

Âu cây, giờ ta sẽ tìm hiểu cách làm đúng nha, mình sẽ chia thành các ví dụ với các bundler khác nhau để tí nữa ta xem có gì khác nhau giữa chúng nhé

Rsbuild

Đầu tiên ta sẽ làm với react-app dùng rsbuild (là 1 tool dựa bên bundler rspack)

Thì như các bạn thấy nếu ta chỉ nói:

<img src="/react.svg" alt="React Logo" width={50} />

Thì khi load lên (runtime) cái đường dẫn kia nó sẽ ăn theo domain của app-shell, do vậy ta phải nói rõ cụ thể đường dẫn của nó ra, ví dụ:

http://localhost:3002/react.svg

Ủa vậy không lẽ deploy ra domain nào thì phải thay domain đó???

https://example.com/react.svg

https://test.com/react.svg

Xong cứ phải build đi build lại mỗi lần đổi domain ý hử??? 🧐

Thế thì ta nhớ tới một pattern mà anh em React hay dùng (nhất là bên React native khi cần import ảnh), đó là ta sẽ import vào file JS thì khi đó nó sẽ trả về cái public URL được resolve ở runtime

Cụ thể ta update lại code ở react-app/src/App.tsx như sau:

import "./App.css";
import reactLogo from "../public/react.svg";
import rsPackLogo from "../public/rspack.png";

const App = () => {
  return (
    <div className="content">
      <h1>Rsbuild with React</h1>
      <p>Start building amazing things with Rsbuild.</p>
      <div>
        <img src={reactLogo} alt="React Logo" width={50} />
      </div>
      <div>
        <img src={rsPackLogo} alt="React Logo" height={100} />
      </div>
    </div>
  );
};

export default App;

Sau đó ta lưu lại, quay trở lại app-shell F5 và login lại sẽ thấy:

Screenshot 2024-09-25 at 10.37.51 PM.png

Ảnh lên ngon nghẻ roàiiii 🎉🎉🎉

Như các bạn thấy thì asset sau khi ta import vào file JS thì chúng thực tế được bundle và resolve thành những cái path khác nhau

Mặc định thì assets size < 4KB thì sẽ được inline thẳng thành base64 luôn, lớn hơn thì giữ URL. Xem ở đây: https://rsbuild.dev/guide/basic/static-assets#import-results

Ta có thể force inline bằng cách:

import reactLogo from "../public/react.svg?inline";

Thử update lại code như sau nhé:

import "./App.css";
import reactLogo from "../public/react.svg?inline";
import rsPackLogo from "../public/rspack.png";

const App = () => {
  return (
    <div className="content">
      <h1>Rsbuild with React</h1>
      <p>Start building amazing things with Rsbuild.</p>
      <div>
        <img src={reactLogo} alt="React Logo" width={50} />
      </div>
      <div>
        <img src={rsPackLogo} alt="React Logo" height={100} />
      </div>
    </div>
  );
};

export default App;

Sau đó lưu lại và ta sẽ thấy ảnh đã được inline luôn thành base64

Screenshot 2024-09-25 at 10.42.44 PM.png

Rsbuild xử lý rất nhiều loại định dạng cho assets, các bạn xem thêm ở đâu nhé: https://rsbuild.dev/guide/basic/static-assets#assets-format

Quá đơn giản ý nhờ, chỉ cần để ý chút thôi 😎, các loại khác ta cũng làm tương tự nhé: PNG, JPG, JSON, video,...

Nhớ là luôn dùng import cho assets trong kiến trúc Microfrontend nhé 😉

Vite

Với ý tưởng tương tự ta tiếp tục start vue-app lên và xem xử lý asset bên Vite có gì khác không nhé:

npm run dev

Sau đó ta mở trình duyệt ở địa chỉ http://localhost:5173thấy như sau:

Screenshot 2024-09-25 at 10.47.11 PM.png

Vẫn tương tự bên React, ta thêm ảnh vào nhé, các bạn sửa file vue-app/src/App.vue:

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import viteLogo from '../public/vite.svg'
import vueLogo from '../public/vue.png'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <!-- Logo here -->
       <img class="logo" :src="viteLogo" alt="Vite Logo" />
      </a>
      <a href="https://vuejs.org/" target="_blank">
        <!-- Logo here -->
        <img class="logo" :src="vueLogo" alt="Vite Logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

Sau đó lưu lại và quay ra trình duyệt F5:

Screenshot 2024-09-25 at 10.50.50 PM.png

Thấy oke 👍️👍️, giờ ta quay sang app-shell xem lúc load nó lên như 1 MFE thì có oke không nhé.

Nhưng trước mắt ta cần build project vue-app và preview nó trước, vì Vite nó không gen ra file remoteEntry.js ở dev mode, các bạn chạy lần lượt 2 command sau:

npm run build

npm run preview

Oke thì ta sang app-shell, F5 và login lại nhé:

Screenshot 2024-09-25 at 10.56.14 PM.png

Ủa sao với Vue thì logo lên có 1 cái, cái còn lại mất tiêu zợ 🧐🧐

Thì ta để ý rằng, cái logo mà lên oke thì nó đang inline base64 (tức là nó được convert thành base64 lúc bundle và inline thẳng vào code), nên nó luôn có sẵn ở đó và display luôn

Tương tự rsbuild, image size nhỏ hơn 4KB sẽ được Vite inline luôn. Xem thêm: https://vitejs.dev/config/build-options.html#build-assetsinlinelimit

Còn cái logo Vue thì ta lại gặp vấn đề là nó đang không có địa chỉ rõ ràng (http...), do vậy nó mặc định trỏ về app shell

Cái này thì Vite hơi khác rsbuild đó nha 😁, với Vite thì folder public là 1 folder đặc biệt, và những thứ mà import được vào file JS thì chỉ nên ở bên trong src

Nếu ta để ý terminal nơi mà ta chạy npm run dev cho vue-app sẽ thấy có warning:

Screenshot 2024-09-25 at 10.59.07 PM.png

Cực kì cụ thể và rõ ràng 😍, Vite bảo ta rằng ta nên để nó vào src. Do vậy ta tạo 1 folder để lưu các assets nha, đặt tên là assets luôn cho cụ thể:

Screenshot 2024-09-25 at 11.01.08 PM.png

Sau đó ta move luôn 2 file logo vào trong đó nhé. Sau đó ta sửa lại code nha:

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import viteLogo from './assets/vite.svg'
import vueLogo from './assets/vue.png'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <!-- Logo here -->
       <img class="logo" :src="viteLogo" alt="Vite Logo" />
      </a>
      <a href="https://vuejs.org/" target="_blank">
        <!-- Logo here -->
        <img class="logo" :src="vueLogo" alt="Vite Logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

Lưu lại build và preview nhé:

npm run build

npm run preview

Sau đó ta quay lại app-shell, F5 và test lại nhé:

Screenshot 2024-09-25 at 11.07.23 PM.png

Ủa ủa???? Vẫn không lên????? oắt đờ héo? Còn chỗ nào chưa đúng nữa à??? 🧐🧐

Thì thực tế đây là behaviour của Vite, nó vẫn không bundle ra full URL cho chúng ta đâu, nhiều người kêu lắm: https://github.com/vitejs/vite/issues/3522 😂

Nhưng trên trang chủ của Vite cũng có hướng dẫn cách đơn giản để tạo full URL cho assets: https://vitejs.dev/guide/assets#new-url-url-import-meta-url

Ta sửa lại code như sau nhé:

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'

const viteLogo = new URL('./assets/vite.svg', import.meta.url).href
const vueLogo = new URL('./assets/vue.png', import.meta.url).href

</script>

//......

Ở đây ta dùng import.meta.urlnó sẽ luôn trỏ về domain hiện tại mà assets được host, nên ví dụ mà ta có deploy vue-app ở các domain khác nhau thì nó cũng tự update

Giờ ta quay lại app-shell, F5 và test lần nữa nhé:

Screenshot 2024-09-25 at 11.13.03 PM.png

Pòm pòm chíu chíu 🎉🎉. Lên roàiii

Webpack

Thí sinh tiếp theo là angular-app với Webpack 🤣🤣

Ta start nó lên nhé:

npm start

Sau đó ta quay lại app-shell, F5 login và kiểm tra xem Angular lên oke chưa đã nha:

Screenshot 2024-09-26 at 9.59.49 PM.png

Ở trong angular-app mình có lưu 1 cái logo Angular ở assets. Bây giờ với kinh nghiệm ta học được từ đầu bài đến giờ, ta sẽ không mắc lại sai lầm import asset nữa 😎😎

Ta mở file angular-app/src/app/app.component.ts và thực hiện import file ảnh vào:

import { Component } from '@angular/core';
import angularLogo from '../assets/angular.png';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  angularLogo = angularLogo;
}

Sau đó ở file app.component.html ta display file ảnh này:

<h1>Hello from Angular</h1>
<img [src]="angularLogo" alt="Angular Logo" />

Ta lưu lại, và pòmmmmmmmmmmmmmmmmmmmmmm, lỗi compile 🤬🤬🤬:

Screenshot 2024-09-26 at 10.10.22 PM.png

Lỗi gì vậy nhờ, để ý lắm rồi mờ 🧐🧐

Ở lỗi in ra có 1 cái link ta click vào thì thấy đoạn đầu người ta nói như sau: Out of the box, webpack only understands JavaScript and JSON files

Ý bảo mặc định thì Webpack nó chỉ hiểu code Javascript và JSON thôi, nếu ta cần load những file khác (ví dụ ảnh) thì ta cần phải có những cái loader tương ứng. Hơn nữa ta đang làm việc với static assets, thì Webpack nó có support cho cái này luôn, và nó gọi là Asset Modules

Giờ ta sửa lại file angular-app/webpack.config.js như sau nhé:

const { ModuleFederationPlugin } = require("webpack").container;

/** @type {import('webpack').Configuration} */
module.exports = {
  output: {
    publicPath: "auto",
    uniqueName: "angular_app",
    scriptType: "text/javascript",
    assetModuleFilename: 'images/[hash][ext][query]',
  },
  optimization: {
    runtimeChunk: false,
  },
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: "asset",
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "angular_mfe_app",
      filename: "remoteEntry.js",
      exposes: {
        AngularAppLoader: "./src/app/loader.ts",
      },
      shared: {
        "@angular/core": { singleton: true },
        "@angular/common": { singleton: true },
        "@angular/router": { singleton: true },
        "@angular/common/http": { singleton: true },
      },
    }),
  ],
  devServer: {
    // this is to remove the error "ws://localhost:4200/ws failed" in the console when we disable live reload
    // also this solve websocket proxy issue in local
    webSocketServer: false,
  },
};

Ở trên mình thêm vào 2 thứ:

  • 1 là module.rules ở đó mình khai báo các file có đuôi như trên thì sẽ coi là asset

  • 2 là assetModuleFilename: đây là đường dẫn mà webpack nó sẽ output ra file asset của chúng ta

Giờ ta lưu lại và và start lại angular-app nhé:

// kill terminal angular-app đi trước

npm start

Sau đó quay lại app-shell F5 login và ta sẽ thấy như sau:

Screenshot 2024-09-26 at 10.20.52 PM.png

Logo Angular lên rồi 🥰🥰🥰, và ở đây nó được convert thành base64 + inline luôn.

Mặc định với các file < 8KB thì Webpack nó sẽ inline base64 luôn, còn lớn hơn 8KB thì sẽ giữ URL http://localhost...../image.png

Nếu ta chỉ muốn dùng URL thì update webpack.config.js đổi module.rules.type -> asset/resource nhé (hoặc asset/inline nếu ta chỉ muốn inline base64), ở đây mình để asset để Webpack nó tự quyết định

Tiếp theo, vẫn ở folder assets, ta tạo tiếp 1 file test.json và import vào xem nó có oke không nhé:

Screenshot 2024-09-26 at 10.24.36 PM.png

Nội dung như sau:

{
  "a": 1
}

Giờ ta quay lại app.component.ts và import file JSON vào:

import { Component } from '@angular/core';
import angularLogo from '../assets/angular.png';
import json from '../assets/test.json';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  angularLogo = angularLogo;

  ngOnInit() {
    console.log('json', json);
  }
}

Thì ở đây ta thấy nó in ra lỗi như sau:

Screenshot 2024-09-26 at 10.25.40 PM.png

Ủa sao bên trên nói Webpack nó hiểu Javascript và JSON mà???? 😝😝

Thì thực tế lỗi này throw ra bởi Typescript, mặc định TS nó không support import JSON đâu, mà ta cần enable tính năng đó lên, ta update file angular-app/tsconfig.json, như sau nhé:

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "ES2022",
    "module": "ES2022",
    "useDefineForClassFields": false,
    "lib": [
      "ES2022",
      "dom"
    ],
    "resolveJsonModule": true
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

Ở trên ta thêm "resolveJsonModule": true vào, ta lưu lại sau đó tắt terminal của angular-app đi lần nữa và start lại:

npm start

Pòm 🔫:

Screenshot 2024-09-26 at 10.29.33 PM.png

Lại lỗi nữaaaaaaaaa 🥲🥲

Tiếp tục ta lại làm như lỗi nó bảo, đấy là thêm allowSyntheticDefaultImports vào tsconfig.json:

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "ES2022",
    "module": "ES2022",
    "useDefineForClassFields": false,
    "lib": [
      "ES2022",
      "dom"
    ],
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

Lưu lại và khởi động angular-app lại lần nữa, cầu trời cho nó build thành công 🙏🙏

Screenshot 2024-09-26 at 10.35.36 PM.png

Build ngon rồiiiiiii 🎉🎉🎉

Sau đó ta quay lại app-shell, F5 login sẽ thấy ở console in ra giá trị của file JSON ta import nhé:

Screenshot 2024-09-26 at 10.36.18 PM.png

Review cuối ngày

Phewwww, như các bạn thấy, với mỗi bundler thì chúng hoạt động khác nhau, nên MFE nào dùng bundler nào thì ta cần hiểu bundler đó để có thể cấu hình cho đúng.

Tổng hợp lại bài hôm nay trong 1 câu: khi làm việc với Microfrontend mà cần xử lý static assets, thì ta phải nhớ luôn import file asset vào JS/TS rồi mới được dùng, không được dùng trực tiếp ở folder public 🙏

Nếu có lỗi không hiển thị được asset, thì Inspect lên xem đường dẫn của nó là gì, có đúng không trước đã, nếu sai thì phải check lại cấu hình của cái bundler mà mình đang dùng.

Chúc các bạn buổi tối vui vẻ, ta hẹn nhau gặp lại vào các bài tới nha 🥰🥰


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí