发布于 2026-01-06 3 阅读
0

标准银行演示:JHipster 生成的微服务和微前端 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

标准银行演示:JHipster 生成的微服务和微前端

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

各位开发者同仁,大家好!

Entando 标准银行演示系列的第二集将带我们了解如何使用微前端调用 JHipster 生成的微服务。

标准银行演示程序在“Hello World”应用程序的基础上更进一步,帮助用户了解复杂的分布式应用程序如何与 Entando 协同工作。

本文将详细介绍代码架构、从领域级别到顶级 API 级别的实体定义,以及前端代码如何利用它。

让我们深入分析代码。

介绍

Entando 定义了组件类型,以代码的形式描述应用程序的不同部分。

Entando 应用程序的每个部分都可以使用组件进行定义,例如构成应用程序的资源、内容、页面、插件和小部件。

微服务以插件形式部署,使用镜像在 Kubernetes 上以 Pod 的形式运行容器。微前端以小部件形式部署,使用 JavaScript Web 组件并包含在页面中。

这些组件可以从零开始创建。不过,Entando 提供了一个名为 Entando 组件生成器 (ECG) 的 JHipster 蓝图,它通过搭建组件框架、创建数据层(域和存储库)、业务层(包括服务和数据传输对象)以及可通过 HTTP 请求调用的 API,来加快编码速度。

默认情况下,ECG 会为每个实体生成 3 个微前端,用于查看、编辑和列出数据。这些微前端涵盖了 CRUD 操作,并且可以根据您的需求进行定制。对于更高级的用例,您还可以实现自己的微前端。

本文将介绍银行微服务以及使用该 API 的微前端。

银行微服务

本文将配合银行应用程序,向大家展示标准银行演示版中的一些功能。

您可以在这里找到标准银行演示包的代码

您可以在这里找到银行微服务的代码

后端代码:重点关注信用卡实体

后端包含 9 个使用JHipster 领域语言定义的实体。

本文将重点介绍信用卡实体。

图像3_1_d0

图像6_1_d0

对于此实体,您可以找到几个生成的类。

域层

最低层级是org.entando.demo.banking.domain包中的领域对象。

@Entity
@Table(name = "creditcard")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Creditcard implements Serializable {

   private static final long serialVersionUID = 1L;

   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
   @SequenceGenerator(name = "sequenceGenerator")
   private Long id;

   @Column(name = "account_number")
   private String accountNumber;

   @Column(name = "balance", precision = 21, scale = 2)
   private BigDecimal balance;

   @Column(name = "reward_points")
   private Long rewardPoints;

   @Column(name = "user_id")
   private String userID;
Enter fullscreen mode Exit fullscreen mode

Repository 是一个扩展Spring Data接口的接口,用于从数据库中检索内容,并定义可用于此给定实体的请求,它可以在org.entando.demo.banking.repository包下找到。

@Repository
public interface CreditcardRepository extends JpaRepository<Creditcard, Long>, JpaSpecificationExecutor<Creditcard> {
   Optional<Creditcard> findByUserID(String userID);
}
Enter fullscreen mode Exit fullscreen mode

服务层

服务层包含此实体的业务代码。基本上,服务层位于数据层和 API 层之间。这里,我们有实现了该接口的服务类。

@Service
@Transactional
public class CreditcardServiceImpl implements CreditcardService {

   private final Logger log = LoggerFactory.getLogger(CreditcardServiceImpl.class);

   private final CreditcardRepository creditcardRepository;

   public CreditcardServiceImpl(CreditcardRepository creditcardRepository) {
       this.creditcardRepository = creditcardRepository;
   }

   @Override
   public Creditcard save(Creditcard creditcard) {
       log.debug("Request to save Creditcard : {}", creditcard);
       return creditcardRepository.save(creditcard);
   }

   @Override
   @Transactional(readOnly = true)
   public Page<Creditcard> findAll(Pageable pageable) {
       log.debug("Request to get all Creditcards");
       return creditcardRepository.findAll(pageable);
   }

   @Override
   @Transactional(readOnly = true)
   public Optional<Creditcard> findOne(Long id) {
       log.debug("Request to get Creditcard : {}", id);
       return creditcardRepository.findById(id);
   }

   @Override
   public void delete(Long id) {
       log.debug("Request to delete Creditcard : {}", id);
       creditcardRepository.deleteById(id);
   }

   @Override
   @Transactional(readOnly = true)
   public Optional<Creditcard> findOneWithUserID(String userID) {
       log.debug("Request to get Creditcard with userID: {}", userID);
       return creditcardRepository.findByUserID(userID);
   }

}
Enter fullscreen mode Exit fullscreen mode

接下来,我们使用 Spring Data Specifications 来实现高级搜索请求的 QueryService。

@Service
@Transactional(readOnly = true)
public class CreditcardQueryService extends QueryService<Creditcard> {

   private final Logger log = LoggerFactory.getLogger(CreditcardQueryService.class);

   private final CreditcardRepository creditcardRepository;

   public CreditcardQueryService(CreditcardRepository creditcardRepository) {
       this.creditcardRepository = creditcardRepository;
   }

   @Transactional(readOnly = true)
   public List<Creditcard> findByCriteria(CreditcardCriteria criteria) {
       log.debug("find by criteria : {}", criteria);
       final Specification<Creditcard> specification = createSpecification(criteria);
       return creditcardRepository.findAll(specification);
   }

   @Transactional(readOnly = true)
   public Page<Creditcard> findByCriteria(CreditcardCriteria criteria, Pageable page) {
       log.debug("find by criteria : {}, page: {}", criteria, page);
       final Specification<Creditcard> specification = createSpecification(criteria);
       return creditcardRepository.findAll(specification, page);
   }

   @Transactional(readOnly = true)
   public long countByCriteria(CreditcardCriteria criteria) {
       log.debug("count by criteria : {}", criteria);
       final Specification<Creditcard> specification = createSpecification(criteria);
       return creditcardRepository.count(specification);
   }

   protected Specification<Creditcard> createSpecification(CreditcardCriteria criteria) {
       Specification<Creditcard> specification = Specification.where(null);
       if (criteria != null) {
           if (criteria.getId() != null) {
               specification = specification.and(buildSpecification(criteria.getId(), Creditcard_.id));
           }
           if (criteria.getAccountNumber() != null) {
               specification = specification.and(buildStringSpecification(criteria.getAccountNumber(), Creditcard_.accountNumber));
           }
           if (criteria.getBalance() != null) {
               specification = specification.and(buildRangeSpecification(criteria.getBalance(), Creditcard_.balance));
           }
           if (criteria.getRewardPoints() != null) {
               specification = specification.and(buildRangeSpecification(criteria.getRewardPoints(), Creditcard_.rewardPoints));
           }
           if (criteria.getUserID() != null) {
               specification = specification.and(buildStringSpecification(criteria.getUserID(), Creditcard_.userID));
           }
       }
       return specification;
   }
}
Enter fullscreen mode Exit fullscreen mode

数据传输对象 (DTO) 用于存储作为参数传递给 QueryService 的条件。

public class CreditcardCriteria implements Serializable, Criteria {

   private static final long serialVersionUID = 1L;

   private LongFilter id;

   private StringFilter accountNumber;

   private BigDecimalFilter balance;

   private LongFilter rewardPoints;

   private StringFilter userID;

   public CreditcardCriteria(){
   }

   public CreditcardCriteria(CreditcardCriteria other){
       this.id = other.id == null ? null : other.id.copy();
       this.accountNumber = other.accountNumber == null ? null : other.accountNumber.copy();
       this.balance = other.balance == null ? null : other.balance.copy();
       this.rewardPoints = other.rewardPoints == null ? null : other.rewardPoints.copy();
       this.userID = other.userID == null ? null : other.userID.copy();
   }
}
Enter fullscreen mode Exit fullscreen mode

Web层

微服务的 Web 层(又称 REST 层)是应用程序的暴露部分,它定义了供客户端(例如微前端)使用的 REST 端点。

发送到端点的请求将被 Web 层捕获,并根据代码逻辑向服务发出调用,间接向域层发出调用。

@RestController
@RequestMapping("/api")
@Transactional
public class CreditcardResource {

   private final Logger log = LoggerFactory.getLogger(CreditcardResource.class);

   private static final String ENTITY_NAME = "creditcardCreditcard";

   @Value("${jhipster.clientApp.name}")
   private String applicationName;

   private final CreditcardService creditcardService;

   private final CreditcardQueryService creditcardQueryService;

   public CreditcardResource(CreditcardService creditcardService, CreditcardQueryService creditcardQueryService) {
       this.creditcardService = creditcardService;
       this.creditcardQueryService = creditcardQueryService;
   }

   @PostMapping("/creditcards")
   public ResponseEntity<Creditcard> createCreditcard(@RequestBody Creditcard creditcard) throws URISyntaxException {
       log.debug("REST request to save Creditcard : {}", creditcard);
       if (creditcard.getId() != null) {
           throw new BadRequestAlertException("A new creditcard cannot already have an ID", ENTITY_NAME, "idexists");
       }
       Creditcard result = creditcardService.save(creditcard);
       return ResponseEntity.created(new URI("/api/creditcards/" + result.getId()))
           .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString()))
           .body(result);
   }

   @PutMapping("/creditcards")
   public ResponseEntity<Creditcard> updateCreditcard(@RequestBody Creditcard creditcard) throws URISyntaxException {
       log.debug("REST request to update Creditcard : {}", creditcard);
       if (creditcard.getId() == null) {
           throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
       }
       Creditcard result = creditcardService.save(creditcard);
       return ResponseEntity.ok()
           .headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, creditcard.getId().toString()))
           .body(result);
   }

   @GetMapping("/creditcards")
   public ResponseEntity<List<Creditcard>> getAllCreditcards(CreditcardCriteria criteria, Pageable pageable) {
       log.debug("REST request to get Creditcards by criteria: {}", criteria);
       Page<Creditcard> page = creditcardQueryService.findByCriteria(criteria, pageable);
       HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
       return ResponseEntity.ok().headers(headers).body(page.getContent());
   }

   @GetMapping("/creditcards/count")
   public ResponseEntity<Long> countCreditcards(CreditcardCriteria criteria) {
       log.debug("REST request to count Creditcards by criteria: {}", criteria);
       return ResponseEntity.ok().body(creditcardQueryService.countByCriteria(criteria));
   }

   @GetMapping("/creditcards/{id}")
   public ResponseEntity<Creditcard> getCreditcard(@PathVariable Long id) {
       log.debug("REST request to get Creditcard : {}", id);
       Optional<Creditcard> creditcard = creditcardService.findOne(id);
       return ResponseUtil.wrapOrNotFound(creditcard);
   }

   @DeleteMapping("/creditcards/{id}")
   public ResponseEntity<Void> deleteCreditcard(@PathVariable Long id) {
       log.debug("REST request to delete Creditcard : {}", id);
       creditcardService.delete(id);
       return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString())).build();
   }

   @GetMapping("/creditcards/user/{userID}")
   public ResponseEntity<Creditcard> getCreditcardByUserID(@PathVariable String userID) {
       log.debug("REST request to get Creditcard by user ID: {}", userID);
       Optional<Creditcard> creditcard = creditcardService.findOneWithUserID(userID);
       return ResponseUtil.wrapOrNotFound(creditcard);
   }
}
Enter fullscreen mode Exit fullscreen mode

微前端

您可以在ui/widgets文件夹下找到所有微前端。每个微前端都对应一个业务用例,并以Web 组件的形式实现,同时调用银行微服务的 API。

银行微服务和微前端架构:

图像8_1_d0

我们将重点介绍使用 Banking API 和 CreditCard 端点来显示信用卡金额和积分的 Dashboard Card React 实例。您可以在该ui/widgets/banking-widgets/dashboard-card-react文件夹下找到它。

图像1_1_d0

前端代码:重点关注信用卡实现

微前端具有足够的通用性,可以处理银行 API 公开的多种交易类型:支票账户、储蓄账户和信用卡。

基本上,同一个前端组件可以多次使用,并配置为显示不同的数据集。

将 React 应用程序声明为自定义元素

自定义元素是 Web 组件规范的一部分。微前端在 React 应用中被声明为自定义元素。

在该src/custom-elements文件夹中,您可以找到一个SeedscardDetailsElement.js文件,该文件通过实现 HTMLElement 接口来定义整个组件。

const ATTRIBUTES = {
 cardname: 'cardname',
};

class SeedscardDetailsElement extends HTMLElement {
 onDetail = createWidgetEventPublisher(OUTPUT_EVENT_TYPES.transactionsDetail);

 constructor(...args) {
   super(...args);

   this.mountPoint = null;
   this.unsubscribeFromKeycloakEvent = null;
   this.keycloak = getKeycloakInstance();
 }

 static get observedAttributes() {
   return Object.values(ATTRIBUTES);
 }

 attributeChangedCallback(cardname, oldValue, newValue) {
   if (!Object.values(ATTRIBUTES).includes(cardname)) {
     throw new Error(`Untracked changed attribute: ${cardname}`);
   }
   if (this.mountPoint && newValue !== oldValue) {
     this.render();
   }
 }

 connectedCallback() {
   this.mountPoint = document.createElement('div');
   this.appendChild(this.mountPoint);

   const locale = this.getAttribute('locale') || 'en';
   i18next.changeLanguage(locale);

   this.keycloak = { ...getKeycloakInstance(), initialized: true };

   this.unsubscribeFromKeycloakEvent = subscribeToWidgetEvent(KEYCLOAK_EVENT_TYPE, () => {
     this.keycloak = { ...getKeycloakInstance(), initialized: true };
     this.render();
   });

   this.render();
 }

 render() {
   const customEventPrefix = 'seedscard.details.';
   const cardname = this.getAttribute(ATTRIBUTES.cardname);

   const onError = error => {
     const customEvent = new CustomEvent(`${customEventPrefix}error`, {
       details: {
         error,
       },
     });
     this.dispatchEvent(customEvent);
   };

   const ReactComponent = React.createElement(SeedscardDetailsContainer, {
     cardname,
     onError,
     onDetail: this.onDetail,
   });
   ReactDOM.render(
     <KeycloakContext.Provider value={this.keycloak}>{ReactComponent}</KeycloakContext.Provider>,
     this.mountPoint
   );
 }

 disconnectedCallback() {
   if (this.unsubscribeFromKeycloakEvent) {
     this.unsubscribeFromKeycloakEvent();
   }
 }
}

if (!customElements.get('sd-seeds-card-details')) {
 customElements.define('sd-seeds-card-details', SeedscardDetailsElement);
}
Enter fullscreen mode Exit fullscreen mode

我们可以看到该cardname属性被传递给自定义元素,以便在我们想要检索的不同类型数据之间进行切换。

'sd-seeds-card-details'标签可用于实例化新组件。以下示例来自public/index.html ,其默认cardname值为“checking”。

<body onLoad="onLoad();">
   <noscript>You need to enable JavaScript to run this app.</noscript>
   <sd-seeds-card-details cardname="checking" />
   <sd-seeds-card-config />
</body>
Enter fullscreen mode Exit fullscreen mode

调用银行 API

银行 API 公开了一些由 JHipster 实体声明生成的端点。MFE 可以通过 HTTP 调用来使用此 API。

src/api/seedscard.js文件包含端点定义:

import { DOMAIN } from 'api/constants';

const getKeycloakToken = () => {
 if (
   window &&
   window.entando &&
   window.entando.keycloak &&
   window.entando.keycloak.authenticated
 ) {
   return window.entando.keycloak.token;
 }
 return '';
};

const defaultOptions = () => {
 const token = getKeycloakToken();

 return {
   headers: new Headers({
     Authorization: `Bearer ${token}`,
     'Content-Type': 'application/json',
   }),
 };
};

const executeFetch = (params = {}) => {
 const { url, options } = params;
 return fetch(url, {
   method: 'GET',
   ...defaultOptions(),
   ...options,
 })
   .then(response =>
     response.status >= 200 && response.status < 300
       ? Promise.resolve(response)
       : Promise.reject(new Error(response.statusText || response.status))
   )
   .then(response => response.json());
};

export const getSeedscard = (params = {}) => {
 const { id, options, cardname } = params;

 const url = `${DOMAIN}${DOMAIN.endsWith('/') ? '' : '/'}banking/api/${cardname}s/${id}`;

 return executeFetch({ url, options });
};

export const getSeedscardByUserID = (params = {}) => {
 const { userID, options, cardname } = params;

 const url = `${DOMAIN}${DOMAIN.endsWith('/') ? '' : '/'}banking/api/${cardname}s/user/${userID}`;

 return executeFetch({ url, options });
};
Enter fullscreen mode Exit fullscreen mode

这里定义的请求足够灵活,可以用于多种类型的信用卡。这就是为什么路径取决于信用卡类型的原因cardnameuserID banking/api/${cardname}s/user/${userID}

渲染银行信息

src/components文件夹包含渲染部分,其中包含两者SeedcardDetails.jsSeedcardDetailsContainer.js.

const SeedscardDetails = ({ classes, t, account, onDetail, cardname }) => {
 const header = (
   <div className={classes.SeedsCard__header}>
     <img alt="interest account icon" className={classes.SeedsCard__icon} src={seedscardIcon} />
     <div className={classes.SeedsCard__title}>
       {t('common.widgetName', {
         widgetNamePlaceholder: cardname.replace(/^\w/, c => c.toUpperCase()),
       })}
     </div>
     <div className={classes.SeedsCard__value}>
       ...
       {account &&
         account.id &&
         account.accountNumber.substring(
           account.accountNumber.length - 4,
           account.accountNumber.length
         )}
     </div>
     <div className={classes.SeedsCard__action}>
       <i className="fas fa-ellipsis-v" />
     </div>
   </div>
 );

 return (
   // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
   <div
     onClick={account && account.id ? () => onDetail({ cardname, accountID: account.id }) : null}
   >
     <div className={classes.SeedsCard}>
       {account && account.id ? (
         <>
           {header}
           <p className={classes.SeedsCard__balance}>
             ${account.balance.toString().replace(/\B(?<!\.\d)(?=(\d{3})+(?!\d))/g, ',')}
           </p>
           <p className={classes.SeedsCard__balanceCaption}>Balance</p>
           {account.rewardPoints && (
             <p className={classes.SeedsCard__balanceReward}>
               Reward Points:{' '}
               <span className={classes.SeedsCard__balanceRewardValue}>
                 {account.rewardPoints}
               </span>
             </p>
           )}
         </>
       ) : (
         <>
           {header}
           <p className={classes.SeedsCard__balanceCaption}>
             You don&apos;t have a {cardname} account
           </p>
         </>
       )}
     </div>
   </div>
 );
};
Enter fullscreen mode Exit fullscreen mode

处理SeedcardDetailsContainer.js.API 调用:

getSeedscardByUserID({ userID, cardname })
 .then(account => {
   this.setState({
     notificationStatus: null,
     notificationMessage: null,
     account,
   });
   if (cardname === 'checking' && firstCall) {
     onDetail({ cardname, accountID: account.id });
   }
 })
 .catch(e => {
   onError(e);
 })
 .finally(() => this.setState({ loading: false }));
Enter fullscreen mode Exit fullscreen mode

当小部件部署完毕后,请求中包含正确的卡片名称值,并且检索到的数据与之匹配,以下是仪表板的第一张屏幕截图。

图像5_1_d0

图像7_1_d0

在 Entando 平台中配置小部件

由于 Entando 将微前端封装成一个组件,因此它可以带有一个配置组件来设置诸如cardname.

这样,您就可以在 Entando App Builder 中更改cardname值,而无需再次部署微前端。

要访问该功能,您需要设计一个页面,点击小部件三脚架菜单,然后点击设置(只有当配置小部件与小部件一起使用时,才会显示设置菜单)。

图像4_1_d0

图像2_1_d0

接下来会发生什么?

在本文中,我们看到了大量从数据层(包括领域定义)到微前端数据渲染(用于显示信用卡信息)的代码。

下一篇文章将深入探讨标准银行演示系统的 CMS 组件。文章代码量较少,将更侧重于标准银行演示系统包,解释可用于构建页面内容的各种 CMS 组件。

附赠:标准银行演示视频

文章来源:https://dev.to/entando/standard-banking-demo-jhipster- generated-microservices-and-micro-frontends-265o