Thực Hành Micro Frontends
- 15-01-2024
- Toanngo92
- 0 Comments
Mục lục
Micro Frontend là gì?
Micro Frontend là một khái niệm xuất phát từ việc áp dụng nguyên lý Microservices vào lĩnh vực Frontend của ứng dụng. Trong thời đại hiện nay, ứng dụng Single Page Apps (SPAs) đã trở nên phổ biến với nhiều tính năng và độ phức tạp cao. Thường xuyên, chúng được tích hợp với kiến trúc Microservices ở tầng backend.
Tuy nhiên, theo thời gian phát triển, các ứng dụng SPAs này trở nên cồng kềnh và khó bảo trì, thường được mô tả như Frontend Monolith. Để giải quyết vấn đề này, Micro Frontends xuất hiện như một giải pháp. Khái niệm này giúp phân tách ứng dụng thành các phần nhỏ, mỗi phần tương ứng với một tính năng cụ thể. Mỗi tính năng có thể được phát triển độc lập bởi một nhóm phát triển riêng biệt, tăng khả năng linh hoạt và quản lý của hệ thống Frontend.
Monolithic Frontends
Micro Frontends
Phương pháp để áp dụng Micro Frontends
Có một số phương pháp để triển khai Micro Frontends, mỗi phương pháp mang lại những ưu điểm và giới hạn khác nhau.
- Iframe:
- Ưu điểm: Dễ triển khai.
- Giới hạn: Gặp khó khăn trong việc điều hướng, thực thi mã JavaScript từ Host App.
<iframe src="microfrontend-app"></iframe>
2. Proxy như nginx:
- Ưu điểm: Yêu cầu ứng dụng phải độc lập.
- Giới hạn: Khi chuyển từ ứng dụng này sang ứng dụng khác, có thể xảy ra reload giống như ứng dụng client-server thông thường.
location /mailbox {
proxy_pass http://microfrontend-mailbox;
}
location /calendar {
proxy_pass http://microfrontend-calendar;
}
3. Web Components:
Ưu điểm: Sử dụng các công nghệ không mới như Angular Elements, Stencil. Có thể tạo ra các phần tử có thể sử dụng như thẻ HTML ở bất kỳ framework nào (Framework Agnostic).
// Ví dụ với Angular Elements
const myCustomElement = createCustomElement(MyComponent, { injector });
customElements.define('my-custom-element', myCustomElement);
4. Orchestrator Frameworks (Webpack 5 and Module Federation, piral, luigi, single-spa):
- Ưu điểm: Sử dụng các framework như Webpack 5 và Module Federation, piral, luigi, single-spa để quản lý và tương tác giữa các Micro Frontends.
// Ví dụ với Webpack 5 Module Federation
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
// ...
}),
],
};
Mỗi phương pháp đều có những điểm mạnh và yếu riêng, tùy thuộc vào yêu cầu cụ thể của dự án mà bạn có thể lựa chọn phương pháp phù hợp.
Develop Email Client Micro Frontends
Ứng dụng Email Client này được phát triển bởi hai nhóm là nhóm quản lý hộp thư (mailbox) và nhóm lịch (calendar), nhằm mục đích chia nhỏ ứng dụng thành hai phần để dễ dàng quản lý và phát triển.
Trong quá trình phát triển, nhóm lịch có thể thêm một widget mới và nhúng nó vào trang của nhóm quản lý hộp thư. Điều này được thực hiện thông qua việc sử dụng Custom Elements, giúp tạo ra những thành phần có thể nhúng vào bất kỳ trang HTML nào.
Mã nguồn mẫu cung cấp một cái nhìn tổng quan về cách triển khai Micro Frontends, giúp bạn hiểu rõ cách mà các nhóm có thể làm việc độc lập và tích hợp linh hoạt vào ứng dụng chung.
Shell or Host app
Để các ứng dụng Micro có thể hoạt động cùng một ứng dụng, chúng ta cần một cái gọi là Shell (hoặc còn được biết đến là ứng dụng chủ). Shell sẽ thực hiện một số công việc như thiết lập định tuyến (routing), quản lý trạng thái chung (shared state), và các công việc khác. Quá trình tạo ra ứng dụng Shell có thể ảnh hưởng đến công nghệ cụ thể mà chúng ta sử dụng cho các ứng dụng Micro.
Ví dụ, nếu chúng ta quyết định sử dụng Angular hoặc React làm ứng dụng Shell, thì các ứng dụng Micro sẽ cần một lớp bao để có thể chạy trên nền của ứng dụng đó. Điều này là do hệ thống định tuyến (routing) của mỗi framework là đặc thù cho từng framework cụ thể. Do đó, để có thể định tuyến đúng và hiển thị đúng thành phần, các ứng dụng Micro phải tuân theo các ràng buộc của framework đó.
Chuẩn bị
Trong bài demo này, chúng ta sẽ sử dụng Webpack 5, một phiên bản mới của nó đã giới thiệu một API tiên tiến là Module Federation, giúp cho việc phát triển Micro Frontend trở nên dễ dàng hơn. Đồng thời, chúng ta sẽ sử dụng Angular v11 (trong thời điểm này là Release Candidate) để tạo các ứng dụng.
Đầu tiên, chúng ta cần tạo một ứng dụng Shell bằng cách sử dụng lệnh sau:
npx @angular/cli@14 new ngft-email-client --create-application=false
Sau khi tạo xong project, chúng ta sẽ tạo thêm 3 ứng dụng khác: 1 ứng dụng Shell và 2 ứng dụng từ xa (mailbox, calendar).
Lưu ý: chúng ta sử dụng Router và SCSS cho cả 3 ứng dụng để đồng nhất.
npx ng generate application shell
# ? Would you like to add Angular routing? Yes
# ? Which stylesheet format would you like to use? SCSS
npx ng generate application mailbox
# ? Would you like to add Angular routing? Yes
# ? Which stylesheet format would you like to use? SCSS
npx ng generate application calendar
# ? Would you like to add Angular routing? Yes
# ? Which stylesheet format would you like to use? SCSS
Ngoài ra, để sử dụng custom webpack config, chúng ta cần cài đặt thêm một package là @angular-builders/custom-webpack
.
npm i -D @angular-builders/custom-webpack@14
File package.json của chúng ta sẽ có dạng như sau:
{
"name": "acme-email-client",
"scripts": {
"start:shell": "ng serve --project=shell",
"start:mailbox": "ng serve --project=mailbox",
"start:calendar": "ng serve --project=calendar"
},
"dependencies": {
"@angular/animations": "^14.2.0",
"@angular/common": "^14.2.0",
"@angular/compiler": "^14.2.0",
"@angular/core": "^14.2.0",
"@angular/forms": "^14.2.0",
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^14.0.1",
"@angular-devkit/build-angular": "^14.2.3",
"@angular/cli": "~14.2.3",
"@angular/compiler-cli": "^14.2.0",
"typescript": "~4.7.2"
}
}
Chúng ta sẽ cấu hình các cổng để chạy ng serve
cho từng ứng dụng trong file angular.json.
Ví dụ, shell sẽ chạy ở cổng 5200, và chúng ta thêm cấu hình như sau:
{
...
"projects": {
"shell": {
...
"architect": {
...
"serve": {
...
"options": {
"port": 5200
},
...
}
}
}
}
}
Sau đó, chúng ta thực hiện tương tự cho mailbox (5300) và calendar (5400).
Bật tính năng Module Federation
Để kích hoạt tính năng này, chúng ta cần sử dụng custom webpack như sau: Trước hết, bạn tạo các tệp cấu hình webpack, sau đó thay thế builder mặc định trong angular.json.
Ví dụ, chúng ta tạo 2 tệp webpack.config.js và webpack.prod.config.js để sử dụng cho 2 môi trường là development và production trong thư mục projects/shell. Sau đó, chúng ta sẽ thay thế nó trong angular.json:
- Thay thế
@angular-devkit/build-angular
bằng@angular-builders/custom-webpack
. - Thêm cấu hình webpack mà chúng ta vừa tạo.
Dưới đây là một phần của file angular.json.
{
"projects": {
"shell": {
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "projects/shell/webpack.config.js"
}
},
"configurations": {
"production": {
"customWebpackConfig": {
"path": "projects/shell/webpack.prod.config.js"
}
}
}
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"port": 5200,
"publicHost": "http://localhost:5200/"
},
"configurations": {
"production": {
"browserTarget": "shell:build:production"
},
"development": {
"browserTarget": "shell:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
}
Sau đó, chúng ta sẽ thực hiện các bước tương tự cho các dự án mailbox và calendar.
Config Shell
Để kích hoạt Module Federation, chúng ta cần cấu hình shell như sau:
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
output: {
publicPath: "auto", // we setup the `publicHost` in `angular.json` file
uniqueName: "shell",
},
optimization: {
runtimeChunk: false,
},
experiments: {
// Allow output javascript files as module source type.
outputModule: true,
},
plugins: [
new ModuleFederationPlugin({
name: "shell",
library: {
// because Angular v14 will output ESModule
type: "module",
},
remotes: {
mailbox: "http://localhost:5300/remoteEntry.js",
calendar: "http://localhost:5400/remoteEntry.js",
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};
Trong đoạn code trên, shell sẽ chạy ở port 5200 và chúng ta cần một unique name cho mỗi ứng dụng. Vì shell sẽ trỏ đến 2 ứng dụng remote, nên chúng ta sẽ cấu hình tương ứng cho cả 2 ứng dụng remote ở đây.
Vì chúng ta đang sử dụng các micro app bằng Angular, nên chúng ta có thể chia sẻ các phần code. Trong cấu hình trên, chúng ta đã chia sẻ 3 package.
Bây giờ, chúng ta có thể thêm cấu hình cho routing của shell để trỏ đến 2 micro app:
const routes: Routes = [
{
path: "mailbox",
loadChildren: () =>
import("mailbox/MailboxModule").then((m) => m.MailboxModule),
},
{
path: "calendar",
loadChildren: () =>
import("calendar/CalendarModule").then((m) => m.CalendarModule),
},
];
Tuy nhiên, có một vấn đề xuất phát từ việc 2 đường dẫn trên không thật sự tồn tại trong app shell, vì chúng là đường dẫn ảo. Do đó, chúng ta cần thông báo cho TypeScript biết rằng chúng có thật sự tồn tại. Điều này có thể được giải quyết bằng cách tạo một tệp types.d.ts trong thư mục src:
declare module "mailbox/MailboxModule";
declare module "calendar/CalendarModule";
Bây giờ bạn có thể chạy shell để xem kết quả:
yarn start:shell
Tuy nhiên, ứng dụng của chúng ta khi chạy có thể báo lỗi như sau: “Uncaught Error: Shared module is not available for eager consumption.” Điều này xảy ra do chúng ta đang chia sẻ các package. Do đó, chúng ta cần cấu hình thêm một số điều để khởi tạo ứng dụng. Chúng ta thực hiện như sau:
Tạo một file chứa phần import và gọi dynamic import:
bootstrap.ts
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
main.ts
import("./bootstrap").catch((err) => console.error(err));
Vậy là ứng dụng của bạn đã chạy thành công.
Config Remotes app
Nếu chúng ta muốn điều hướng vào hai ứng dụng micro kia, chúng ta cũng cần cấu hình tương tự, nhưng có một số điều chỉnh, vì những ứng dụng đó là ứng dụng remote.
Dưới đây là cấu hình dành cho ứng dụng mailbox:
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
output: {
publicPath: "auto", // we setup the `publicHost` in `angular.json` file
uniqueName: "mailbox",
},
optimization: {
runtimeChunk: false,
},
experiments: {
// Allow output javascript files as module source type.
outputModule: true,
},
plugins: [
new ModuleFederationPlugin({
name: "mailbox",
filename: "remoteEntry.js",
library: {
// because Angular v14 will output ESModule
type: "module",
},
exposes: {
"./MailboxModule": "projects/mailbox/src/app/mailbox/mailbox.module.ts",
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};
Trong đoạn mã trên, chúng ta cấu hình phần output giống như ứng dụng shell. Phần khác biệt chủ yếu là ở cấu hình cho ModuleFederationPlugin.
Chúng ta cần cấu hình một số trường như name, library, và đặc biệt là fileName cần phải trùng khớp với phần shell mà chúng ta đã cấu hình trước đó (ở đây là remoteEntry.js) và phần exposes.
Phần exposes cho phép chúng ta cấu hình những gì sẽ được public ra bên ngoài. Mỗi khóa của nó nên tuân theo cú pháp ESM (ECMAScript Module) trong Node 14.
Chế Độ Độc Lập Cho Ứng Dụng Remote: Ở đây, chúng ta cũng thảo luận về các gói được chia sẻ. Vì vậy, để chạy được chế độ này, có nghĩa là các ứng dụng micro sẽ có thể chạy như các ứng dụng độc lập, chúng ta cũng sẽ áp dụng kỹ thuật tương tự bằng cách sử dụng dynamic import cho phần bootstrap như shell ở trên.
Các micro apps lúc này có thể có cấu hình routing riêng theo ý muốn.
Ví dụ, chúng ta có thể cấu hình routing cho ứng dụng mailbox như sau:
export const MAILBOX_ROUTES: Routes = [
{
path: "",
component: MailboxHomeComponent,
},
];
@NgModule({
declarations: [MailboxHomeComponent],
imports: [CommonModule, RouterModule.forChild(MAILBOX_ROUTES)],
})
export class MailboxModule {}
Tương tự, chúng ta có thể cấu hình cho ứng dụng calendar như sau:
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
output: {
publicPath: "auto", // we setup the `publicHost` in `angular.json` file
uniqueName: "calendar",
},
optimization: {
runtimeChunk: false,
},
experiments: {
// Allow output javascript files as module source type.
outputModule: true,
},
plugins: [
new ModuleFederationPlugin({
name: "calendar",
library: {
// because Angular v14 will output ESModule
type: "module",
},
filename: "remoteEntry.js",
exposes: {
"./CalendarModule":
"projects/calendar/src/app/calendar/calendar.module.ts",
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};
Lưu ý rằng, chúng ta có thể cấu hình routing cho ứng dụng calendar tùy ý tương tự như ứng dụng mailbox.
Khỏi chạy ứng dụng
Bây giờ bạn có thể khởi chạy cả ba ứng dụng như sau:
npm run start:shell
npm run start:mailbox
npm run start:calendar
Sau đó, truy cập các địa chỉ sau: http://localhost:5200/, http://localhost:5300/, http://localhost:5400/
Dưới đây là kết quả bạn sẽ nhận được. Bạn có thể chạy mỗi ứng dụng micro độc lập hoặc chạy chúng từ ứng dụng shell.