Feuille de chaleur ionique¶
Ionique - Applications mobiles multiplates avec technologies Web
Ionic est un cadre de développement d'applications mobiles open-source qui permet aux développeurs de construire des applications mobiles multiplateforme de haute qualité utilisant des technologies Web comme HTML, CSS et JavaScript. Il fournit des composants et des outils mobiles natifs pour la construction d'applications mobiles hybrides.
Sommaire¶
- [Installation] (#installation)
- [Pour commencer] (#getting-started)
- [Structure du projet] (#project-structure)
- [Composants] (#components)
- [Navigation] (#navigation)
- [Styling] (#styling)
- [Caractéristiques autochtones] (#native-features)
- [Gestion de l'État] (#state-management)
- [HTTP et données] (#http-and-data)
- [Conservation] (#storage)
- [Essais] (#testing)
- [Bâtiment et déploiement] (#building-and-deployment)
- [Intégration des capacités] (#capacitor-integration)
- [Optimisation du rendement] (#performance-optimization)
- [Meilleures pratiques] (#best-practices)
- [Dépannage] (#troubleshooting)
Installation¶
Préalables¶
# Install Node.js (LTS version recommended)
# Download from https://nodejs.org/
# Install npm or yarn
npm --version
yarn --version
CLI ionique Installation¶
# Install Ionic CLI globally
npm install -g @ionic/cli
# Verify installation
ionic --version
# Install Cordova CLI (for Cordova integration)
npm install -g cordova
# Install Capacitor CLI (for Capacitor integration)
npm install -g @capacitor/cli
```_
### Développement Environnement
```bash
# For Android development
# Install Android Studio from https://developer.android.com/studio
# Set up Android SDK and emulator
# For iOS development (macOS only)
# Install Xcode from Mac App Store
# Install CocoaPods
sudo gem install cocoapods
```_
## Commencer
### Créer un nouveau projet
```bash
# Create a new Ionic project
ionic start myApp tabs --type=angular
# Available templates:
# - blank: A blank starter project
# - tabs: A starting project with a simple tabbed interface
# - sidemenu: A starting project with a side menu with navigation
# Framework options:
# - angular (default)
# - react
# - vue
# Create with specific framework
ionic start myApp tabs --type=react
ionic start myApp tabs --type=vue
# Navigate to project directory
cd myApp
# Serve the app in development mode
ionic serve
# Serve with specific port
ionic serve --port=8100
# Serve with lab mode (iOS and Android preview)
ionic serve --lab
Structure du projet¶
myApp/
├── src/
│ ├── app/
│ │ ├── pages/ # Page components
│ │ ├── components/ # Reusable components
│ │ ├── services/ # Services and providers
│ │ ├── models/ # Data models
│ │ └── app.module.ts # Main app module
│ ├── assets/ # Static assets
│ ├── theme/ # Global styles
│ └── index.html # Main HTML file
├── android/ # Android platform files
├── ios/ # iOS platform files
├── www/ # Built web assets
├── ionic.config.json # Ionic configuration
├── capacitor.config.json # Capacitor configuration
└── package.json # Dependencies
Configuration de base de l'application (Angulaire)¶
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent],
})
export class AppModule {}
// src/app/app.component.ts
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(private platform: Platform) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
console.log('Platform is ready');
});
}
}
Composantes¶
Composantes de base¶
// Home page component
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
title = 'Welcome to Ionic';
items = [
{ id: 1, name: 'Item 1', description: 'Description 1' },
{ id: 2, name: 'Item 2', description: 'Description 2' },
{ id: 3, name: 'Item 3', description: 'Description 3' },
];
constructor() {}
onItemClick(item: any) {
console.log('Item clicked:', item);
}
doRefresh(event: any) {
setTimeout(() => {
console.log('Async operation has ended');
event.target.complete();
}, 2000);
}
}
<!-- home.page.html -->
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button>
<ion-icon name="settings-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<!-- Refresher -->
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<!-- Header -->
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<!-- Content -->
<div class="ion-padding">
<!-- Cards -->
<ion-card>
<ion-card-header>
<ion-card-title>Card Title</ion-card-title>
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
This is the card content. You can put any content here.
</ion-card-content>
</ion-card>
<!-- List -->
<ion-list>
<ion-list-header>
<ion-label>Items</ion-label>
</ion-list-header>
<ion-item *ngFor="let item of items" (click)="onItemClick(item)">
<ion-avatar slot="start">
<img src="https://via.placeholder.com/40" />
</ion-avatar>
<ion-label>
<h2>{{ item.name }}</h2>
<p>{{ item.description }}</p>
</ion-label>
<ion-icon name="chevron-forward-outline" slot="end"></ion-icon>
</ion-item>
</ion-list>
<!-- Buttons -->
<ion-button expand="block" fill="solid" color="primary">
Primary Button
</ion-button>
<ion-button expand="block" fill="outline" color="secondary">
Secondary Button
</ion-button>
<!-- Input -->
<ion-item>
<ion-label position="floating">Email</ion-label>
<ion-input type="email" placeholder="Enter email"></ion-input>
</ion-item>
<!-- Checkbox -->
<ion-item>
<ion-checkbox slot="start"></ion-checkbox>
<ion-label>Accept terms and conditions</ion-label>
</ion-item>
<!-- Toggle -->
<ion-item>
<ion-label>Enable notifications</ion-label>
<ion-toggle slot="end"></ion-toggle>
</ion-item>
<!-- Range -->
<ion-item>
<ion-label>Volume</ion-label>
<ion-range min="0" max="100" value="50" slot="end">
<ion-icon name="volume-low" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
</ion-range>
</ion-item>
</div>
<!-- Floating Action Button -->
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button>
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>
Composants avancés¶
// Modal component
import { Component, Input } from '@angular/core';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-modal',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Modal</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<p>{{ data }}</p>
</div>
</ion-content>
`,
})
export class ModalComponent {
@Input() data: string = '';
constructor(private modalController: ModalController) {}
dismiss() {
this.modalController.dismiss();
}
}
// Using modal in a page
import { ModalController } from '@ionic/angular';
import { ModalComponent } from './modal.component';
export class HomePage {
constructor(private modalController: ModalController) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalComponent,
componentProps: {
data: 'Hello from modal!'
}
});
return await modal.present();
}
}
// Popover component
import { Component } from '@angular/core';
import { PopoverController } from '@ionic/angular';
@Component({
selector: 'app-popover',
template: `
<ion-content>
<ion-list>
<ion-item button (click)="close('option1')">
<ion-label>Option 1</ion-label>
</ion-item>
<ion-item button (click)="close('option2')">
<ion-label>Option 2</ion-label>
</ion-item>
</ion-list>
</ion-content>
`,
})
export class PopoverComponent {
constructor(private popoverController: PopoverController) {}
close(data?: any) {
this.popoverController.dismiss(data);
}
}
// Action Sheet
import { ActionSheetController } from '@ionic/angular';
export class HomePage {
constructor(private actionSheetController: ActionSheetController) {}
async presentActionSheet() {
const actionSheet = await this.actionSheetController.create({
header: 'Albums',
cssClass: 'my-custom-class',
buttons: [
{
text: 'Delete',
role: 'destructive',
icon: 'trash',
handler: () => {
console.log('Delete clicked');
}
},
{
text: 'Share',
icon: 'share',
handler: () => {
console.log('Share clicked');
}
},
{
text: 'Cancel',
icon: 'close',
role: 'cancel',
handler: () => {
console.log('Cancel clicked');
}
}
]
});
await actionSheet.present();
}
}
// Alert
import { AlertController } from '@ionic/angular';
export class HomePage {
constructor(private alertController: AlertController) {}
async presentAlert() {
const alert = await this.alertController.create({
header: 'Alert',
subHeader: 'Subtitle',
message: 'This is an alert message.',
buttons: ['OK']
});
await alert.present();
}
async presentConfirm() {
const alert = await this.alertController.create({
header: 'Confirm!',
message: 'Are you sure you want to delete this item?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary',
handler: () => {
console.log('Confirm Cancel');
}
},
{
text: 'Okay',
handler: () => {
console.log('Confirm Okay');
}
}
]
});
await alert.present();
}
}
// Toast
import { ToastController } from '@ionic/angular';
export class HomePage {
constructor(private toastController: ToastController) {}
async presentToast() {
const toast = await this.toastController.create({
message: 'Your settings have been saved.',
duration: 2000,
position: 'bottom'
});
toast.present();
}
}
// Loading
import { LoadingController } from '@ionic/angular';
export class HomePage {
constructor(private loadingController: LoadingController) {}
async presentLoading() {
const loading = await this.loadingController.create({
message: 'Please wait...',
duration: 2000
});
await loading.present();
const { role, data } = await loading.onDidDismiss();
console.log('Loading dismissed!');
}
}
Navigation¶
Navigation du routeur¶
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'home',
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'details/:id',
loadChildren: () => import('./details/details.module').then(m => m.DetailsPageModule)
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }
// Navigation in component
import { Router } from '@angular/router';
import { NavController } from '@ionic/angular';
export class HomePage {
constructor(
private router: Router,
private navController: NavController
) {}
// Using Angular Router
navigateToDetails(id: number) {
this.router.navigate(['/details', id]);
}
// Using Ionic NavController
navigateWithNavController() {
this.navController.navigateForward('/details/123');
}
goBack() {
this.navController.back();
}
}
Navigation des onglets¶
// tabs.page.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage {
constructor() {}
}
<!-- tabs.page.html -->
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home">
<ion-icon name="home"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button tab="search">
<ion-icon name="search"></ion-icon>
<ion-label>Search</ion-label>
</ion-tab-button>
<ion-tab-button tab="profile">
<ion-icon name="person"></ion-icon>
<ion-label>Profile</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
Navigation du menu latéral¶
// app.component.ts
import { Component } from '@angular/core';
import { MenuController } from '@ionic/angular';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
public appPages = [
{ title: 'Home', url: '/home', icon: 'home' },
{ title: 'Profile', url: '/profile', icon: 'person' },
{ title: 'Settings', url: '/settings', icon: 'settings' },
];
constructor(private menu: MenuController) {}
openMenu() {
this.menu.enable(true, 'first');
this.menu.open('first');
}
closeMenu() {
this.menu.close();
}
}
<!-- app.component.html -->
<ion-app>
<ion-split-pane contentId="main-content">
<ion-menu contentId="main-content" type="overlay">
<ion-content>
<ion-list id="inbox-list">
<ion-list-header>Menu</ion-list-header>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages; let i = index">
<ion-item routerDirection="root" [routerLink]="[p.url]" lines="none" detail="false">
<ion-icon slot="start" [name]="p.icon"></ion-icon>
<ion-label>{{ p.title }}</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
</ion-app>
Style¶
CSS Variables et thèmes¶
// src/theme/variables.scss
:root {
/** Primary colors **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** Secondary colors **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** Custom colors **/
--ion-color-custom: #ff6b6b;
--ion-color-custom-rgb: 255, 107, 107;
--ion-color-custom-contrast: #ffffff;
--ion-color-custom-contrast-rgb: 255, 255, 255;
--ion-color-custom-shade: #e05e5e;
--ion-color-custom-tint: #ff7a7a;
}
.ion-color-custom {
--ion-color-base: var(--ion-color-custom);
--ion-color-base-rgb: var(--ion-color-custom-rgb);
--ion-color-contrast: var(--ion-color-custom-contrast);
--ion-color-contrast-rgb: var(--ion-color-custom-contrast-rgb);
--ion-color-shade: var(--ion-color-custom-shade);
--ion-color-tint: var(--ion-color-custom-tint);
}
// Dark mode
@media (prefers-color-scheme: dark) {
:root {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66, 140, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
}
}
Styling des composants¶
// home.page.scss
.welcome-card {
ion-card-header {
background: linear-gradient(135deg, var(--ion-color-primary), var(--ion-color-secondary));
color: white;
}
ion-card-title {
font-size: 1.5rem;
font-weight: bold;
}
}
.custom-button {
--background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
--border-radius: 25px;
--padding-start: 2rem;
--padding-end: 2rem;
margin: 1rem 0;
}
.item-list {
ion-item {
--border-color: var(--ion-color-light-shade);
--padding-start: 1rem;
&:hover {
--background: var(--ion-color-light);
}
}
}
// Responsive design
@media (min-width: 768px) {
.desktop-only {
display: block;
}
.mobile-only {
display: none;
}
}
@media (max-width: 767px) {
.desktop-only {
display: none;
}
.mobile-only {
display: block;
}
}
Styles mondiaux¶
// src/global.scss
// Custom utility classes
.text-center {
text-align: center;
}
.margin-top {
margin-top: 1rem;
}
.padding-horizontal {
padding-left: 1rem;
padding-right: 1rem;
}
// Custom animations
@keyframes slideIn {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideIn 0.3s ease-in-out;
}
// Custom scrollbar
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--ion-color-light);
}
::-webkit-scrollbar-thumb {
background: var(--ion-color-medium);
border-radius: 4px;
}
Caractéristiques autochtones¶
Caméra¶
// Install Capacitor Camera plugin
// npm install @capacitor/camera
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
export class CameraService {
async takePicture() {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
return image.webPath;
} catch (error) {
console.error('Error taking picture:', error);
throw error;
}
}
async selectFromGallery() {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri,
source: CameraSource.Photos
});
return image.webPath;
} catch (error) {
console.error('Error selecting from gallery:', error);
throw error;
}
}
}
Géolocalisation¶
// Install Capacitor Geolocation plugin
// npm install @capacitor/geolocation
import { Geolocation } from '@capacitor/geolocation';
export class LocationService {
async getCurrentPosition() {
try {
const coordinates = await Geolocation.getCurrentPosition();
return {
latitude: coordinates.coords.latitude,
longitude: coordinates.coords.longitude,
accuracy: coordinates.coords.accuracy
};
} catch (error) {
console.error('Error getting location:', error);
throw error;
}
}
async watchPosition() {
const watchId = await Geolocation.watchPosition({
enableHighAccuracy: true,
timeout: 10000
}, (position, err) => {
if (err) {
console.error('Error watching position:', err);
return;
}
console.log('Position updated:', position);
});
return watchId;
}
async clearWatch(watchId: string) {
await Geolocation.clearWatch({ id: watchId });
}
}
Informations sur le périphérique¶
// Install Capacitor Device plugin
// npm install @capacitor/device
import { Device } from '@capacitor/device';
export class DeviceService {
async getDeviceInfo() {
try {
const info = await Device.getInfo();
return {
model: info.model,
platform: info.platform,
operatingSystem: info.operatingSystem,
osVersion: info.osVersion,
manufacturer: info.manufacturer,
isVirtual: info.isVirtual,
webViewVersion: info.webViewVersion
};
} catch (error) {
console.error('Error getting device info:', error);
throw error;
}
}
async getBatteryInfo() {
try {
const info = await Device.getBatteryInfo();
return {
batteryLevel: info.batteryLevel,
isCharging: info.isCharging
};
} catch (error) {
console.error('Error getting battery info:', error);
throw error;
}
}
}
Notifications de poussée¶
// Install Capacitor Push Notifications plugin
// npm install @capacitor/push-notifications
import { PushNotifications } from '@capacitor/push-notifications';
export class PushNotificationService {
async initializePushNotifications() {
// Request permission to use push notifications
await PushNotifications.requestPermissions();
// Register with Apple / Google to receive push via APNS/FCM
await PushNotifications.register();
// On success, we should be able to receive notifications
PushNotifications.addListener('registration', (token) => {
console.log('Push registration success, token: ' + token.value);
});
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError', (error) => {
console.error('Error on registration: ' + JSON.stringify(error));
});
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Push received: ' + JSON.stringify(notification));
});
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed', (notification) => {
console.log('Push action performed: ' + JSON.stringify(notification));
});
}
}
Administration de l ' État¶
Services angulaires¶
// services/data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class DataService {
private usersSubject = new BehaviorSubject<User[]>([]);
public users$: Observable<User[]> = this.usersSubject.asObservable();
private loadingSubject = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor() {}
getUsers(): User[] {
return this.usersSubject.value;
}
setUsers(users: User[]): void {
this.usersSubject.next(users);
}
addUser(user: User): void {
const currentUsers = this.usersSubject.value;
this.usersSubject.next([...currentUsers, user]);
}
updateUser(updatedUser: User): void {
const currentUsers = this.usersSubject.value;
const index = currentUsers.findIndex(user => user.id === updatedUser.id);
if (index !== -1) {
currentUsers[index] = updatedUser;
this.usersSubject.next([...currentUsers]);
}
}
deleteUser(id: number): void {
const currentUsers = this.usersSubject.value;
const filteredUsers = currentUsers.filter(user => user.id !== id);
this.usersSubject.next(filteredUsers);
}
setLoading(loading: boolean): void {
this.loadingSubject.next(loading);
}
}
// Using the service in a component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService, User } from '../services/data.service';
@Component({
selector: 'app-users',
templateUrl: './users.page.html',
styleUrls: ['./users.page.scss'],
})
export class UsersPage implements OnInit, OnDestroy {
users: User[] = [];
loading = false;
private subscriptions: Subscription[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
// Subscribe to users
this.subscriptions.push(
this.dataService.users$.subscribe(users => {
this.users = users;
})
);
// Subscribe to loading state
this.subscriptions.push(
this.dataService.loading$.subscribe(loading => {
this.loading = loading;
})
);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
addUser() {
const newUser: User = {
id: Date.now(),
name: 'New User',
email: 'user@example.com'
};
this.dataService.addUser(newUser);
}
deleteUser(id: number) {
this.dataService.deleteUser(id);
}
}
NgRx (Gestion avancée de l'État)¶
// store/user.actions.ts
import { createAction, props } from '@ngrx/store';
import { User } from '../models/user.model';
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: any }>()
);
export const addUser = createAction(
'[User] Add User',
props<{ user: User }>()
);
// store/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { User } from '../models/user.model';
import * as UserActions from './user.actions';
export interface UserState {
users: User[];
loading: boolean;
error: any;
}
export const initialState: UserState = {
users: [],
loading: false,
error: null
};
export const userReducer = createReducer(
initialState,
on(UserActions.loadUsers, state => ({
...state,
loading: true
})),
on(UserActions.loadUsersSuccess, (state, { users }) => ({
...state,
loading: false,
users
})),
on(UserActions.loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(UserActions.addUser, (state, { user }) => ({
...state,
users: [...state.users, user]
}))
);
// store/user.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { UserState } from './user.reducer';
export const selectUserState = createFeatureSelector<UserState>('users');
export const selectAllUsers = createSelector(
selectUserState,
(state: UserState) => state.users
);
export const selectUsersLoading = createSelector(
selectUserState,
(state: UserState) => state.loading
);
export const selectUsersError = createSelector(
selectUserState,
(state: UserState) => state.error
);
HTTP et données¶
Client HTTP¶
// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, map } from 'rxjs/operators';
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = 'https://api.example.com';
private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
constructor(private http: HttpClient) {}
// GET request
get<T>(endpoint: string): Observable<T> {
return this.http.get<T>(`${this.baseUrl}/${endpoint}`, this.httpOptions)
.pipe(
retry(3),
catchError(this.handleError)
);
}
// POST request
post<T>(endpoint: string, data: any): Observable<T> {
return this.http.post<T>(`${this.baseUrl}/${endpoint}`, data, this.httpOptions)
.pipe(
catchError(this.handleError)
);
}
// PUT request
put<T>(endpoint: string, data: any): Observable<T> {
return this.http.put<T>(`${this.baseUrl}/${endpoint}`, data, this.httpOptions)
.pipe(
catchError(this.handleError)
);
}
// DELETE request
delete<T>(endpoint: string): Observable<T> {
return this.http.delete<T>(`${this.baseUrl}/${endpoint}`, this.httpOptions)
.pipe(
catchError(this.handleError)
);
}
// Error handling
private handleError(error: HttpErrorResponse) {
let errorMessage = 'Unknown error!';
if (error.error instanceof ErrorEvent) {
// Client-side errors
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side errors
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error(errorMessage);
return throwError(errorMessage);
}
// Upload file
uploadFile(endpoint: string, file: File): Observable<any> {
const formData = new FormData();
formData.append('file', file);
return this.http.post(`${this.baseUrl}/${endpoint}`, formData)
.pipe(
catchError(this.handleError)
);
}
}
// Using the API service
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../services/api.service';
import { LoadingController, ToastController } from '@ionic/angular';
@Component({
selector: 'app-data',
templateUrl: './data.page.html',
styleUrls: ['./data.page.scss'],
})
export class DataPage implements OnInit {
data: any[] = [];
constructor(
private apiService: ApiService,
private loadingController: LoadingController,
private toastController: ToastController
) {}
ngOnInit() {
this.loadData();
}
async loadData() {
const loading = await this.loadingController.create({
message: 'Loading data...'
});
await loading.present();
this.apiService.get('users').subscribe({
next: (response: any) => {
this.data = response.data || response;
loading.dismiss();
},
error: async (error) => {
loading.dismiss();
const toast = await this.toastController.create({
message: 'Error loading data: ' + error,
duration: 3000,
color: 'danger'
});
toast.present();
}
});
}
async saveData(item: any) {
const loading = await this.loadingController.create({
message: 'Saving...'
});
await loading.present();
this.apiService.post('users', item).subscribe({
next: async (response) => {
loading.dismiss();
const toast = await this.toastController.create({
message: 'Data saved successfully!',
duration: 2000,
color: 'success'
});
toast.present();
this.loadData(); // Refresh data
},
error: async (error) => {
loading.dismiss();
const toast = await this.toastController.create({
message: 'Error saving data: ' + error,
duration: 3000,
color: 'danger'
});
toast.present();
}
});
}
}
Stockage¶
Stockage ionique¶
// services/storage.service.ts
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private _storage: Storage | null = null;
constructor(private storage: Storage) {
this.init();
}
async init() {
const storage = await this.storage.create();
this._storage = storage;
}
// Store data
public async set(key: string, value: any): Promise<any> {
return this._storage?.set(key, value);
}
// Get data
public async get(key: string): Promise<any> {
return this._storage?.get(key);
}
// Remove data
public async remove(key: string): Promise<any> {
return this._storage?.remove(key);
}
// Clear all data
public async clear(): Promise<void> {
return this._storage?.clear();
}
// Get all keys
public async keys(): Promise<string[]> {
return this._storage?.keys() || [];
}
// Get length
public async length(): Promise<number> {
return this._storage?.length() || 0;
}
// Store object
public async setObject(key: string, object: any): Promise<any> {
return this._storage?.set(key, JSON.stringify(object));
}
// Get object
public async getObject(key: string): Promise<any> {
const data = await this._storage?.get(key);
return data ? JSON.parse(data) : null;
}
}
// app.module.ts
import { IonicStorageModule } from '@ionic/storage-angular';
@NgModule({
imports: [
IonicStorageModule.forRoot()
]
})
export class AppModule {}
// Using storage service
import { Component, OnInit } from '@angular/core';
import { StorageService } from '../services/storage.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
export class SettingsPage implements OnInit {
settings = {
notifications: true,
darkMode: false,
language: 'en'
};
constructor(private storageService: StorageService) {}
async ngOnInit() {
await this.loadSettings();
}
async loadSettings() {
const savedSettings = await this.storageService.getObject('app-settings');
if (savedSettings) {
this.settings = { ...this.settings, ...savedSettings };
}
}
async saveSettings() {
await this.storageService.setObject('app-settings', this.settings);
console.log('Settings saved');
}
async resetSettings() {
await this.storageService.remove('app-settings');
this.settings = {
notifications: true,
darkMode: false,
language: 'en'
};
console.log('Settings reset');
}
}
Préférences du catalyseur¶
// Install Capacitor Preferences
// npm install @capacitor/preferences
import { Preferences } from '@capacitor/preferences';
export class PreferencesService {
// Set a value
async setValue(key: string, value: string): Promise<void> {
await Preferences.set({
key: key,
value: value,
});
}
// Get a value
async getValue(key: string): Promise<string | null> {
const { value } = await Preferences.get({ key: key });
return value;
}
// Remove a value
async removeValue(key: string): Promise<void> {
await Preferences.remove({ key: key });
}
// Clear all values
async clearAll(): Promise<void> {
await Preferences.clear();
}
// Get all keys
async getAllKeys(): Promise<string[]> {
const { keys } = await Preferences.keys();
return keys;
}
// Store object
async setObject(key: string, value: any): Promise<void> {
await this.setValue(key, JSON.stringify(value));
}
// Get object
async getObject(key: string): Promise<any> {
const value = await this.getValue(key);
return value ? JSON.parse(value) : null;
}
}
Essais¶
Essai en unité¶
// home.page.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HomePage],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have correct title', () => {
expect(component.title).toBe('Welcome to Ionic');
});
it('should handle item click', () => {
spyOn(console, 'log');
const item = { id: 1, name: 'Test Item' };
component.onItemClick(item);
expect(console.log).toHaveBeenCalledWith('Item clicked:', item);
});
});
// Service testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
service = TestBed.inject(ApiService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should fetch users', () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' }
];
service.get('users').subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
E2E Essais¶
// e2e/src/app.e2e-spec.ts
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('new App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toContain('Welcome to Ionic');
});
it('should navigate to details page', () => {
page.navigateTo();
page.clickFirstItem();
expect(browser.getCurrentUrl()).toContain('/details');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
// e2e/src/app.po.ts
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-home ion-title')).getText() as Promise<string>;
}
clickFirstItem(): Promise<void> {
return element(by.css('app-home ion-item:first-child')).click() as Promise<void>;
}
}
Construction et déploiement¶
Construire des commandes¶
# Build for production
ionic build --prod
# Build with specific configuration
ionic build --configuration=staging
# Build for specific platform
ionic capacitor build android
ionic capacitor build ios
# Build and run
ionic capacitor run android
ionic capacitor run ios
# Build with live reload
ionic capacitor run android --livereload --external
ionic capacitor run ios --livereload --external
Déploiement Android¶
# Add Android platform
ionic capacitor add android
# Sync changes
ionic capacitor sync android
# Open in Android Studio
ionic capacitor open android
# Build APK
cd android
./gradlew assembleDebug
./gradlew assembleRelease
# Build AAB (Android App Bundle)
./gradlew bundleRelease
# Install on device
adb install app/build/outputs/apk/debug/app-debug.apk
Déploiement iOS¶
# Add iOS platform
ionic capacitor add ios
# Sync changes
ionic capacitor sync ios
# Open in Xcode
ionic capacitor open ios
# Build from command line
xcodebuild -workspace ios/App/App.xcworkspace -scheme App -configuration Release -destination generic/platform=iOS -archivePath ios/App/App.xcarchive archive
# Export IPA
xcodebuild -exportArchive -archivePath ios/App/App.xcarchive -exportPath ios/App/App.ipa -exportOptionsPlist ios/App/ExportOptions.plist
Déploiement du Web¶
# Build for web
ionic build --prod
# Deploy to Firebase Hosting
npm install -g firebase-tools
firebase login
firebase init hosting
firebase deploy
# Deploy to Netlify
npm install -g netlify-cli
netlify deploy --prod --dir=www
# Deploy to Vercel
npm install -g vercel
vercel --prod
Intégration des capacités¶
Configuration du catalyseur¶
// capacitor.config.json
{
"appId": "com.example.myapp",
"appName": "My App",
"webDir": "www",
"bundledWebRuntime": false,
"plugins": {
"Camera": {
"permissions": ["camera", "photos"]
},
"Geolocation": {
"permissions": ["location"]
},
"PushNotifications": {
"presentationOptions": ["badge", "sound", "alert"]
}
},
"server": {
"androidScheme": "https"
}
}
Plugin Capaciteur personnalisé¶
// Create a custom plugin
npm init @capacitor/plugin my-plugin
// src/definitions.ts
export interface MyPluginPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
getDeviceId(): Promise<{ deviceId: string }>;
}
// src/web.ts
import { WebPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';
export class MyPluginWeb extends WebPlugin implements MyPluginPlugin {
async echo(options: { value: string }): Promise<{ value: string }> {
console.log('ECHO', options);
return options;
}
async getDeviceId(): Promise<{ deviceId: string }> {
return { deviceId: 'web-device-id' };
}
}
// src/index.ts
import { registerPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';
const MyPlugin = registerPlugin<MyPluginPlugin>('MyPlugin', {
web: () => import('./web').then(m => new m.MyPluginWeb()),
});
export * from './definitions';
export { MyPlugin };
// Using the plugin
import { MyPlugin } from 'my-plugin';
export class HomePage {
async usePlugin() {
const result = await MyPlugin.echo({ value: 'Hello World' });
console.log(result.value);
const deviceInfo = await MyPlugin.getDeviceId();
console.log(deviceInfo.deviceId);
}
}
Optimisation des performances¶
Chargement paresseux¶
// app-routing.module.ts
const routes: Routes = [
{
path: 'home',
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'profile',
loadChildren: () => import('./profile/profile.module').then(m => m.ProfilePageModule)
}
];
// Preloading strategy
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
],
exports: [RouterModule]
})
export class AppRoutingModule { }
Le défilement virtuel¶
<!-- For large lists -->
<ion-content>
<ion-virtual-scroll [items]="items" approxItemHeight="70px">
<ion-item *virtualItem="let item; let itemBounds = bounds;">
<ion-avatar slot="start">
<img [src]="item.avatar" />
</ion-avatar>
<ion-label>
<h2>{{ item.name }}</h2>
<p>{{ item.description }}</p>
</ion-label>
</ion-item>
</ion-virtual-scroll>
</ion-content>
Optimisation de l'image¶
// Lazy loading images
export class ImageOptimizationPage {
images = [
{ src: 'assets/images/image1.jpg', loaded: false },
{ src: 'assets/images/image2.jpg', loaded: false },
{ src: 'assets/images/image3.jpg', loaded: false }
];
onImageLoad(image: any) {
image.loaded = true;
}
onImageError(image: any) {
image.src = 'assets/images/placeholder.jpg';
image.loaded = true;
}
}
<!-- Lazy loading template -->
<ion-content>
<div *ngFor="let image of images" class="image-container">
<img
[src]="image.src"
[class.loaded]="image.loaded"
(load)="onImageLoad(image)"
(error)="onImageError(image)"
loading="lazy"
/>
<ion-spinner *ngIf="!image.loaded" name="crescent"></ion-spinner>
</div>
</ion-content>
Analyse de l'ensemble¶
# Analyze bundle size
npm install -g webpack-bundle-analyzer
# Build with stats
ionic build --prod --stats-json
# Analyze
npx webpack-bundle-analyzer www/stats.json
Meilleures pratiques¶
Code Organisation¶
src/
├── app/
│ ├── core/ # Core functionality (guards, interceptors)
│ ├── shared/ # Shared components, pipes, directives
│ ├── features/ # Feature modules
│ │ ├── auth/ # Authentication feature
│ │ ├── profile/ # Profile feature
│ │ └── settings/ # Settings feature
│ ├── services/ # Global services
│ ├── models/ # Data models and interfaces
│ ├── utils/ # Utility functions
│ └── constants/ # App constants
├── assets/ # Static assets
├── environments/ # Environment configurations
└── theme/ # Global styles and themes
Lignes directrices en matière de résultats¶
// Use OnPush change detection strategy
@Component({
selector: 'app-optimized',
templateUrl: './optimized.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
@Input() data: any;
constructor(private cdr: ChangeDetectorRef) {}
updateData(newData: any) {
this.data = newData;
this.cdr.markForCheck();
}
}
// Use trackBy functions for ngFor
trackByFn(index: number, item: any): any {
return item.id || index;
}
// Unsubscribe from observables
export class ComponentWithSubscriptions implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
// Handle data
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// Use async pipe when possible
@Component({
template: `
<div *ngFor="let item of items$ | async">
{{ item.name }}
</div>
`
})
export class AsyncPipeComponent {
items$ = this.dataService.getItems();
constructor(private dataService: DataService) {}
}
Pratiques exemplaires en matière de sécurité¶
// Sanitize user input
import { DomSanitizer } from '@angular/platform-browser';
export class SecurityComponent {
constructor(private sanitizer: DomSanitizer) {}
sanitizeHtml(html: string) {
return this.sanitizer.sanitize(SecurityContext.HTML, html);
}
sanitizeUrl(url: string) {
return this.sanitizer.sanitize(SecurityContext.URL, url);
}
}
// Use environment variables for sensitive data
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.production.com',
apiKey: process.env['API_KEY'] // Use environment variables
};
// Implement proper error handling
export class ErrorHandlerService {
handleError(error: any): void {
// Log error to external service
console.error('An error occurred:', error);
// Don't expose sensitive information
const userMessage = this.getUserFriendlyMessage(error);
this.showToast(userMessage);
}
private getUserFriendlyMessage(error: any): string {
if (error.status === 404) {
return 'Resource not found';
} else if (error.status === 500) {
return 'Server error. Please try again later.';
}
return 'An unexpected error occurred';
}
}
Dépannage¶
Questions et solutions communes¶
Créer des problèmes¶
# Clear cache and reinstall
rm -rf node_modules
rm package-lock.json
npm install
# Clear Ionic cache
ionic cache clear
# Reset Capacitor
npx cap clean
npx cap sync
Questions spécifiques à la plate-forme¶
# Android issues
# Clean and rebuild
cd android
./gradlew clean
cd ..
ionic capacitor sync android
# iOS issues
# Clean derived data
rm -rf ~/Library/Developer/Xcode/DerivedData
cd ios/App
pod install
cd ../..
ionic capacitor sync ios
Erreurs d'exécution¶
// Handle platform-specific code
import { Platform } from '@ionic/angular';
export class PlatformService {
constructor(private platform: Platform) {}
isNative(): boolean {
return this.platform.is('capacitor');
}
isWeb(): boolean {
return !this.platform.is('capacitor');
}
isIOS(): boolean {
return this.platform.is('ios');
}
isAndroid(): boolean {
return this.platform.is('android');
}
async runOnPlatform(callback: () => void): Promise<void> {
await this.platform.ready();
callback();
}
}
// Error boundary component
@Component({
selector: 'app-error-boundary',
template: `
<div *ngIf="hasError; else content" class="error-container">
<ion-icon name="warning-outline"></ion-icon>
<h2>Something went wrong</h2>
<p>{{ errorMessage }}</p>
<ion-button (click)="retry()">Try Again</ion-button>
</div>
<ng-template #content>
<ng-content></ng-content>
</ng-template>
`
})
export class ErrorBoundaryComponent {
hasError = false;
errorMessage = '';
@HostListener('window:error', ['$event'])
handleError(event: ErrorEvent) {
this.hasError = true;
this.errorMessage = event.message || 'An unexpected error occurred';
}
retry() {
this.hasError = false;
this.errorMessage = '';
window.location.reload();
}
}
Résumé¶
Ionic est un cadre puissant pour la construction d'applications mobiles multiplateforme utilisant des technologies web. Ses principaux avantages sont les suivants :
- Platforme de choc Développement: Écrivez une fois, exécutez sur iOS, Android et web
- Technologies Web: Utilisez HTML familier, CSS et JavaScript/TypeScript
- Native Performance: Accédez aux fonctionnalités de l'appareil natif via Capacitor
- Rich UI Components: Bibliothèque complète de composants optimisés par mobile
- ** Flexibilité du cadre**: Fonctionne avec Angulaire, Réaction et Vue
- Développement rapide: Cycle de développement rapide avec recharge en direct
- Strong Ecosystem: Soutien étendu aux écosystèmes et aux communautés
- Coût effectif: Réduire le temps de développement et les coûts de maintenance
Ionic excelle à permettre aux développeurs web de construire des applications mobiles de qualité native tout en tirant parti des compétences de développement web existantes et en maintenant une base de code unique sur plusieurs plateformes.