+12

Xử lý CSS khi làm việc với kiến trúc Microfrontend

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

Hôm nay nhân một ngày đẹp trời ta tiếp tục quay trở lại với series Chập chững làm quen với Microfrontend nha 😍

Ở bài hôm nay ta sẽ cùng nhau tìm hiểu một số giải pháp để xử lý CSS trong kiến trúc Microfrontend nhé.

Cầm chèo và lên thuyền với mình thôi nàoooooo 🚣🚣

Vấn đề hiện tại

Vì ở kiến trúc MFE mục đích của ta là cho phép các MFE được tự do nhất có thể, tự do chọn tech stack, tự quản lý source code ở các repo riêng, tự deploy,...

4ac64722-af4f-4d60-a90f-d38c9e50d028.jpg

Và cuối cùng tất cả sự "tự do" đó đều được đem lên App Shell 😂

Vấn đề là App Shell là 1 môi trường chung, nếu ai cũng thích style theo ý mình "style của tôi phải là Global", "tôi thích dùng Material design, mọi người phải theo...", "Ông dùng Material thì kệ ông, team tôi dùng Ant design mấy năm rồi không thay đổi được":

1_29JcPhIL-6LiDyP6f6EfhQ.png

Và kết quả là CSS của cả App shell bị override loạn xạ, khó đoán trước, bug tùm lum, đang chạy ngon, mở thêm 1 MFE trên App Shell nữa xong style break hết 😂😂

Đây là một vấn đề khá nan giản vì nó là thứ mà MFE nào cũng cần phải có phải làm, làm thường xuyên, và nếu không để ý thì rất dễ conflict. MFE A thì cãi là hôm qua tôi deploy chạy ngon, tự nhiên nay không biết sao Style hỏng hết cả, check ra thì là do MFE B override global CSS 😒😒

Giờ ta sẽ cùng nhau rượt qua các giải pháp cho vấn đề này nhé 💪💪

Setup

Đầu tiên các bạn clone source code cho bài này ở đây nhé: https://github.com/maitrungduc1410/viblo-mfe-css-handling

Xong thì ta có như sau:

Screenshot 2024-10-29 at 11.46.26 PM.png

Sau đó ta chạy npm install cho từng folder nha

Tổng quan ở đây ta có:

Sau đó ta start project lên nhé:

# app-shell
npm start

# angular-app
npm start

# react-app
npm run dev

# vue-app
npm run build
npm run preview

Oke thì ta mở trình duyệt ở địa chỉ http://localhost:4200, login với user user/user, thấy như sau là được nha:

Screenshot 2024-10-29 at 11.51.45 PM.png

Giờ ta zô món chính nhé 😋😋

Các giải pháp để scope/isolate css

Ở đây ta sẽ tận dụng các feature của framework/library/bundler mà ta đang sử dụng để có thể "hạn chế" mức độ ảnh hưởng của style mà ta áp dụng cho từng MFE

Emulated CSS / Shadow DOM

Các bạn để ý rằng trên màn hình, ở mỗi MFE ta đều có 1 thẻ <h1> này:

Screenshot 2024-10-29 at 11.51.45 PM.png

Giờ ở angular-app ta sẽ đổi background của <h1> thành màu đỏ nhé, các bạn ở file angular-app/src/app/app.component.scss, và ta thêm vào:

:host {
  display: block;
  border: solid 2px rgb(183, 0, 255);
  padding: 10px;
}

// Thêm vào đây
h1 {
  background-color: red;
}

Sau đó ta lưu lại, quay trở lại trình duyệt F5 login sẽ thấy như sau:

Screenshot 2024-10-30 at 12.01.30 AM.png

He, ngon luôn, chỉ áp dụng cho mỗi Angular 😎😎 Tiện nhỉ. Ta Inspect check coi sao nhé:

Screenshot 2024-10-30 at 12.02.33 AM.png

Như các bạn thấy thì mặc định không cần nói gì là Angular đã tự "scope" CSS lại cho ta dùng CSS Attribute Selector, mỗi 1 component sẽ có 1 attribute riêng, không conflict với component khác.

Cái này trong Angular họ gọi là Emulated CSS hoặc View Encapsulation Emulated

Ta mở file angular-app/src/app/app.component.ts, thì cái View Encapsulation này có thể được thay đổi như sau:

import { Component, ViewEncapsulation } 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'],
  encapsulation: ViewEncapsulation.Emulated
})
export class AppComponent {
  angularLogo = angularLogo;

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

Mặc định thì encapsulation=ViewEncapsulation.Emulated, nếu ta đổi thành ViewEncapsulation.None là nó thành Global luôn và sẽ ảnh hưởng tới tất cả các thẻ <h1> có trên trang:

Screenshot 2024-10-30 at 12.08.22 AM.png

Ngoài ra encapsulation còn có 1 giá trị nữa đó là ViewEncapsulation.ShadowDom, khi ta để như này thì Angular sẽ biến component thành Web component, và vì bản chất của web component nó cũng đã isolate, nên CSS cũng sẽ không ảnh hưởng tới các MFE khác:

Screenshot 2024-10-30 at 12.37.42 AM.png

Nhưng khi dùng Web component vì nó có DOM "của riêng nó" nên sẽ có thể dẫn tới rất nhiều vấn đề đau đầu sau này, do vậy khi code Angular trừ khi có yêu cầu đặc biệt, còn không thì thường ta sẽ bỏ qua option encapsulation và để nó default về ViewEncapsulation.Emulated luôn

💡Dùng Emulated CSS hoặc ShadowDOM sẽ giúp giới hạn khả năng tác động của CSS được chỉ trong phạm vi component của MFE

Scoped CSS

Tiếp theo ở vue-app ta sẽ sửa <h1> thành màu xanh 🔵 nhé. Các bạn mở file vue-app/src/components/HelloWorld.vue

Ở thẻ <style> ta sửa lại như sau:

<style scoped>
.read-the-docs {
  color: #888;
}

h1 {
  background-color: blue;
}
</style>

Sau đó ta lưu lại và chạy (ở folder vue-app):

npm run build

Quay lại app shell login và ta sẽ thấy như sau:

Screenshot 2024-10-30 at 10.34.44 PM.png

Như các bạn thấy thì style của ta đã được áp dụng cho thẻ <h1> của chỉ riêng vue-app, và vì ta đang để là scoped nên Vue cũng sẽ "giới hạn" mức độ ảnh hưởng của CSS ở tại component HelloWorld, vẫn sử dụng CSS attribute selector như Emulated CSS phía Angular

Nếu ta bỏ scoped đi:

<style>
.read-the-docs {
  color: #888;
}

h1 {
  background-color: blue;
}
</style>

Lưu lại và chạy npm run build cho vue-app, sau đó quay lại app shell F5, ta sẽ thấy CSS sẽ apply global luôn:

Screenshot 2024-10-30 at 10.36.49 PM.png

💡Tương tự Emulated CSS, Scoped CSS trong VueJS cũng sẽ giúp hạn chế phạm vi tác động của CSS trong Vue Component

Trước khi làm tiếp thì các bạn để lại scoped rồi npm run build cho Vue đã nhé 😂

CSS Modules

Tiếp theo tới React, giờ nếu ta sửa file react-app/src/App.css , và set background-color cho thẻ h1 thành màu xanh lá 🟢:

/* ..... */

h1 {
  background-color: green;
}

Sau đó ta lưu lại và quay lại App shell F5 login sẽ thấy rằng css đã apply Global:

Screenshot 2024-10-30 at 10.53.36 PM.png

Từ đây ta cũng để ý rằng, mặc định với hầu hết các project React, dùng bundler gì đi chăng nữa thì CSS của chúng hầu như là có tác động Global

Và một trong những cách để tránh việc này đó là sử dụng CSS Modules.

Đầu tiên các bạn bỏ cái css cho thẻ h1 ở file App.css mà ta vừa thêm vào. Sau đó ta tạo thêm 1 file mới tên là App.module.css:

Screenshot 2024-10-30 at 10.58.31 PM.png

Nội dung như sau:

.title {
  background-color: green;
}

Sau ta import file này vào App.tsx:

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

import styles from "./App.module.css"; // -->> ở đây

const App = () => {
  return (
    <div className="content">
     {/* Ở đây */}
      <h1 className={styles.title}>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, quay trở lại App Shell F5:

Screenshot 2024-10-30 at 11.07.58 PM.png

Ở đây các bạn thấy tên CSS class đã được đổi thành filepath-tên CSS class-unique number. Và nó cũng sẽ chỉ có tác động trong phạm vi của component mà ta import vào.

Hầu hết thì với các bundler xịn xịn là CSS Module đều được built-in support cả, ta không cần cài gì thêm, chỉ cần đặt tên file theo đúng quy ước xxx.module.css (hoặc .scss), và import nó và sử dụng

Ở bên phía Vue (Vite) ta cũng có thể dùng CSS Module nhé:

<script setup lang="ts">
import { ref } from 'vue'

defineProps<{ msg: string }>()

const count = ref(0)
</script>

<template>
  <h1 :class="$style.title">{{ msg }}</h1>

  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
    <p>
      Edit
      <code>components/HelloWorld.vue</code> to test HMR
    </p>
  </div>

  <p>
    Check out
    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
      >create-vue</a
    >, the official Vue + Vite starter
  </p>
  <p>
    Learn more about IDE Support for Vue in the
    <a
      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
      target="_blank"
      >Vue Docs Scaling up Guide</a
    >.
  </p>
  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>

<style module>
.read-the-docs {
  color: #888;
}

.title {
  background-color: blue;
}
</style>

Ở trên ta chú ý ở thẻ <style thay vì dùng scoped thì ta dùng module, và khi cần tham chiếu tới tên class ta dùng $style.title, $style là biến đặc biệt ta không cần khai báo nó ở đâu, Vue lo hết 👍️👍️. Chạy lên cho kết quả như sau:

Screenshot 2024-10-30 at 11.18.02 PM.png

💡Sử dụng CSS Module cũng là một trong những cách khá phổ biến và hiệu quả trong việc giảm thiểu khả năng đụng độ style, khá oke trong kiến trúc MFE

Vấn đề về Performance

Emulated CSS/Scoped CSS sử dụng CSS Attribute Selectors, và cách này sẽ chậm hơn việc sử dụng class, ví dụ:

<style scoped>
h1 {
  background-color: blue;
}
</style>

Ở trên khi compile thì ta có kết quả như sau:

<style>
  h1[data-v-xxxxxx] {
    background-color: blue;
  }
</style>

Với đoạn code trên thì trình duyệt sẽ:

  • Tìm tất cả các thẻ <h1>
  • Với mỗi thẻ tìm được thì duyệt tất cả các attribute của nó và tìm data-v-xxxxxx. Mà số lượng attribute của từng HTML Element thì rất nhiều

Nếu so sánh với việc dùng class:

<style>
.title {
  background-color: blue;
}
</style>

<h1 class="title">Hello World</h1>

Ở đây thì trình duyệt chỉ cần duyệt tìm tất cả các element có class=title là được rồi.

Thực tế thì mình thấy vấn đề này thường không quá ảnh hưởng, nếu như app của các bạn lớn, hoặc đề cao vấn đề về performance thì lúc đó chắc ta mới cần quan tâm 😁, vì nhìn chung thì scoped css cũng khá là tiện😍

Nếu thật sự phải dùng CSS Global

Nếu trong trường hợp ta không thể (không muốn dùng) Emulated/Scoped CSS, và cần phải dùng Global CSS, thì ta nên để nó bên trong 1 class cha của riêng MFE đó.

Ví dụ:

<style lang="scss">
.vue-app {
  .title {
    background-color: blue;
  }
}
</style>

<template>
  <div class="vue-app">
    <h1 class="title">Hello world</h1>
  </div>
</template>

Hoặc nếu ta cần phải @import style thì ta có thể dùng kết hợp với scss như sau. Ở react-app ta cài plugin scss:

npm add @rsbuild/plugin-sass -D

Sau đó import plugin vào rsbuild.config.ts:

import { pluginSass } from '@rsbuild/plugin-sass';

//......
plugins: [pluginReact(), pluginSass()],

Sau đó ta rename file App.css -> App.scss, giữ nguyên nội dung như sau:

.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;
}

h1 {
  background-color: green;
}

Tiếp đó ta tạo AppNew.scss:

.react-app {
  @import './App.scss';
}

Cuối cùng ta update lại App.tsx 1 chút , import AppNew.scss vào và sử dụng:

import "./AppNew.scss";
import reactLogo from "../public/react.svg?inline";
import rsPackLogo from "../public/rspack.png";

const App = () => {
  return (
    <div className="react-app">
      <div className="content">
        <h1 className="title">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>
    </div>
  );
};

export default App;

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

Screenshot 2024-10-31 at 6.59.37 PM.png

Như các bạn đã thấy thì SCSS đã đưa tất cả css ta @import vào bên trong .react-app 😎😎

Dùng chung design system

Một trong những cách khác để giảm thiểu việc đụng độ CSS đó là "khuyến khích" các MFE dùng chung 1 design system (chung UI component library).

Ví dụ App shell import Tailwind/Radix UI và các MFE cứ thế follow theo dùng luôn. Kiểu này mình thấy khá ổn, giúp cho toàn bộ App shell + MFE thống nhất, gắn kết, UX nom oke hơn. (Mặc dù làm như này đúng là đang "giới hạn" MFE chút 😂)

Sử dụng nhiều UI component library

Ngoài những vấn đề về scoped/isolate CSS như mình nói ở trên, thì còn có 1 khả năng khác đó là mỗi một MFE lại dùng 1 UI component library khác nhau:

Microfrontend (4).jpg

Ví dụ như ở trên ta có 3 MFE, mỗi MFE thì lại dùng 1 UI component library khác nhau, cái thì Prime, cái thì Material, cái lại Ant design. Có những library người ta không không cần import css global (ví dụ Ant design), css của họ được append runtime luôn, nhưng có những cái yêu cầu ta import global ví dụ PrimeNG. Đôi khi, đôi chỗ cũng sẽ có thể gây conflict. Rất may là hầu hết các library họ cũng đã có prefix cho tất cả các css class của họ, ví dụ:

  • primeNG: p-dialog
  • Vuetify: v-btn
  • Ant design: ant-btn

Nhưng thực tế thì nếu để mỗi MFE dùng 1 UI component library khác nhau thì trông cái App shell sẽ khá là hổ lốn, thường là ta sẽ dùng chung 1 design system, ví dụ dùng toàn bộ của Prime (PrimeNG, PrimeReact, PrimeVue), hay dùng Tailwind. CSS import 1 lần global ở App shell và các MFE cứ thế dùng luôn.

Nhìn chung đây cũng là một vấn đề khá phổ biến, gặp đâu ta xử lý đó, và App shell nên có 1 số "chuẩn" để ít nhất các MFE follow theo chút, chứ cũng không nên để tự do quá 😂

Kết bài

Xử lý CSS trong kiến trúc MFE sao cho hợp lý, vẫn tạo sự thoải mái cho anh em dev MFE và không bị conflict cũng là một bài toán khá hay, giải pháp có nhiều và phụ thuộc vào business của từng người, các bạn tham khảo bài viết và vận dụng sao cho linh hoạt nhé.

Chú ý thêm nữa là trong bài mình lấy 3 framework/library Vue/React/Angular, chúng có những tính năng riêng để support CSS, với các framework/lib khác thì cũng sẽ (có thể) có những tính năng tương tự ta áp dụng cho phù hợp nha

Chúc các bạn ngủ ngon và hẹn gặp lại vào những bài sau 🌙🌙


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í