209 lines
5.8 KiB
Markdown
209 lines
5.8 KiB
Markdown
|
|
# DI-005: Arquitectura Frontend
|
||
|
|
|
||
|
|
**Proyecto:** Sistema de Registro de Estudiantes
|
||
|
|
**Fecha:** 2026-01-07
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Stack Tecnológico
|
||
|
|
|
||
|
|
| Tecnología | Propósito |
|
||
|
|
|------------|-----------|
|
||
|
|
| Angular 21 | Framework SPA |
|
||
|
|
| Angular Material | UI Components |
|
||
|
|
| Apollo Angular | Cliente GraphQL |
|
||
|
|
| Signals | Estado reactivo |
|
||
|
|
| TypeScript | Tipado estático |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Estructura de Carpetas
|
||
|
|
|
||
|
|
```
|
||
|
|
src/frontend/src/app/
|
||
|
|
├── core/ # Singleton services
|
||
|
|
│ ├── services/
|
||
|
|
│ │ ├── student.service.ts
|
||
|
|
│ │ └── enrollment.service.ts
|
||
|
|
│ ├── graphql/
|
||
|
|
│ │ ├── generated/ # Tipos generados
|
||
|
|
│ │ ├── queries/
|
||
|
|
│ │ │ ├── students.graphql
|
||
|
|
│ │ │ └── subjects.graphql
|
||
|
|
│ │ └── mutations/
|
||
|
|
│ │ ├── student.graphql
|
||
|
|
│ │ └── enrollment.graphql
|
||
|
|
│ └── interceptors/
|
||
|
|
│ └── error.interceptor.ts
|
||
|
|
│
|
||
|
|
├── shared/ # Reutilizables
|
||
|
|
│ ├── components/
|
||
|
|
│ │ ├── confirm-dialog/
|
||
|
|
│ │ └── loading-spinner/
|
||
|
|
│ └── pipes/
|
||
|
|
│ └── credits.pipe.ts
|
||
|
|
│
|
||
|
|
└── features/ # Módulos por funcionalidad
|
||
|
|
├── students/
|
||
|
|
│ ├── pages/
|
||
|
|
│ │ ├── student-list/
|
||
|
|
│ │ └── student-form/
|
||
|
|
│ └── components/
|
||
|
|
│ └── student-card/
|
||
|
|
│
|
||
|
|
├── enrollment/
|
||
|
|
│ ├── pages/
|
||
|
|
│ │ └── enrollment-page/
|
||
|
|
│ └── components/
|
||
|
|
│ ├── subject-selector/
|
||
|
|
│ └── enrolled-subjects/
|
||
|
|
│
|
||
|
|
└── classmates/
|
||
|
|
└── pages/
|
||
|
|
└── classmates-page/
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Configuración Apollo
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// app.config.ts
|
||
|
|
export const appConfig: ApplicationConfig = {
|
||
|
|
providers: [
|
||
|
|
provideHttpClient(),
|
||
|
|
provideApollo(() => ({
|
||
|
|
link: httpLink.create({ uri: environment.graphqlUrl }),
|
||
|
|
cache: new InMemoryCache(),
|
||
|
|
defaultOptions: {
|
||
|
|
watchQuery: { fetchPolicy: 'cache-and-network' },
|
||
|
|
mutate: { errorPolicy: 'all' }
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
]
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Gestión de Estado
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────┐
|
||
|
|
│ Apollo Cache │
|
||
|
|
│ (Estado del servidor: students, subjects) │
|
||
|
|
└─────────────────────────────────────────────┘
|
||
|
|
▲
|
||
|
|
│ watchQuery / mutate
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────┐
|
||
|
|
│ Services │
|
||
|
|
│ (Encapsulan operaciones GraphQL) │
|
||
|
|
└─────────────────────────────────────────────┘
|
||
|
|
▲
|
||
|
|
│ inject()
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────┐
|
||
|
|
│ Components │
|
||
|
|
│ (Signals para estado local/UI) │
|
||
|
|
└─────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Ejemplo de Service
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
@Injectable({ providedIn: 'root' })
|
||
|
|
export class StudentService {
|
||
|
|
private apollo = inject(Apollo);
|
||
|
|
|
||
|
|
getStudents() {
|
||
|
|
return this.apollo.watchQuery<{ students: Student[] }>({
|
||
|
|
query: GET_STUDENTS,
|
||
|
|
fetchPolicy: 'cache-and-network'
|
||
|
|
}).valueChanges;
|
||
|
|
}
|
||
|
|
|
||
|
|
createStudent(input: CreateStudentInput) {
|
||
|
|
return this.apollo.mutate<{ createStudent: StudentPayload }>({
|
||
|
|
mutation: CREATE_STUDENT,
|
||
|
|
variables: { input },
|
||
|
|
refetchQueries: [{ query: GET_STUDENTS }]
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Ejemplo de Componente (Signals)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
@Component({
|
||
|
|
selector: 'app-student-list',
|
||
|
|
standalone: true,
|
||
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||
|
|
template: `
|
||
|
|
@if (loading()) {
|
||
|
|
<mat-spinner />
|
||
|
|
} @else {
|
||
|
|
<mat-table [dataSource]="students()">
|
||
|
|
<!-- columns -->
|
||
|
|
</mat-table>
|
||
|
|
}
|
||
|
|
`
|
||
|
|
})
|
||
|
|
export class StudentListComponent {
|
||
|
|
private studentService = inject(StudentService);
|
||
|
|
|
||
|
|
students = signal<Student[]>([]);
|
||
|
|
loading = signal(true);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.studentService.getStudents().subscribe(({ data, loading }) => {
|
||
|
|
this.students.set(data?.students ?? []);
|
||
|
|
this.loading.set(loading);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Rutas (Lazy Loading)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export const routes: Routes = [
|
||
|
|
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
||
|
|
{
|
||
|
|
path: 'students',
|
||
|
|
loadComponent: () => import('./features/students/pages/student-list/student-list.component')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
path: 'students/new',
|
||
|
|
loadComponent: () => import('./features/students/pages/student-form/student-form.component')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
path: 'enrollment/:studentId',
|
||
|
|
loadComponent: () => import('./features/enrollment/pages/enrollment-page/enrollment-page.component')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
path: 'classmates/:studentId',
|
||
|
|
loadComponent: () => import('./features/classmates/pages/classmates-page/classmates-page.component')
|
||
|
|
}
|
||
|
|
];
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. Decisiones de Diseño
|
||
|
|
|
||
|
|
| Decisión | Razón |
|
||
|
|
|----------|-------|
|
||
|
|
| Standalone Components | Angular moderno, tree-shakeable |
|
||
|
|
| Signals vs RxJS | Más simple para estado local, menos boilerplate |
|
||
|
|
| Apollo Cache | Single source of truth para datos del servidor |
|
||
|
|
| OnPush | Mejor rendimiento, menos re-renders |
|
||
|
|
| Lazy Loading | Bundle inicial pequeño |
|