10 Dicas de Angular: Organização dos Módulos, VSCode e boas práticas

Loiane Groner

Java, JavaScript + HTML5, Sencha, Cordova/Ionic, Angular, RxJS + all things reactive

Angular CLI

#1

VS Code

#2

Configure o VSCode para a equipe

Extensões

{
  // See http://go.microsoft.com/fwlink/?LinkId=827846
  // for the documentation about the extensions.json format
  "recommendations": [
    "loiane.angular-extension-pack",
    "loiane.ts-extension-pack"
  ]
}

extensions.json

TSLint, snippets, testes, debug, organização TS, formatação arquivo

Configuração do workspace

{
  "editor.wordWrap": "off",
  "editor.minimap.enabled": false,

  "editor.codeActionsOnSave": { "source.fixAll.tslint": true },

  "files.autoSave": "afterDelay",
  "files.autoSaveDelay": 3000,

  "prettier.tabWidth": 2,
  "prettier.singleQuote": true,
  "prettier.useTabs": false,

  "html.format.wrapAttributes": "auto",
  "html.format.wrapLineLength": 0,
  "html.format.unformatted": "a, abbr, acronym, b, bdo, big, br, ...",

  "workbench.editor.enablePreview": false,

  "auto-rename-tag.activationOnLanguage": ["html", "xml"]
}

settings.json

Tarefas do VS Code (Debug)

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome with ng serve",
      "url": "http://localhost:4200",
      "webRoot": "${workspaceRoot}"
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome with ng test",
      "url": "http://localhost:9876/debug.html",
      "webRoot": "${workspaceRoot}"
    },
    {
      "type": "chrome",
      "request": "attach",
      "name": "Attach to Chrome",
      "port": 9222,
      "webRoot": "${workspaceRoot}"
    }
  ]
}

launch.json

Atalhos para tarefas comuns

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build",
      "type": "npm",
      "script": "build",
      "presentation": {
        "reveal": "always"
      },
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": ["$tsc"]
    },
    {
      "label": "test",
      "type": "npm",
      "script": "test",
      "presentation": {
        "reveal": "always"
      },
      "group": {
        "kind": "test",
        "isDefault": true
      }
    }
  ]
}

tasks.json

Atalhos Teclado: tasks.json

// Place your key bindings in this file to overwrite the defaults
[
  // Tests
  {
    "key": "ctrl+shift+t",
    "command": "workbench.action.tasks.test"
  },

  // Lint
  {
    "key": "ctrl+shift+l",
    "command": "workbench.action.tasks.runTask",
    "args": "lint"
  },

  // Serve app
  {
    "key": "ctrl+shift+s",
    "command": "workbench.action.tasks.runTask",
    "args": "serve"
  }
]

keybindings-win.json

Ambientes

#3

Qual seu workflow de trabalho?

Código back-end

Protótipo

Angular

Integração back-end

Build produção

Configure cada ambiente

"scripts": {
  "ng": "ng",
  "start": "ng serve --aot --env=dev",
  "server": "ng serve --proxy-config proxy.conf.js",
  "build":  "ng build --prod --aot --build-optimizer -op ../webapps/app"
},

package.json

Desenvolvimento Local

Desenvolvimento com Back-end

Produção

Configure cada ambiente

export const environment = {
  production: false,
  baseUrl: '/'
};

environment.dev.ts

export const environment = {
  production: false,
  baseUrl: '/api/'
};

environment.ts

export const environment = {
  production: false,
  baseUrl: '../'
};

environment.prod.ts

Configure os endpoints

import { environment } from '@env/environment';

export class API {

  static readonly BASE_URL = environment.baseUrl;

  // LISTA DE CLIENTES
  static readonly CLIENTS = `${API.BASE_URL}clients`;
}

// para usar essa API:
// this.http.get(API.CLIENTES)

API.ts

Mockar JSON para Dev Local

{
    "author" : [ {
        "id": 1,
        "name" : "Loiane"
      }, 
      {
        "id": 2,
        "name" : "abcdef"
      }
    ],
    "client" : []
}

meuServidor.json

npm install -g json-server
json-server --watch meuServidor.json

Se tiver back-end pronto...

Angular Style Guide

#4

TypeScript Hero

  • Tenta resolver os imports automaticamente
  • Pode organizar os imports ao salvar o arquivo
  • Ctrl + Alt + o para organizar (de acordo com styleguide) e remover imports não usados

Módulos

#5

Core

Feature

Shared

Core Module

  • Serviços Singleton
  • AuthService
  • APIService
  • etc

Shared Module

  • Componentes "dumb" reutilizáveis
  • Pipes
  • Não usam nada do Core ou de outros módulos
  • Pode incluir imports de libs UIs e re-exportá-las

Feature Module

  • Componentes do módulo X
  • "Tentar" criar componentes somente com dependência do shared e core
  • Dividir em sub-módulos se possível

Use e abuse do Lazy

Estratégias de Lazy Loading

const routes: Routes = [
  {
    path: '',
    redirectTo: 'modulo-1',
    pathMatch: 'full'
  },
  {
    path: 'modulo-1',
    component: Modulo1Component
  },
  {
    path: 'modulo-2',
    loadChildren: './modulo-2/modulo-2.module#Modulo2Module',
    data: { preload: true }  // pré-carregar em background
  },
  {
    path: 'modulo-3',
    loadChildren: './feature-3/modulo-3.module#Modulo3Module'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { 
    preloadingStrategy: AppCustomPreloader 
  })], // implementamos nossa lógica
  exports: [RouterModule],
  providers: [AppCustomPreloader]
})
export class AppRoutingModule { }

Estratégias de Lazy Loading

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

import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';

export class AppCustomPreloader implements PreloadingStrategy {
  preload(route: Route, load: Function): Observable<any> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

Containers e Components

tamanho é documento!

#6

  • Ajudam a separar a camada lógica da camada de apresentação
  • Components: testes unitários
  • Containers: testes unitários + integração
  • Facilita muito muito muito a manutenção do projeto

Dumb Components

  • Componentes de apresentação
  • Somente mostram dados no template
  • Emitem eventos para qualquer ação

Dumb Components

<mat-list>
  <app-task-item *ngFor="let task of tasks$ | async"
    [task]="task"
    (remove)="onRemove(task)"
    (edit)="onUpdate(task, $event)">
  </app-task-item>
</mat-list>

tasks-list.component.html

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TasksListComponent {

  @Input('tasks') tasks$: Observable<Task[]>;
  @Output() remove: EventEmitter<any> = new EventEmitter(false);
  @Output() edit: EventEmitter<any> = new EventEmitter(false);

  onRemove(task) {
    this.remove.emit(task);
  }

  onUpdate(task, changes) {
    this.edit.emit({task: task, updates: changes});
  }
}

tasks-list.component.ts

Containers

  • Smart Components
  • Componentes que organizam a tela e a funcionalidade
  • Contém os dumb-components
  • Escutam os eventos dos dumb-components
  • Passam dados para os dumb-components

Containers

<mat-card>
  <app-task-form (createTask)="onCreateTask($event)"></app-task-form>
    <mat-spinner *ngIf="isLoading$ | async; else taskList"></mat-spinner>
    <ng-template #taskList>
      <app-tasks-list
        [tasks]="tasks$"
        (remove)="onRemoveTask($event)"
        (edit)="onUpdateTask($event)">
      </app-tasks-list>
    </ng-template>
  <div class="error-msg" *ngIf="error$ | async as error">
    <p>{{ error }}</p>
  </div>
</mat-card>

tasks.component.html

Containers

export class TasksComponent implements OnInit {
  tasks$: Observable<Task[]>;
  isLoading$: Observable<boolean>;
  error$: Observable<string>;

  constructor(private taskStoreService: TaskStoreService) {}

  ngOnInit() {
    this.taskStoreService.dispatchLoadAction();
    this.tasks$ = this.taskStoreService.getTasks();
    this.isLoading$ = this.taskStoreService.getIsLoading();
    this.error$ = this.taskStoreService.getError();
  }

  onCreateTask(title) {
    this.taskStoreService.dispatchCreateAction(new Task(title));
  }

  onRemoveTask(task) {
    this.taskStoreService.dispatchRemoveAction(task);
  }

  onUpdateTask(event) {
    this.taskStoreService.dispatchUpdateAction(event.updates);
  }
}

tasks.component.ts

Prefira componentes pequenos

<form (ngSubmit)="onSubmit()" novalidate>
  <mat-input-container class="example-full-width">
    <input matInput placeholder="What needs to be done?" type="text"
      name="title"
      [(ngModel)]="title"
      (keyup.escape)="clear()"
      autocomplete="off"
      autofocus>
  </mat-input-container>
</form>

component1.html

<mat-list-item class="list-item">
  <mat-checkbox color="primary" type="checkbox" [name]="task.id" [(ngModel)]="task.completed" (change)="onEdit()">
    <span [class.task-completed]="task.completed">{{task.title}}</span>
  </mat-checkbox>
  <span class="fill-remaining-space"></span>
  <span>
  <button mat-mini-fab (click)="onRemove()">
    <mat-icon>delete_forever</mat-icon>
  </button>
  <button mat-mini-fab (click)="openEditDialog()" [disabled]="task.completed" color="primary">
    <mat-icon>mode_edit</mat-icon>
  </button>
  </span>
</mat-list-item>

component2.html

<mat-list>
  <app-task-item *ngFor="let task of tasks$ | async"
    [task]="task"
    (remove)="onRemove(task)"
    (edit)="onUpdate(task, $event)">
  </app-task-item>
</mat-list>

component3.html

<mat-card>
  <form (ngSubmit)="onSubmit()" novalidate>
      <mat-input-container class="example-full-width">
        <input matInput placeholder="What needs to be done?" type="text"
          name="title"
          [(ngModel)]="title"
          (keyup.escape)="clear()"
          autocomplete="off"
          autofocus>
      </mat-input-container>
    </form>
    <mat-spinner *ngIf="isLoading$ | async; else taskList" style="margin:0 auto;"></mat-spinner>
    <ng-template #taskList>
     <mat-list>
      <mat-list-item class="list-item" *ngFor="let task of tasks$ | async">
      <mat-checkbox color="primary" type="checkbox" [name]="task.id" [(ngModel)]="task.completed" (change)="onEdit()">
        <span [class.task-completed]="task.completed">{{task.title}}</span>
      </mat-checkbox>
      <span class="fill-remaining-space"></span>
      <span>
      <button mat-mini-fab (click)="onRemove()">
        <mat-icon>delete_forever</mat-icon>
      </button>
      <button mat-mini-fab (click)="openEditDialog()" [disabled]="task.completed" color="primary">
        <mat-icon>mode_edit</mat-icon>
      </button>
      </span>
    </mat-list-item>
    </mat-list>
    </ng-template>
  <div class="error-msg" *ngIf="error$ | async as error">
    <p>{{ error }}</p>
  </div>
</mat-card>

component1.html

component3.html

Abstração

#7

Crie Wrappers

Mantenha consistência!

Http             HttpClient

@Injectable()
export class ApiService {
  constructor(public http: HttpClient) { }

  getRequest<T>(url: string, params?: any) {
    return this.http.get<RetornoDoServer<T>>(url, { 
        params: this.getQueryParams(params) 
    })
    .take(1); // já podemos 'matar' o Observable no Ajax
  }

  postRequest(url: string, body: any) {
    // POST 
  }

  private getQueryParams(params: any) {
    // lógica para setar os query params com Angular v5+
  }

  downloadFile(url: string, params?: any) {
    // lógica para download de arquivo
  }

  uploadFile(file: AppFile | AppFile[], body?: any, params?: any) {
    // lógica para download de arquivo
  }
}  

api.service.ts

@Injectable()
export class TasksService {
  constructor(public http: ApiService) { }

  getAllTasksWithPaging(start = 0, limit = 100) {
    return this.http
      .getRequest<Task[]>(API.READ_TASKS, {start: start, limit: limit});
  }

  getById(id: number) {
     return this.http.getRequest<Task>(`${API.READ_TASKS}/${id}`);
  }
}  

tasks.service.ts

export interface RetornoDoServer<T> {
  dados: T[];
  total: number;
  successo?: boolean;
  msgErro?: string;
}

Retorno

Saiba usar herança e composição!

export abstract class BaseFormComponent implements IFormCanDeactivate {
  protected formSubmitAttempt: boolean;
  protected validateDirty = true;
  form: FormGroup;

  constructor() {
    this.formSubmitAttempt = false;
  }

  isFieldInvalid(field: string) {
    // return lógica validação dos campos
  }


  onSubmit() {
    this.formSubmitAttempt = true;
    // lógica genérica
  }

  onReset() { }

  canDeactivate(): Observable<boolean> {
    return this.modalService.showConfirm(
      'DoYouWantToLeavePage',
      'DoYouWantToLeavePageMsg'
    );
  }

  onCancel() {
    this.location.back();
  }
}
@Injectable()
export class CRUDService<T> {

  constructor(public http: HttpClient, private API_URL) {}

  load() {
    return this.http.get<T[]>(this.API_URL);
  }

  create(record: T) {
    return this.http.post<Task>(this.API_URL, record);
  }

  update(record: T) {
    return this.http.put<Task>(`${this.API_URL}/${record.id}`, record);
  }

  remove(id: string) {
    return this.http.delete<T>(`${this.API_URL}/${id}`);
  }
}
@Injectable()
export class TasksService extends CRUDService<Task> {

  constructor(public http: HttpClient) {
    super(http, API.TASKS_URL)
  }
}

Componentes são para apresentação!

Coloque Lógica nos serviços!

export class TasksLogicService {
  canCreate() {
    // lógica para verificar se usuário pode criar
  }

  filterTasks(querySearch: string) {
    // faz filtro e retorna resultado para o componente
  }

  verifyTaskIsValid(task: Task) {
    // lógica com 50 linhas de código
  }

}

Crie apelidos para os módulos

#8

import { AuthService } from '../../../core/security/auth/auth.service';
import { AuthService } from '@projeto/core';
import { AuthService } from '../../../core/security/auth/auth.service';
import { AuthService } from '@projeto/core';
{
  "compilerOptions": {
    ...
    
    "baseUrl": "src",
    "paths": {
      "@projeto/*": ["app/*"],
      "@env/*": ["environments/*"]
    }
  }
}

tsconfig.json

Build e bundle

#9

"scripts": {
  "ng": "ng",
  "build":  "ng build --prod --aot --build-optimizer -op ../webapps/app"
},

package.json

Automatize no CI!

Crie libs compartilhadas

#10

export * from './my-module/my-module.module';
"dependencies": {
    ...
    "toolkit-cwt": "file:../toolkit/dist/toolkit-cwt-0.0.0.tgz",
    ...
  }

package.json

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<app-my-component></app-my-component>

app.component.html

Obrigada!

Links e referências

Angular: Organização dos Módulos, VSCode e boas práticas

By Loiane Groner

Angular: Organização dos Módulos, VSCode e boas práticas

Você está trabalhando em projetos Angular e ainda tem algumas perguntas sobre a estrutura do seu projeto? Como organizar os módulos e componentes de uma aplicação para facilitar o fluxo de dados e a manutenção do projeto? Como fazer com que todo o time siga o mesmos padrões e melhore a produtividade? Existe alguma maneira mais fácil de se trabalhar com prototipação mesmo integrando o front-end com o servidor? Como fazer para diminuir o impacto das migrações de uma versão para outra? Nessa talk compartilho algumas das experiências e decisões sobre melhorias de compartilhamento de código e componentes que aprendi desenvolvendo projetos angular que estão em produção.

  • 6,559