Padrões de Desenvolvimento Frontend
Stack Atual
Esta documentação refere-se à stack atual utilizada no projeto painel-v3 (Angular 8.x.x).
Para novos projetos, utilize sempre a última versão estável do Angular e suas dependências.
Stack Tecnológica
Principais Tecnologias
- Angular 8.x.x: Framework principal
- TypeScript 3.5.x: Linguagem de programação
- Nebular 5.x.x: Design System e componentes UI
- RxJS 6.5.x: Programação reativa
- Keycloak: Autenticação e autorização
Bibliotecas Principais
UI e Visualização
"@nebular/theme": "^5.0.0" // Design system
"@nebular/auth": "5.0.0" // Autenticação
"@nebular/eva-icons": "5.0.0" // Ícones
"@ng-select/ng-select": "3.7.3" // Select customizado
"@sweetalert2/ngx-sweetalert2": "^8.1.1" // Modais e alertas
"ngx-bootstrap": "^5.1.1" // Componentes Bootstrap
Gráficos e Mapas
"@swimlane/ngx-charts": "^13.0.2" // Gráficos
"ng-apexcharts": "1.5.12" // Gráficos avançados
"ngx-echarts": "^5.0.0" // Gráficos ECharts
"@agm/core": "^1.0.0-beta.5" // Google Maps
"@asymmetrik/ngx-leaflet": "3.0.1" // Leaflet Maps
Utilitários
"moment": "^2.29.1" // Manipulação de datas
"lodash": "^4.17.20" // Utilitários JS
"rxjs": "6.5.2" // Programação reativa
"file-saver": "^2.0.2" // Download de arquivos
"xlsx": "^0.16.2" // Manipulação de Excel
Estrutura de Projetos
Organização de Diretórios
client/
├── src/
│ ├── app/
│ │ ├── @theme/ # Tema e layouts
│ │ │ ├── components/ # Componentes do tema
│ │ │ ├── layouts/ # Layouts da aplicação
│ │ │ ├── pipes/ # Pipes compartilhados
│ │ │ ├── services/ # Serviços do tema
│ │ │ └── styles/ # Estilos globais
│ │ ├── auth/ # Autenticação
│ │ ├── pages/ # Módulos de páginas
│ │ │ ├── freight/ # Módulo de fretes
│ │ │ │ ├── components/ # Componentes compartilhados
│ │ │ │ ├── interfaces/ # Interfaces e tipos
│ │ │ │ ├── services/ # Serviços do módulo
│ │ │ │ ├── freight-list/
│ │ │ │ ├── freight-detail/
│ │ │ │ ├── freight-new/
│ │ │ │ ├── freight.module.ts
│ │ │ │ └── freight.routing.ts
│ │ │ └── [outros-modulos]/
│ │ ├── shared/ # Código compartilhado
│ │ │ ├── components/ # Componentes reutilizáveis
│ │ │ ├── directives/ # Diretivas
│ │ │ ├── pipes/ # Pipes
│ │ │ ├── services/ # Serviços compartilhados
│ │ │ ├── validators/ # Validadores
│ │ │ └── utils/ # Utilitários
│ │ ├── services/ # Serviços globais
│ │ ├── app-routing.module.ts
│ │ ├── app.component.ts
│ │ └── app.module.ts
│ ├── assets/ # Recursos estáticos
│ ├── environments/ # Configurações de ambiente
│ └── index.html
├── angular.json # Configuração do Angular
├── package.json
├── tsconfig.json
├── .eslintrc.json # Configuração ESLint
├── .prettierrc # Configuração Prettier
└── README.md
Arquitetura de Módulos
Estrutura de um Módulo
Cada funcionalidade do sistema é organizada como um módulo Angular independente:
// freight.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FreightRoutingModule } from './freight.routing';
import { SharedModule } from '../shared/shared.module';
@NgModule({
declarations: [
FreightListComponent,
FreightDetailComponent,
FreightNewComponent,
],
imports: [
CommonModule,
FreightRoutingModule,
SharedModule,
],
providers: [
FreightService,
],
})
export class FreightModule {}
Lazy Loading
Todos os módulos utilizam lazy loading para otimizar o carregamento:
// app-routing.module.ts
const routes: Routes = [
{
path: 'freight',
loadChildren: () => import('./pages/freight/freight.module')
.then(m => m.FreightModule),
canActivate: [AuthGuard],
},
];
Convenções de Nomenclatura
Arquivos
- Componentes:
nome-componente.component.ts - Serviços:
nome-servico.service.ts - Módulos:
nome-modulo.module.ts - Interfaces:
NomeInterface.ts(PascalCase) - Guards:
nome-guard.service.ts - Pipes:
nome-pipe.pipe.ts - Diretivas:
nome-diretiva.directive.ts
Classes e Interfaces
// PascalCase para classes e interfaces
export class FreightService {}
export interface FreightData {}
export class FreightListComponent {}
// camelCase para variáveis e métodos
private freightList: Freight[];
public getFreightById(id: string): Observable<Freight> {}
Constantes
// UPPER_SNAKE_CASE para constantes
export const API_BASE_URL = 'https://api.example.com';
export const DEFAULT_PAGE_SIZE = 10;
Padrões de Código
Componentes
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-freight-list',
templateUrl: './freight-list.component.html',
styleUrls: ['./freight-list.component.scss'],
})
export class FreightListComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
freightList: Freight[] = [];
loading = false;
constructor(
private freightService: FreightService,
private router: Router,
) {}
ngOnInit(): void {
this.loadFreights();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private loadFreights(): void {
this.loading = true;
this.freightService.getFreights()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: data => {
this.freightList = data;
this.loading = false;
},
error: err => {
console.error('Erro ao carregar fretes', err);
this.loading = false;
},
});
}
}
Serviços
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class FreightService {
private apiUrl = '/api/freights';
constructor(private http: HttpClient) {}
getFreights(): Observable<Freight[]> {
return this.http.get<FreightResponse>(this.apiUrl).pipe(
map(response => response.data),
catchError(this.handleError),
);
}
getFreightById(id: string): Observable<Freight> {
return this.http.get<Freight>(`${this.apiUrl}/${id}`);
}
createFreight(freight: FreightRequest): Observable<Freight> {
return this.http.post<Freight>(this.apiUrl, freight);
}
private handleError(error: any): Observable<never> {
console.error('Erro na requisição', error);
throw error;
}
}
Interfaces
// interfaces/Freight.ts
export interface Freight {
id: string;
freightId: string;
status: FreightStatus;
shipper: Shipper;
recipient: Recipient;
createdAt: Date;
updatedAt: Date;
}
export interface FreightRequest {
shipper: ShipperRequest;
recipient: RecipientRequest;
volumes: Volume[];
}
export enum FreightStatus {
PENDING = 'pending',
IN_TRANSIT = 'in_transit',
DELIVERED = 'delivered',
}
Gerenciamento de Estado
RxJS e Observables
import { BehaviorSubject, Observable } from 'rxjs';
export class FreightStateService {
private freightSubject = new BehaviorSubject<Freight[]>([]);
public freight$ = this.freightSubject.asObservable();
updateFreights(freights: Freight[]): void {
this.freightSubject.next(freights);
}
addFreight(freight: Freight): void {
const current = this.freightSubject.value;
this.freightSubject.next([...current, freight]);
}
}
Autenticação e Autorização
Keycloak Integration
// app.keycloak.ts
import { KeycloakService } from 'keycloak-angular';
export function initializeKeycloak(keycloak: KeycloakService) {
return () =>
keycloak.init({
config: {
url: 'https://keycloak.example.com/auth',
realm: 'freterapido',
clientId: 'painel-v3',
},
initOptions: {
onLoad: 'login-required',
checkLoginIframe: false,
},
});
}
Guards
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root',
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected router: Router,
protected keycloakService: KeycloakService,
) {
super(router, keycloakService);
}
async isAccessAllowed(): Promise<boolean> {
if (!this.authenticated) {
await this.keycloakService.login();
return false;
}
return true;
}
}
Controle de Permissões
// Diretiva hasPermission
@Directive({
selector: '[hasPermission]',
})
export class HasPermissionDirective implements OnInit {
@Input() hasPermission: string;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private aclService: NbAclService,
) {}
ngOnInit(): void {
const hasAccess = this.aclService.can(this.hasPermission);
if (hasAccess) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
Uso no template:
<button *hasPermission="'freight:create'">Criar Frete</button>
Interceptors
Request Interceptor
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
@Injectable()
export class RequestInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
const userType = this.getUserType(); // 'shipper' ou 'carrier'
const modifiedReq = req.clone({
url: `/api/${userType}${req.url}`,
setHeaders: {
'Content-Type': 'application/json',
},
});
return next.handle(modifiedReq);
}
private getUserType(): string {
// Lógica para identificar tipo de usuário
return 'shipper';
}
}
Formulários
Reactive Forms
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
export class FreightFormComponent implements OnInit {
freightForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.freightForm = this.fb.group({
shipper: this.fb.group({
registerNumber: ['', [Validators.required]],
name: ['', [Validators.required]],
}),
recipient: this.fb.group({
name: ['', [Validators.required]],
address: this.fb.group({
zipCode: ['', [Validators.required]],
street: [''],
number: [''],
}),
}),
volumes: this.fb.array([]),
});
}
onSubmit(): void {
if (this.freightForm.valid) {
const freight = this.freightForm.value;
// Processar formulário
}
}
}
Validadores Customizados
// validators/cnpj-validator.ts
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function cnpjValidator(control: AbstractControl): ValidationErrors | null {
const cnpj = control.value?.replace(/\D/g, '');
if (!cnpj || cnpj.length !== 14) {
return { invalidCnpj: true };
}
// Lógica de validação do CNPJ
const isValid = validateCnpj(cnpj);
return isValid ? null : { invalidCnpj: true };
}
Pipes
Pipes Customizados
// pipes/cnpj.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'cnpj',
})
export class CnpjPipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
const cnpj = value.replace(/\D/g, '');
return cnpj.replace(
/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/,
'$1.$2.$3/$4-$5',
);
}
}
Uso:
<p>{{ company.registerNumber | cnpj }}</p>
Estilos e Temas
SCSS Structure
// styles/styles.scss
@import 'themes';
@import 'layout';
@import 'overrides';
// Variáveis globais
$primary-color: #3366ff;
$secondary-color: #00d68f;
$danger-color: #ff3d71;
Nebular Theme
// styles/theme.default.ts
import { NbJSThemeOptions } from '@nebular/theme';
export const DEFAULT_THEME: NbJSThemeOptions = {
name: 'default',
variables: {
fontMain: 'Open Sans, sans-serif',
fontSecondary: 'Raleway, sans-serif',
},
};
Testes
Unit Tests
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FreightListComponent } from './freight-list.component';
import { FreightService } from './freight.service';
import { of } from 'rxjs';
describe('FreightListComponent', () => {
let component: FreightListComponent;
let fixture: ComponentFixture<FreightListComponent>;
let freightService: jasmine.SpyObj<FreightService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('FreightService', ['getFreights']);
TestBed.configureTestingModule({
declarations: [FreightListComponent],
providers: [{ provide: FreightService, useValue: spy }],
});
fixture = TestBed.createComponent(FreightListComponent);
component = fixture.componentInstance;
freightService = TestBed.inject(FreightService) as jasmine.SpyObj<FreightService>;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load freights on init', () => {
const mockFreights = [{ id: '1', freightId: 'FR-001' }];
freightService.getFreights.and.returnValue(of(mockFreights));
component.ngOnInit();
expect(component.freightList).toEqual(mockFreights);
});
});
Boas Práticas
1. Unsubscribe de Observables
// Usar takeUntil para gerenciar subscriptions
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.service.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => {});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
2. OnPush Change Detection
@Component({
selector: 'app-freight-list',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FreightListComponent {}
3. TrackBy em *ngFor
<div *ngFor="let freight of freights; trackBy: trackByFreightId">
{{ freight.freightId }}
</div>
trackByFreightId(index: number, freight: Freight): string {
return freight.id;
}
4. Lazy Loading de Imagens
<img [src]="imageUrl" loading="lazy" alt="Descrição">
5. Evitar Lógica no Template
// ❌ Evitar
<div *ngIf="user && user.role === 'admin' && user.active">
// ✅ Preferir
<div *ngIf="isAdmin">
// Component
get isAdmin(): boolean {
return this.user?.role === 'admin' && this.user?.active;
}
Code Quality
ESLint Configuration
{
"extends": [
"plugin:@angular-eslint/recommended",
"airbnb-typescript/base",
"plugin:prettier/recommended"
],
"rules": {
"max-len": ["error", { "code": 120 }],
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_"
}]
}
}
Prettier Configuration
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
}
Husky e Lint-Staged
{
"husky": {
"hooks": {
"pre-commit": "lint-staged --relative"
}
},
"lint-staged": {
"*.{js,ts}": ["eslint --fix"]
}
}
Build e Deploy
Scripts NPM
{
"scripts": {
"start": "ng serve --host 0.0.0.0 --port 8107",
"build": "ng build",
"build:prod": "ng build --prod --aot",
"build:homolog": "ng build --configuration=homolog --aot",
"test": "ng test",
"test:coverage": "ng test --code-coverage",
"lint": "ng lint",
"eslint": "eslint 'src/**/*.ts' --fix"
}
}
Environments
// environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
keycloakUrl: 'http://localhost:8080/auth',
};
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.freterapido.com',
keycloakUrl: 'https://auth.freterapido.com/auth',
};