SAP BTP - React Micro Frontend를 Luigi에 붙이기
임의의 데이터를 만들기 위해 /react-core-mf/src/assets 경로에 products.js 파일 생성
export const ProductCollection = [
{
"id": 101,
"name": "Mouse",
"price": 45.0,
"stock": 80,
"icon": "product",
"currencyCode": "EUR",
"orderQuantity": 2,
"description": "Wireless Gaming Mouse with Sensor"
},
{
"id": 102,
"name": "Keyboard",
"price": 50.0,
"stock": 0,
"icon": "product",
"currencyCode": "EUR",
"orderQuantity": 1,
"description": "A physical keyboard that uses an individual spring and switch for each key. Today, only premium keyboards are built with key switches."
},
{
"id": 103,
"name": "Optical Mouse",
"price": 35.0,
"stock": 4,
"icon": "product",
"currencyCode": "EUR",
"orderQuantity": 2,
"description": "Utilizing the latest optical sensing technology, the USB Optical Scroll Mouse records precise motion."
},
{
"id": 104,
"name": "Laptop Pro",
"price": 1299.0,
"stock": 11,
"icon": "laptop",
"currencyCode": "EUR",
"orderQuantity": 3,
"description": "Newest laptop featuring a touch-sensitive OLED display."
},
{
"id": 105,
"name": "Mouse 2",
"price": 40.0,
"stock": 20,
"icon": "product",
"currencyCode": "EUR",
"orderQuantity": 6,
"description": "The Mouse 2 is a computer mouse featuring a multi-touch acrylic surface for scrolling. The mouse features a lithium-ion rechareable battery and Lightning connector for charging and pairing."
},
{
"id": 106,
"name": "Printer",
"price": 235.0,
"stock": 24,
"icon": "fx",
"currencyCode": "EUR",
"orderQuantity": 1,
"description": "Affordable printer providing you with the optimal way to take care of all your printing needs."
},
{
"id": 107,
"name": "Phone 11",
"price": 835.0,
"stock": 45,
"icon": "iphone",
"currencyCode": "EUR",
"orderQuantity": 8,
"description": "The Phone 11 dimensions are 150.9mm x 75.7mm x 8.3mm (H x W x D). It weighs about 194 grams (6.84 ounces)."
},
{
"id": 108,
"name": "Phone 3a",
"price": 299.0,
"stock": 54,
"icon": "desktop-mobile",
"currencyCode": "EUR",
"orderQuantity": 7,
"description": "At 5.6 inches, the display is proportionate to the relatively small body of the phone."
},
{
"id": 109,
"name": "Game Console 4",
"price": 330.0,
"stock": 94,
"icon": "video",
"currencyCode": "EUR",
"orderQuantity": 1,
"description": "This is the fourth home video game console compatible with all gaming systems."
},
{
"id": 110,
"name": "Monitor",
"price": 630.0,
"stock": 20,
"icon": "sys-monitor",
"currencyCode": "EUR",
"orderQuantity": 3,
"description": "34'' Monitor, Display with stand Height adjustable (115 mm), titltable (-5° to 21°), rotatable (-30° to 30°) Security slot (cable lock sold separately), anti-theft slot for locking to stand (for display). Includes: DisplayPort cable, HDMI cable, Power cable, Stand, USB 3.0 Type-A to Type-B cable, USB-C cable."
}
]
/react-core-mf/public/sampleapp.html 수정
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Luigi Simple React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
webpack.config.js의 HTMLWebpackPlugin을 설정하는 이유는 micro frontend도 다른 환경과 같은 환경을 가져가야 하기 때문에 html 파일을 생성하기 위함
...
new HTMLWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: './public/sampleapp.html',
filename: 'sampleapp.html'
},
...
/react-core-mf/webpack.config.js 파일 수정 ( line 80에 Add this rule 부분 추가 )
...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
}
},
/// <---Add this rule -->
{
test: /\.(css)$/,
use: ['style-loader', 'css-loader'],
},
/// <----->
]
},
navigation과 general settings와 관련된 간단한 요소를 사용해 첫 번째 “Home” navigation node를 생성하고 application을 반응형으로 만든다.
사용될 Luigi navigation 요소
- pathSegment : URL에 추가된 text segment
- label : navigation에 표시될 node의 이름
- icon : label 옆에 표시될 SAP icon
- viewUrl : 마이크로 프론트엔드의 URL
- /react-core-mf/public/luigi-config.js 파일 수정
Luigi.setConfig({
navigation: {
nodes: () => [
{
pathSegment: "home",
label: "Home",
icon: "home",
viewUrl: "/sampleapp.html#/microfrontend/home",
},
],
},
settings: {
header: { title: "Luigi React App"},
responsiveNavigation: "simpleMobileOnly",
customTranslationImplementation: myTranslationProvider,
},
lifecycleHooks: {
luigiAfterInit: () => {
Luigi.i18n().setCurrentLocale(defaultLocale);
},
},
communication: {
customMessagesListeners: {
"set-language": (msg) => {
Luigi.i18n().setCurrentLocale(msg.locale);
},
},
},
});
var defaultLocale = "en-US";
function myTranslationProvider() {
var dict = {
"en-US": { PRODUCTS: "Products", ORDERHISTORY: "Order History" },
};
return {
getTranslation: function (label, interpolation, locale) {
const local = locale || Luigi.i18n().getCurrentLocale() || defaultLocale
return (
dict[local][label] || label
);
},
};
}
6. Configure router for “Home” view
- React app 진입점 ‘index.js’ 변경.
- ‘/react-core-mf/src/views’ 경로에 ‘home.js’ view 생성 > router 구성 > import Luigi Client
- localization을 다룰 localization에서 사용될 language.js 생성
- /react-core-mf/src/index.js 파일 수정 (해당 파일이 없어 생성하여 진행)
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './views/home.js';
import { dict } from './language.js';
import { addInitListener, addContextUpdateListener, removeContextUpdateListener, removeInitListener, uxManager} from '@luigi-project/client';
import { ThemeProvider } from "@ui5/webcomponents-react";
const container = document.getElementById('root');
const root = createRoot(container);
const App = () => {
const [currentLocale, setCurrentLocale] = useState('en-US');
const [initListener, setInitListener] = useState(null);
const [contextUpdateListener, setContextUpdateListener] = useState(null);
useEffect(() => {
const updateCurrentLanguage = () => {
setCurrentLocale(uxManager().getCurrentLocale())
}
setInitListener(
addInitListener(() => {
console.log("Luigi Client initialized.");
// update current language upon Luigi Client initialization
updateCurrentLanguage();
})
);
setContextUpdateListener(
addContextUpdateListener(() => {
// update current language upon Luigi Client context update event
updateCurrentLanguage();
})
);
return function cleanup() {
removeContextUpdateListener(contextUpdateListener);
removeInitListener(initListener);
};
}, []);
return (
<ThemeProvider>
<React.StrictMode>
<Router basename="microfrontend">
<Routes>
<Route path="/home" element={<Home localeDict={dict[currentLocale]} currentLocale={currentLocale} />} />
</Routes>
</Router>
</React.StrictMode>
</ThemeProvider>
);
};
root.render(<App />);
‘/react-core-mf/src/views/home.js’ 파일 수정
import React, { useState, useEffect } from 'react';
import "fundamental-styles/dist/fundamental-styles.css";
import { addInitListener, addContextUpdateListener, removeContextUpdateListener, removeInitListener, sendCustomMessage } from "@luigi-project/client";
import { Grid, Panel, Select, Option } from "@ui5/webcomponents-react";
const Home = (props) => {
const [options] = useState([{ key: 'en-US', text: 'en-US' }]);
function onChangeValue(event) {
sendCustomMessage({
id: "set-language",
locale: event.detail.selectedOption.innerText,
});
}
return (
<Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10">
<Panel headerText={props.localeDict.WELCOME_LUIGI} headerLevel="H3">
<Select onChange={onChangeValue}>
{options.map((language) => (
<Option key={language.key}>{language.text}</Option>
))}
</Select>
</Panel>
</Grid>
);
};
export default Home;
‘/react-core-mf/src’ 경로에 language.js 파일 생성
export const dict = {
"en-US": {
ITEMS: "Products",
STOCKS: "Stocks",
SELECTLANGUAGE: "Please select a language",
PRICE: "Price",
WELCOME_LUIGI: "Welcome to Luigi - a micro-frontend framework",
DESCRIPTION: "Description",
PRODUCTADDED: "Product has been added to cart",
AVAILABLE: "Available",
AVAILABLEQUANT: "Available quantity: ",
ADDTOCART: "Add to cart",
BACK: "Back",
OUTOFSTOCK: "Out of stock",
},
};
“Products” 마이크로 프론트엔드에 대한 navigation node를 Luigi에 추가
‘/react-core-mf/public/luigi-config.js’ 파일 수정 (children에 Products node 추가)
navigation: {
nodes: () => [
{
pathSegment: "home",
label: "Home",
icon: "home",
viewUrl: "/sampleapp.html#/microfrontend/home",
//<---Add the section below to the config file--->
children: [
{
pathSegment: "products",
label: "Products",
icon: "product",
viewUrl: "/sampleapp.html#/microfrontend/products",
}
],
// <------>
},
],
},
‘/react-core-mf/src/views/sample1.js’ 파일 이름 변경 후 파일 수정 ( sample1.js -> products.js )
import React, { useEffect, useState } from 'react';
import "../../node_modules/fundamental-styles/dist/fundamental-styles.css";
import "@ui5/webcomponents-icons/dist/AllIcons.js";
import { Grid, List, StandardListItem } from "@ui5/webcomponents-react";
import { linkManager } from "@luigi-project/client";
import { ProductCollection } from "../assets/products.js";
const Products = (props) => {
const [listItems, setListItems] = useState([]);
useEffect(() => {
const tempList = [];
ProductCollection.forEach((product) => {
tempList.push(
<StandardListItem id={product.id} key={product.id} additionalText={product.price + " " + product.currencyCode} additionalTextState="Information" description={product.description} growing="None" headerText={product.orderQuantity} icon={product.icon} type="Active" mode="None" onItemClick={() => handleItemClick(product.id)}>
<p>{product.name}</p>
</StandardListItem>
);
setListItems(tempList);
});
}, [])
// navigates to productDetail microfrontend through Luigi Client linkManager API
function handleItemClick(event) {
linkManager().withParams({ root: "products" });
linkManager().navigate(
"/home/products/" + event.detail.item.id.toString()
);
};
return (
<Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10">
<List headerText={props.localeDict.ITEMS + ": " + ProductCollection.length} onItemClick={handleItemClick}>
{listItems}
</List>
</Grid>
);
};
export default Products;
‘index.js’에 routing module 추가
//<---Around line 8, paste this line: --->:
import Products from "./views/products.js";
//<------>
...
<Router basename="microfrontend">
<Routes>
<Route path="/home" render={(props) => <Home {...props} localeDict={dict[this.state.currentLocale]} currentLocale={this.state.currentLocale} />} />
//<---Around line 45, paste this line: --->
<Route path="/products" element={<Products localeDict={dict[currentLocale]} />} />
//<------>
</Routes>
</Router>
8. Add “Product Detail” view to Luigi app
ProductDetail view 추가. Luigi dynamic parameter( 해당 tutorial에서는
:id
사용 )을 통해 각 product의 상세 정보를 볼 수 있음.
‘/react-core-mf/public/luigi-config.js’ 파일 수정
products node에 :id
child node 추가
children: [
{
pathSegment: "products",
label: "Products",
icon: "product",
viewUrl: "/sampleapp.html#/microfrontend/products",
//<---Paste the section below to your config:--->
keepSelectedForChildren: true,
children: [{
pathSegment: ':id',
viewUrl: '/sampleapp.html#/microfrontend/productDetail/:id',
context: { id: ':id' }
}]
//<------>
},
‘/react-core-mf/srv/views’ 경로에 ‘productDetail.js’ 파일 생성
import React, { useEffect, useState, useRef } from 'react';
import "../../node_modules/fundamental-styles/dist/fundamental-styles.css";
import "@ui5/webcomponents-icons/dist/AllIcons.js";
import {
Grid, ObjectPage, Label, DynamicPageHeader, DynamicPageTitle, ObjectStatus, FlexBox, Button, Toast, ObjectPageSection, FormItem, Form, Text, Bar
} from "@ui5/webcomponents-react";
import { linkManager, getContext } from "@luigi-project/client";
import { ProductCollection } from "../assets/products.js";
const ProductDetail = (props) => {
const [currentProduct, setCurrentProduct] = useState({});
const [availability, setAvailability] = useState({
state: "Warning",
text: props.localeDict.OUTOFSTOCK
});
const toast = useRef(null);
useEffect(() => {
setProductAndAvailability();
}, []);
function setProductAndAvailability() {
// get id from Luigi Client getContext API
const id = getContext().id;
let product = ProductCollection.find(
(product) => product.id.toString() === id
) || ProductCollection[0]
setCurrentProduct(product);
product.stock ? setAvailability({ state: "Success", text: props.localeDict.AVAILABLE }) : ""
}
// Use Luigi Client linkManager API to navigate to the previous microfrontend
function navBack() {
// checks if there is a previous view in history
if (linkManager().hasBack()) {
// navigates to the previously opened microfrontend
linkManager().goBack();
} else {
// navigates to the products page directly
linkManager().navigate("/home/products");
}
};
return (
<Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10">
<ObjectPage
headerContent={
<DynamicPageHeader>
<FlexBox alignItems="Center" wrap="Wrap">
<FlexBox direction="Column" style=>
<Label>
{props.localeDict.AVAILABLEQUANT + currentProduct.stock}
</Label>
</FlexBox>
</FlexBox>
</DynamicPageHeader>
}
footer={
<Bar design="FloatingFooter"
endContent={
<>
<Button design="Emphasized" onClick={() => { toast.current.show() }}>
{props.localeDict.ADDTOCART}
</Button>
<Button design="Default" onClick={navBack}>
{props.localeDict.BACK}
</Button>
</>
}
/>
}
headerContentPinnable
headerTitle={
<DynamicPageTitle
actions={
<>
<Toast ref={toast}>
{props.localeDict.PRODUCTADDED}
</Toast>
<Button design="Emphasized" onClick={() => { toast.current.show() }}>
{props.localeDict.ADDTOCART}
</Button>
<Button onClick={navBack}>
{props.localeDict.BACK}
</Button>
</>
}
header={currentProduct.name}
>
<ObjectStatus state={availability.state}>
{availability.text}
</ObjectStatus>
</DynamicPageTitle>
}
onSelectedSectionChange={function noRefCheck() { }}
selectedSectionId="details"
showHideHeaderButton
style=
>
<ObjectPageSection aria-label="Details" id="details" titleText="Details">
<Form columnsL={2} columnsM={2} columnsXL={3} labelSpanL={1} labelSpanM={1} labelSpanXL={1}>
<FormItem label="Name">
<Text>{currentProduct.name}</Text>
</FormItem>
<FormItem label={props.localeDict.DESCRIPTION}>
<Text>{currentProduct.description}</Text>
</FormItem>
<FormItem label={props.localeDict.PRICE}>
<Text>
{currentProduct.price + " " + currentProduct.currencyCode}
</Text>
</FormItem>
</Form>
</ObjectPageSection>
</ObjectPage>
</Grid>
);
};
export default ProductDetail;
‘index.js’를 아래와 같이 변경
return (
<ThemeProvider>
<React.StrictMode>
<Router basename="microfrontend">
<Routes>
<Route path="/home" element={<Home localeDict={dict[currentLocale]} currentLocale={currentLocale} />} />
<Route path="/products" element={<Products localeDict={dict[currentLocale]}/>}/>
<Route path="/productDetail/:id" element={<ProductDetail localeDict={dict[currentLocale]}/>}/>
</Routes>
</Router>
</React.StrictMode>
</ThemeProvider>
);
setCurrentProduct(product)에 있는 product를 이용하는 것으로 변경하여 정상적으로 동작하도록 수정.
재고가 있을 경우.
재고가 없을 경우.