Створення веб-застосунку з фронтендом на Angular та бекендом на Micronaut за допомогою NX Monorepos — Частина 3

Як наступний крок ми додаємо кілька прикладних компонентів до фронтенду. Але перед тим, як це зробити, нам потрібно додати кілька речей. У нашому випадку ми використовуємо Angular Material[1] разом з TailwindCSS[2], щоб налаштувати кожен додаток за допомогою наступної команди nx:

nx g @nx/angular:setup-tailwind sample-webapp-app  
nx g @nx/angular:setup-tailwind sample-webapp-public  
nx g @nx/angular:setup-tailwind sample-webapp-backoffice

Це створить файл tailwind.config.js у кожному додатку, змінить package.json і додасть налаштування Tailwind до styles.scss. Щоб використовувати додаткові можливості Tailwind, наприклад, типографіку, ми модифікуємо package.json і додаємо її як залежність.

"@tailwindcss/typography": "0.5.15"

І додаємо плагін до tailwind.config.js:

plugins: [require('@tailwindcss/typography')],

Тепер Tailwind встановлено. Тепер давайте зосередимося на Angular Material. Зазвичай ви встановлюєте це за допомогою:

ng add @angular/material

Але оскільки ми працюємо з nx, тут немає Angular CLI. Тому ми встановлюємо його вручну. Ви можете знайти інструкції тут https://material.angular.io/guide/getting-started.

Почнемо з додавання залежностей:

npm install @angular/material  
npm install @angular/cdk

Далі додаємо шрифти до index.html:




І змінюємо styles.scss:

@use '@angular/material' as mat;  
@tailwind base;  
@tailwind components;  
@tailwind utilities;  

@include mat.elevation-classes();  
@include mat.app-background();  

html, body {  
 height: 100%;  
}  

body {  
 margin: 0;  
 font-family: Roboto, "Helvetica Neue", sans-serif;  
}

Якщо вам потрібно налаштувати тему, потрібно додати файл theme.scss і змінити project.json:

"targets": {  
 "build": {  
 ...  
 "styles": [  
 "apps/sample-webapp-app/src/theme.scss",  
 "apps/sample-webapp-app/src/styles.scss"  
 ],

Я зробив це тільки для додатку.

І, зрештою, потрібно змінити конфігурацію Bootstrap для Angular додатку. Для цього додаємо наступні рядки до app.config.ts:

registerLocaleData(localeDe, 'de-DE', localeDeExtra);

А також додаємо провайдери:

provideAnimationsAsync(),  
{provide: MAT_DATE_LOCALE, useValue: 'de-DE'},  
{provide: LOCALE_ID, useValue: 'de-DE'},  
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},  
provideNativeDateAdapter(),  
provideHttpClient(),

Функція provideAnimationsAsync() налаштовує анімації браузера для Angular Material, і зазвичай це робиться через команду ng add @angular/material.

Два провайдери MATDATELOCALE та LOCALE_ID використовуються для німецької мови (ви можете змінити це на будь-яку іншу, і якщо ви використовуєте англійську, це не потрібно).

MATFORMFIELDDEFAULTOPTIONS налаштовує зовнішній вигляд полів форм (я віддаю перевагу вигляду "outline", тому використовую його, якщо ви хочете залишити стандартний вигляд, це не обов’язково).

Наступний крок — NativeDateAdapter, необхідний, якщо ви працюєте з Datepickers.

І останнє, але не менш важливе — httpClient для доступу до бекенду.

Тепер можемо запустити додаток:

nx run sample-webapp-app:serve:development

І ви зможете отримати доступ до нього на http://localhost:4200/.

Але поки що з’являється лише біла сторінка, досить нудно, я знаю. Але ми це виправимо за мить.

Якщо ви запустите інші додатки, такі як backoffice або public, в такий самий спосіб, ви побачите стандартну стартову сторінку nx. Не соромтеся експериментувати.

Тепер давайте додамо трохи візуального контенту. Як я вже сказав, ми хочемо показати список подій і дозволити користувачам додавати нові. Для цього ми використовуємо три компоненти, які додамо до папки core нашого додатку sample-webapp-app.
Використовуйте інтерфейс або консоль — що вам зручніше.

nx g @nx/angular:component .\apps\sample-webapp-app\src\core\event-board/event-board.component.ts –name event-board  
nx g @nx/angular:component .\apps\sample-webapp-app\src\core\event-board-entry/event-board-entry.component.ts - style=scss –name event-board-entry

Якщо ви не впевнені, можна використовувати параметр –dry-run, щоб змоделювати результат. Одна з проблем, яку я виявив, полягає в тому, що навіть якщо ви налаштуєте scss на рівні проєкту, компоненти створюються з css файлами. Якщо ви хочете використовувати scss всюapps\sample-webapp-app\src\core\dashboard/dashboard.component.ts - style=scss
```

Тепер у нас є три компоненти, які ми можемо використовувати. Dashboard — це свого роду головний вигляд. Event Board показує події, а event-board-entry відповідає за відображення елемента події у списку на дошці.

pic

Наступним кроком буде додавання маршрутизації. Для цього створимо файл core.route.ts, який буде використовуватися для маршрутизації модуля.

import {Routes} from "@angular/router";  

export const routes: Routes = [  
 {  
 path: '',  
 loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),  
 children: [  
 {path: '', redirectTo: 'event', pathMatch: "full"},  
 {  
 path: 'event',  
 loadComponent: () => import('./event-board/event-board.component').then(m => m.EventBoardComponent)  
 }  
 ]  
 },  
];

І додаємо це до app.routes.ts з використанням lazy loading:

import {Route} from '@angular/router';  

export const appRoutes: Route[] = [  
 {path: '', pathMatch: 'full', redirectTo: 'core'},  
 {  
 path: 'core',  
 loadChildren: () => import('../core/core.routes').then(m => m.routes),  
 },  
];

Тепер ми маємо щось на зразок цього:

pic

Тепер давайте додамо доступ до бекенду.
Отже, я вручну створив обліковий запис і подію в базі даних, щоб мати щось для сервера.

Ми також використовуємо новий ресурсний API для доступу до бекенду, який виглядає ось так (event-board.component.ts):

import {Component, computed, resource, signal} from '@angular/core';  
import {CommonModule} from '@angular/common';  
import {EventService, toPromise} from "@sample-webapp-workspace/core";  
import {EventBoardEntryComponent} from "../event-board-entry/event-board-entry.component";  

@Component({  
 selector: 'app-event-board',  
 imports: [CommonModule, EventBoardEntryComponent],  
 templateUrl: './event-board.component.html',  
 styleUrl: './event-board.component.scss',  
})  
export class EventBoardComponent {  

 pageSize = signal(20)  
 pageIndex = signal(0)  


 eventsCriteria = computed(() => ({  
 pageSize: this.pageSize(),  
 pageIndex: this.pageIndex()  
 }))  


 eventsResource = resource({  
 request: this.eventsCriteria,  
 loader: (param) => {  
 return toPromise(this.service.getAllEvents(param.request.pageIndex, param.request.pageSize), param.abortSignal)  
 }  
 })  

 reloading = this.eventsResource.isLoading  
 totalSize = computed(() => this.eventsResource.value()?.totalSize ?? 0)  
 events = computed(() => this.eventsResource.value()?.content ?? [])  

 constructor(private service: EventService) {  
 }  

}

Щоб показати деякі результати, ми змінюємо вміст файлу event-board.component.html ось так:


    @for (e of events(); track e){        }   

І додаємо вхідний сигнал (input signal) у файл event-board-entry.component.ts:

import {Component, input} from '@angular/core';   
import {CommonModule} from '@angular/common';   
import {Event} from '@sample-webapp-workspace/core'   
import {MatDivider} from "@angular/material/divider";   
import {MatCard} from "@angular/material/card";  

@Component({  
 selector: 'app-event-board-entry',  
 imports: [CommonModule, MatDivider, MatCard],  
 templateUrl: './event-board-entry.component.html',  
 styleUrl: './event-board-entry.component.scss',  
})  
export class EventBoardEntryComponent {  
 event = input.required()  
}  

Тепер перезапустіть застосунок і перевірте результати. І ми отримуємо… нічого. Але чому? Якщо ви подивитеся на вкладку мережі (Network) у вашому браузері, то побачите, що всі запити йдуть до localhost:4200.

pic

Але наш бекенд слухає на порту 8080. Нам потрібно повідомити Angular застосунок, що всі запити до /api повинні бути перенаправлені на порт 8080. І для цього використовуємо файл proxy.conf.json.

Додайте цей файл на верхньому рівні з таким вмістом:

{  
 "/api": {  
   "target": "http://localhost:8080",  
   "secure": true,  
   "logLevel": "debug"  
 }  
}  

Це перенаправить всі запити, що починаються з /api, на localhost:8080, зберігаючи при цьому дані авторизації та надаючи рівень логування для діагностики.

Щоб застосувати це налаштування, змініть ціль "serve" у вашому файлі project.json для застосунку, додавши параметр proxyConfig:

"serve": {  
 "executor": "@angular-devkit/build-angular:dev-server",  
 "configurations": {  
   "production": {  
     "buildTarget": "sample-webapp-app:build:production"  
   },  
   "development": {  
     "buildTarget": "sample-webapp-app:build:development"  
   }  
 },  
 "defaultConfiguration": "development",  
 "options": {  
   "proxyConfig": "proxy.conf.json"  
 }  
},  

Тепер, якщо ви перезапустите застосунок, побачите зміну на 401.

pic

І деяку активність на застосунку Micronaut.

pic

Завдяки налаштуванням безпеки в Micronaut, нам потрібно додати авторизацію до нашого веб-застосунку.
Це можна зробити за допомогою плагіна angular-keycloak.

Додавання Keycloak до вашого Angular застосунку

Спочатку встановіть залежності:

npm install keycloak-angular keycloak-js

Далі створюємо файл keycloak.config.ts для конфігурації.

Щоб уникнути зберігання налаштувань Keycloak у нашому вихідному коді, ми використовуємо налаштування середовища Angular. Для цього створіть папку environments у src/app з двома файлами.

pic

Додайте конфігурацію Keycloak до файлу environment.development.ts:

export const environment = {  
 keycloak: {  
   url: 'http://localhost:8081/',  
   realm: 'sample-webapp',  
   clientId: 'sample-webapp-app',  
 },  
};

І змініть конфігурацію project.json для цільового середовища:

"configurations": {  
 ...  
 "development": {  
 ....  
 "fileReplacements": [  
   {  
     "replace": "apps/sample-webapp-app/src/environments/environment.ts",  
     "with": "apps/sample-webapp-app/src/environments/environment.development.ts"  
   }  
 ]  
 }

І останнє — додайте конфігурацію та об'єднайте все, змінивши app.config.ts:

provideKeycloakAngular(),  
 provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),

Можливо, ви помітили, що ми вже мали provideHttpClient, тепер ми замінюємо його, щоб Keycloak міг перехоплювати запити до бекенду та додавати Bearer Token до запитів.

Не забувайте створити клієнта Keycloak для фронтенду.

pic

pic

Створіть користувача, якщо його ще не існує, і увійдіть за допомогою цього користувача.

Оновіть сторінку, і вона залишиться білою. Чому? Перевіривши логи застосунку Micronaut, ми побачимо помилку:

pic

Це хороший знак. Це означає, що все працює, нам просто не вистачає деяких налаштувань на стороні Keycloak. Пам'ятайте, що ми використовували перевірку "checkPermission" на стороні Controller. Для цього потрібно не лише мати користувача, а й додати певні дозволи до користувача. У цьому випадку це може бути дозвіл "read" або дозвіл "admin". Ви знайдете їх у EventAPI.

pic

Ми використовуємо дозволи на рівні realm, але ви можете також додавати їх на рівні клієнта, якщо бажаєте. В такому випадку потрібно змінити AuthUtils.kt з функцією checkPermission.

Отже, повертаємось до Keycloak і додаємо ролі на рівні Realm:

pic

Далі створюємо групу, щоб трохи полегшити управління:

pic

І додаємо ролі (read та write):

pic

Потім призначаємо групу користувачу.

pic

До речі, переконайтеся, що email вашого користувача Keycloak збігається з email вручну створеного облікового запису в базі даних, інакше результат буде порожнім. Тому що якщо у користувача є дозвіл "read", бекенд буде показувати тільки події, які безпосередньо пов'язані з обліковим записом. Це може здатися дивним, але пам'ятайте, що ми маємо маленький демонстраційний застосунок, щоб показати основні принципи. Щоб отримати справжній корисний результат, я рекомендую використовувати щось на зразок OpenSearch або подібне, щоб отримувати події на основі дозволів. На даний момент це абсолютно нормально, оскільки ми є єдиними користувачами нашого застосунку.
Після кількох перезапусків бекенду ви повинні побачити:

pic

pic

Остання зміна для файлу event-board-entry.component.ts, і ми готові:



    {{ event().title }}    
Start: {{ event().start |date:'medium' }}
Finish: {{ event().finish |date:'medium' }}
{{ event().shortText }}
{{ event().longText }}

pic

Частина 2: https://medium.com/@iee1394/create-an-webapp-with-angular-frontend-with-micronaut-backend-using-nx-monorepos-part-2-6b51825db21d

[1] https://material.angular.io/
[2] https://tailwindcss.com/

Перекладено з: Create an Webapp with Angular Frontend with Micronaut Backend using NX Monorepos — Part 3

Leave a Reply

Your email address will not be published. Required fields are marked *