taller-ionic/cap6.md
2025-04-24 15:57:53 -04:00

711 lines
20 KiB
Markdown

# Capítulo 6: Estructura del Backend y Despliegue de la Aplicación
En este capítulo, exploraremos la estructura del backend que hemos implementado para nuestra aplicación de reservas de gimnasio y el proceso de despliegue para dispositivos móviles reales.
## 1. Arquitectura del Backend
Nuestra aplicación ahora cuenta con un backend real basado en Node.js, Express y PostgreSQL que gestiona todas las operaciones de datos. Este enfoque tiene varias ventajas sobre el almacenamiento local:
- Datos centralizados y accesibles desde cualquier dispositivo
- Mayor seguridad y control de acceso
- Escalabilidad mejorada para manejar más usuarios
- Sincronización de datos en tiempo real
### Estructura de Carpetas del Backend
```
backend/
├── config/
│ └── db.js # Configuración de la base de datos
├── controllers/
│ ├── authController.js # Controlador de autenticación
│ ├── bookingController.js # Controlador de reservas
│ ├── gymClassController.js # Controlador de clases
│ ├── uploadController.js # Controlador para subida de archivos
│ └── userController.js # Controlador de usuarios
├── models/
│ ├── Booking.js # Modelo de datos para reservas
│ ├── GymClass.js # Modelo de datos para clases
│ ├── User.js # Modelo de datos para usuarios
│ └── index.js # Exporta todos los modelos
├── routes/
│ ├── authRoutes.js # Rutas de autenticación
│ ├── bookingRoutes.js # Rutas de reservas
│ ├── gymClassRoutes.js # Rutas de clases
│ ├── uploadRoutes.js # Rutas para subida de archivos
│ └── userRoutes.js # Rutas de usuarios
├── uploads/ # Directorio para archivos subidos
├── server.js # Punto de entrada principal
└── package.json # Dependencias y scripts
```
### Tecnologías Utilizadas en el Backend
- **Node.js**: Entorno de ejecución JavaScript del lado del servidor
- **Express**: Framework web para crear APIs REST
- **PostgreSQL**: Base de datos relacional robusta para almacenar los datos
- **Sequelize**: ORM para modelar los datos y conectar con PostgreSQL
- **JWT (JSON Web Tokens)**: Para gestionar la autenticación de usuarios
- **Multer**: Para manejar la subida de archivos
## 2. Implementación del Backend
### Configuración de la Base de Datos
**config/db.js**:
```javascript
const { Sequelize } = require('sequelize');
const dotenv = require('dotenv');
dotenv.config();
// Create Sequelize instance
let sequelize;
if (process.env.DATABASE_URL) {
// Use connection string if available
sequelize = new Sequelize(process.env.DATABASE_URL, {
logging: process.env.NODE_ENV === 'development' ? console.log : false,
define: {
freezeTableName: true // Prevent Sequelize from pluralizing table names
},
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false
}
},
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
});
} else {
// Fall back to individual parameters
sequelize = new Sequelize(
process.env.DB_NAME || 'postgres',
process.env.DB_USER || 'postgres',
process.env.DB_PASSWORD || 'postgres',
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: process.env.NODE_ENV === 'development' ? console.log : false,
define: {
freezeTableName: true // Prevent Sequelize from pluralizing table names
},
dialectOptions: {
ssl: process.env.NODE_ENV === 'production' ? {
require: true,
rejectUnauthorized: false
} : false
},
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
);
}
// Test the connection
const connectDB = async () => {
try {
await sequelize.authenticate();
console.log('Database connection established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
process.exit(1);
}
};
module.exports = { sequelize, connectDB };
```
### Modelos de Datos
**models/User.js**:
```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');
const bcrypt = require('bcryptjs');
const User = sequelize.define('user', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
profilePicUrl: {
type: DataTypes.STRING,
defaultValue: '/uploads/default-avatar.jpg'
},
notificationsEnabled: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
hooks: {
beforeCreate: async (user) => {
if (user.password) {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
}
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
}
}
},
tableName: 'users' // Explicitly set lowercase table name
});
// Method to check if password matches
User.prototype.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = User;
```
**models/GymClass.js**:
```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');
const GymClass = sequelize.define('gymClass', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
instructor: {
type: DataTypes.STRING,
allowNull: false
},
startTime: {
type: DataTypes.DATE,
allowNull: false
},
endTime: {
type: DataTypes.DATE,
allowNull: false
},
maxCapacity: {
type: DataTypes.INTEGER,
allowNull: false
},
currentBookings: {
type: DataTypes.INTEGER,
defaultValue: 0
},
category: {
type: DataTypes.STRING,
defaultValue: 'Otros'
},
imageUrl: {
type: DataTypes.STRING,
defaultValue: '/uploads/default-class.jpg'
}
}, {
tableName: 'classes' // Explicitly set lowercase table name
});
module.exports = GymClass;
```
**models/Booking.js**:
```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');
const Booking = sequelize.define('booking', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
classId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'classes',
key: 'id'
}
},
className: {
type: DataTypes.STRING,
allowNull: false
},
date: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
status: {
type: DataTypes.ENUM('confirmed', 'cancelled', 'pending'),
defaultValue: 'confirmed'
}
}, {
tableName: 'bookings' // Explicitly set lowercase table name
});
module.exports = Booking;
```
**models/index.js**:
```javascript
const User = require('./User');
const GymClass = require('./GymClass');
const Booking = require('./Booking');
// Define associations
User.hasMany(Booking, { foreignKey: 'userId' });
Booking.belongsTo(User, { foreignKey: 'userId' });
GymClass.hasMany(Booking, { foreignKey: 'classId' });
Booking.belongsTo(GymClass, { foreignKey: 'classId' });
module.exports = {
User,
GymClass,
Booking
};
```
### Controladores
**controllers/authController.js**:
```javascript
const jwt = require('jsonwebtoken');
const { User } = require('../models');
// Generate JWT Token
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET || 'secret_key_12345', {
expiresIn: '30d'
});
};
// @desc Register a new user
// @route POST /api/auth/register
// @access Public
exports.registerUser = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'Email is already registered'
});
}
// Create user
const user = await User.create({
name,
email,
password
});
// Generate token
const token = generateToken(user.id);
res.status(201).json({
success: true,
token,
user: {
id: user.id,
name: user.name,
email: user.email,
profilePicUrl: user.profilePicUrl,
notificationsEnabled: user.notificationsEnabled
}
});
} catch (error) {
console.error('Error registering user:', error);
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Login user
// @route POST /api/auth/login
// @access Public
exports.loginUser = async (req, res) => {
try {
const { email, password } = req.body;
// Check for user email
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check if password matches
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Generate token
const token = generateToken(user.id);
res.json({
success: true,
token,
user: {
id: user.id,
name: user.name,
email: user.email,
profilePicUrl: user.profilePicUrl,
notificationsEnabled: user.notificationsEnabled
}
});
} catch (error) {
console.error('Error logging in:', error);
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
```
### Rutas API
**routes/authRoutes.js**:
```javascript
const express = require('express');
const { registerUser, loginUser } = require('../controllers/authController');
const router = express.Router();
router.post('/register', registerUser);
router.post('/login', loginUser);
module.exports = router;
```
### Punto de Entrada del Servidor
**server.js**:
```javascript
const path = require('path');
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const { connectDB, sequelize } = require('./config/db');
// Load env variables
dotenv.config();
// Import route files
const userRoutes = require('./routes/userRoutes');
const gymClassRoutes = require('./routes/gymClassRoutes');
const bookingRoutes = require('./routes/bookingRoutes');
const uploadRoutes = require('./routes/uploadRoutes');
const authRoutes = require('./routes/authRoutes');
// Initialize express app
const app = express();
// Middleware
app.use(express.json());
app.use(cors());
// Dev logging middleware
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Set static folder for uploads
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Mount routers
app.use('/api/users', userRoutes);
app.use('/api/classes', gymClassRoutes);
app.use('/api/bookings', bookingRoutes);
app.use('/api/upload', uploadRoutes);
app.use('/api/auth', authRoutes);
// Root route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to Gym API' });
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
message: 'Server Error',
error: err.message
});
});
// Define port
const PORT = process.env.PORT || 3000;
// Connect to database and start server
const startServer = async () => {
try {
// Connect to database
await connectDB();
await sequelize.sync();
console.log('Database synchronized');
// Start server
app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
} catch (error) {
console.error('Unable to start server:', error);
process.exit(1);
}
};
startServer();
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.log(`Error: ${err.message}`);
// Close server & exit process
process.exit(1);
});
```
## 3. Despliegue en Plataformas Móviles
Una de las grandes ventajas de usar Ionic con Capacitor es la posibilidad de desplegar la misma base de código en múltiples plataformas. Vamos a revisar el proceso para Android.
### Preparación para Android
1. **Instalar las Dependencias de Android**:
- Android Studio
- JDK (Java Development Kit)
- Configurar variables de entorno (ANDROID_HOME, JAVA_HOME)
2. **Añadir la Plataforma Android a nuestro Proyecto**:
```bash
# Instalar @capacitor/android si no está instalado
npm install @capacitor/android
# Añadir Android al proyecto
npx cap add android
```
3. **Sincronizar el Proyecto**:
Después de hacer cambios en el código web, necesitamos construir el proyecto y sincronizarlo con las plataformas nativas:
```bash
# Construir la aplicación
ionic build --prod
# Sincronizar con Android
npx cap sync android
```
4. **Abrir el Proyecto en Android Studio**:
```bash
npx cap open android
```
### Configuración del AndroidManifest.xml
El archivo `AndroidManifest.xml` es crucial para definir los permisos y configuraciones de la aplicación Android. Ya cubrimos algunos permisos esenciales:
```xml
<!-- Permisos para cámara -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<!-- Permisos para notificaciones -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
```
### Personalización de Iconos y Splash Screen
Para personalizar los iconos y la pantalla de inicio:
1. **Reemplazar los Archivos de Iconos**:
- Crear iconos para diferentes resoluciones
- Reemplazar los archivos en `android/app/src/main/res/mipmap-*/`
2. **Reemplazar la Pantalla de Inicio (Splash Screen)**:
- Crear imágenes para diferentes orientaciones y resoluciones
- Reemplazar los archivos en `android/app/src/main/res/drawable-*/splash.png`
3. **Personalizar el Nombre de la Aplicación**:
- Editar `android/app/src/main/res/values/strings.xml`:
```xml
<resources>
<string name="app_name">Gym Reservation</string>
<string name="title_activity_main">Gym Reservation</string>
<string name="package_name">com.example.gymreservation</string>
<string name="custom_url_scheme">com.example.gymreservation</string>
</resources>
```
### Construir un APK de Prueba
Para generar un APK que puedas compartir con probadores:
1. En Android Studio, selecciona **Build > Build Bundle(s) / APK(s) > Build APK(s)**
2. Cuando termine, haz clic en **locate** para encontrar el archivo APK
3. Este archivo se puede compartir directamente para instalarlo en dispositivos Android (se debe habilitar la instalación desde fuentes desconocidas)
### Construir un Bundle para Google Play
Para publicar en la tienda de Google Play:
1. En Android Studio, selecciona **Build > Generate Signed Bundle / APK**
2. Elige **Android App Bundle**
3. Crea o selecciona una clave de firma (keystore)
4. Completa la información necesaria y genera el bundle (.aab)
5. Este archivo es el que se sube a la Google Play Console
## 4. Consideraciones de Seguridad
Al desplegar una aplicación móvil que se comunica con un backend, es crucial considerar aspectos de seguridad:
### Autenticación Segura
Nuestra aplicación implementa JWT (JSON Web Tokens) para autenticar usuarios, pero aquí hay algunas mejoras adicionales:
- **Tokens de Actualización**: Implementar tokens de corta duración con tokens de actualización
- **Almacenamiento Seguro**: Utilizar almacenamiento nativo seguro para tokens
- **HTTPS**: Garantizar que todas las comunicaciones API usen HTTPS
### Manejo de Datos Sensibles
- **No almacenar contraseñas en texto plano**: Ya estamos usando bcrypt para hash de contraseñas
- **Minimizar los datos almacenados localmente**: Solo guardar lo necesario en el dispositivo
- **Eliminar datos al cerrar sesión**: Limpiar datos sensibles cuando el usuario cierre sesión
### Permisos de Aplicación
- **Solicitar solo los permisos necesarios**: No solicitar permisos que no sean esenciales
- **Explicar por qué se necesitan permisos**: Proporcionar contexto cuando se solicitan permisos
- **Manejar denegaciones graciosamente**: La aplicación debe funcionar aunque se denieguen permisos no críticos
## 5. Optimizaciones de Rendimiento
Para garantizar que nuestra aplicación funcione de manera óptima en dispositivos reales:
### Optimización de Imágenes
- **Redimensionar imágenes**: Utilizar tamaños apropiados para dispositivos móviles
- **Compresión de imágenes**: Reducir el tamaño de archivo sin perder calidad visible
- **Carga progresiva**: Implementar carga progresiva para imágenes grandes
### Optimización de Red
- **Implementar caché**: Almacenar en caché respuestas de API para reducir solicitudes de red
- **Carga perezosa (lazy loading)**: Cargar datos solo cuando sean necesarios
- **Compresión de datos**: Utilizar gzip en el servidor para respuestas API
### Optimización de la UI
- **Virtualización de listas**: Para listas largas, solo renderizar elementos visibles
- **Animaciones eficientes**: Usar animaciones CSS en lugar de JavaScript cuando sea posible
- **Evitar work blocking**: No bloquear el hilo principal con operaciones pesadas
## 6. Pruebas y Depuración
Para identificar y resolver problemas en dispositivos reales:
### Herramientas de Depuración
- **Chrome Remote Debugging**: Para depurar WebView en dispositivos Android
- **Safari Web Inspector**: Para depurar WebView en dispositivos iOS
- **Capacitor Logs**: Usar `npx cap logs android` para ver logs en tiempo real
### Pruebas en Dispositivos Reales
- **Probar en diferentes tamaños de pantalla**: Asegurar que la UI sea responsiva
- **Probar con diferentes velocidades de red**: Simular conexiones lentas
- **Pruebas de batería**: Verificar que la aplicación no consuma demasiada batería
## Resumen
En este capítulo, hemos explorado:
1. La arquitectura de nuestro backend Node.js/Express/PostgreSQL
2. Los detalles de implementación de los modelos, controladores y rutas del backend
3. El proceso de despliegue para dispositivos Android
4. Consideraciones de seguridad para aplicaciones móviles
5. Técnicas de optimización de rendimiento
6. Herramientas y enfoques para pruebas y depuración
Con esto, nuestra aplicación Gym Reservation está lista para ser utilizada por usuarios reales, con un backend robusto y una experiencia móvil optimizada.