标准银行演示: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 领域语言定义的实体。
本文将重点介绍信用卡实体。
对于此实体,您可以找到几个生成的类。
域层
最低层级是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;
Repository 是一个扩展Spring Data接口的接口,用于从数据库中检索内容,并定义可用于此给定实体的请求,它可以在org.entando.demo.banking.repository包下找到。
@Repository
public interface CreditcardRepository extends JpaRepository<Creditcard, Long>, JpaSpecificationExecutor<Creditcard> {
Optional<Creditcard> findByUserID(String userID);
}
服务层
服务层包含此实体的业务代码。基本上,服务层位于数据层和 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);
}
}
接下来,我们使用 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;
}
}
数据传输对象 (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();
}
}
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);
}
}
微前端
您可以在ui/widgets文件夹下找到所有微前端。每个微前端都对应一个业务用例,并以Web 组件的形式实现,同时调用银行微服务的 API。
银行微服务和微前端架构:
我们将重点介绍使用 Banking API 和 CreditCard 端点来显示信用卡金额和积分的 Dashboard Card React 实例。您可以在该ui/widgets/banking-widgets/dashboard-card-react文件夹下找到它。
前端代码:重点关注信用卡实现
微前端具有足够的通用性,可以处理银行 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);
}
我们可以看到该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>
调用银行 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 });
};
这里定义的请求足够灵活,可以用于多种类型的信用卡。这就是为什么路径取决于信用卡类型的原因cardname。userID banking/api/${cardname}s/user/${userID}
渲染银行信息
该src/components文件夹包含渲染部分,其中包含两者SeedcardDetails.js。SeedcardDetailsContainer.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't have a {cardname} account
</p>
</>
)}
</div>
</div>
);
};
处理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 }));
当小部件部署完毕后,请求中包含正确的卡片名称值,并且检索到的数据与之匹配,以下是仪表板的第一张屏幕截图。
在 Entando 平台中配置小部件
由于 Entando 将微前端封装成一个组件,因此它可以带有一个配置组件来设置诸如cardname.
这样,您就可以在 Entando App Builder 中更改cardname值,而无需再次部署微前端。
要访问该功能,您需要设计一个页面,点击小部件三脚架菜单,然后点击设置(只有当配置小部件与小部件一起使用时,才会显示设置菜单)。
接下来会发生什么?
在本文中,我们看到了大量从数据层(包括领域定义)到微前端数据渲染(用于显示信用卡信息)的代码。
下一篇文章将深入探讨标准银行演示系统的 CMS 组件。文章代码量较少,将更侧重于标准银行演示系统包,解释可用于构建页面内容的各种 CMS 组件。







