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,...
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":
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:
Sau đó ta chạy npm install
cho từng folder nha
Tổng quan ở đây ta có:
- App shell + angular-app: Webpack
- react-app: rsbuild + https://module-federation.io
- vue-app: Vite
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:
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:
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:
He, ngon luôn, chỉ áp dụng cho mỗi Angular 😎😎 Tiện nhỉ. Ta Inspect check coi sao nhé:
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:
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:
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:
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:
💡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:
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
:
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:
Ở đâ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:
💡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:
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:
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