|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
1. Ionic 4简介
Ionic是一个开源的移动应用开发框架,允许开发者使用Web技术(HTML、CSS和JavaScript)构建跨平台的移动应用。Ionic 4是这个框架的一个重要版本,它带来了许多改进和新特性。
1.1 Ionic 4的特点
• 基于Web Components:Ionic 4完全基于Web Components标准,使其更加模块化和可重用。
• 框架无关:Ionic 4不再仅限于Angular,支持与React、Vue等主流前端框架集成。
• 性能优化:相比之前的版本,Ionic 4在性能方面有显著提升。
• 改进的UI组件:提供了更丰富、更美观的UI组件库。
• 更好的工具支持:增强的CLI工具和开发体验。
1.2 跨平台开发的优势
使用Ionic 4进行跨平台开发有以下优势:
• 一次编写,多平台运行:同一套代码可以部署到iOS、Android和Web平台。
• 降低开发成本:无需为每个平台单独维护代码库。
• 快速开发:利用Web技术栈,大多数前端开发者可以快速上手。
• 丰富的生态系统:可以访问大量的Cordova和Capacitor插件,调用原生设备功能。
2. 开发环境安装
在开始Ionic 4开发之前,我们需要安装和配置必要的开发环境。
2.1 安装Node.js和npm
Ionic 4需要Node.js环境。请访问Node.js官网下载并安装最新的LTS版本。
安装完成后,打开终端或命令提示符,验证安装:
2.2 安装Ionic CLI
Ionic CLI是开发Ionic应用的命令行工具。使用npm全局安装:
- npm install -g @ionic/cli
复制代码
验证安装:
2.3 安装其他必要工具
Cordova和Capacitor是Ionic的运行时环境,用于访问原生设备功能。Ionic 4默认使用Capacitor,但也支持Cordova。
- # 安装Capacitor
- npm install -g @capacitor/cli @capacitor/core
- # 或安装Cordova
- npm install -g cordova
复制代码
推荐使用Visual Studio Code作为开发环境,它提供了丰富的插件支持:
- # 安装VS Code(根据操作系统选择相应的安装方式)
- # Windows: 下载并运行安装程序
- # Mac: 使用Homebrew安装
- brew cask install visual-studio-code
- # Linux: 根据发行版选择相应的安装方式
复制代码
安装VS Code后,推荐安装以下插件:
• Ionic Snippets
• Angular Essentials
• TSLint
• Prettier - Code formatter
3. 创建第一个Ionic 4项目
现在我们已经安装了必要的工具,让我们创建第一个Ionic 4项目。
3.1 使用Ionic CLI创建项目
在终端中运行以下命令:
- ionic start myFirstApp blank
复制代码
这个命令会创建一个名为”myFirstApp”的新项目,使用”blank”模板。Ionic提供了多种模板:
• blank:空模板,适合从零开始
• tabs:带有标签页的模板
• sidemenu:带有侧边菜单的模板
创建过程中,CLI会询问一些问题:
• 是否集成Angular框架?(选择Yes)
• 是否使用Capacitor?(选择Yes)
3.2 项目结构解析
创建完成后,进入项目目录:
查看项目结构:
- myFirstApp/
- ├── e2e/ # 端到端测试文件
- ├── node_modules/ # 依赖包
- ├── src/ # 源代码
- │ ├── app/ # 应用根目录
- │ │ ├── app.module.ts # 应用根模块
- │ │ └── app-routing.module.ts # 路由配置
- │ ├── assets/ # 静态资源
- │ ├── environments/ # 环境配置
- │ ├── theme/ # 主题文件
- │ └── pages/ # 页面组件
- ├── www/ # 构建输出目录
- ├── angular.json # Angular配置文件
- ├── capacitor.config.json # Capacitor配置文件
- ├── ionic.config.json # Ionic配置文件
- ├── package.json # 项目依赖和脚本
- ├── tsconfig.json # TypeScript配置
- └── tslint.json # TSLint配置
复制代码
3.3 运行应用
在浏览器中运行应用:
这个命令会启动开发服务器,并在默认浏览器中打开应用。应用会自动监听文件变化并刷新。
4. Ionic 4基本组件和布局
Ionic 4提供了丰富的UI组件,让我们可以快速构建美观的移动应用界面。
4.1 基本布局组件
IonContent是应用的主要内容区域,通常包含可滚动的内容。
- <ion-content>
- <p>这里是应用的主要内容</p>
- </ion-content>
复制代码
IonHeader是应用的固定头部,通常包含IonToolbar。
- <ion-header>
- <ion-toolbar>
- <ion-title>我的应用</ion-title>
- </ion-toolbar>
- </ion-header>
复制代码
IonFooter是应用的固定底部。
- <ion-footer>
- <ion-toolbar>
- <ion-title>底部信息</ion-title>
- </ion-toolbar>
- </ion-footer>
复制代码
4.2 常用UI组件
按钮组件,支持多种样式和功能。
- <!-- 基本按钮 -->
- <ion-button>默认按钮</ion-button>
- <!-- 不同颜色的按钮 -->
- <ion-button color="primary">主要按钮</ion-button>
- <ion-button color="secondary">次要按钮</ion-button>
- <ion-button color="danger">危险按钮</ion-button>
- <!-- 不同大小的按钮 -->
- <ion-button size="small">小按钮</ion-button>
- <ion-button size="large">大按钮</ion-button>
- <!-- 块级按钮 -->
- <ion-button expand="block">块级按钮</ion-button>
- <!-- 圆形按钮 -->
- <ion-button shape="round">圆形按钮</ion-button>
- <!-- 带图标的按钮 -->
- <ion-button>
- <ion-icon slot="start" name="star"></ion-icon>
- 收藏
- </ion-button>
复制代码
列表组件,用于显示数据列表。
- <ion-list>
- <ion-item>
- <ion-label>项目1</ion-label>
- </ion-item>
- <ion-item>
- <ion-label>项目2</ion-label>
- </ion-item>
- <ion-item>
- <ion-label>项目3</ion-label>
- </ion-item>
- </ion-list>
复制代码
卡片组件,用于展示相关信息的集合。
- <ion-card>
- <ion-card-header>
- <ion-card-subtitle>卡片副标题</ion-card-subtitle>
- <ion-card-title>卡片标题</ion-card-title>
- </ion-card-header>
- <ion-card-content>
- 这里是卡片的内容,可以包含文本、图片等任何内容。
- </ion-card-content>
- </ion-card>
复制代码
网格系统,用于创建响应式布局。
- <ion-grid>
- <ion-row>
- <ion-col>
- 列1
- </ion-col>
- <ion-col>
- 列2
- </ion-col>
- </ion-row>
- <ion-row>
- <ion-col size="8">
- 宽度为8的列
- </ion-col>
- <ion-col size="4">
- 宽度为4的列
- </ion-col>
- </ion-row>
- </ion-grid>
复制代码
4.3 表单组件
输入框组件,用于接收用户输入。
- <ion-item>
- <ion-label position="floating">用户名</ion-label>
- <ion-input type="text" [(ngModel)]="username"></ion-input>
- </ion-item>
复制代码
多行文本输入组件。
- <ion-item>
- <ion-label position="floating">描述</ion-label>
- <ion-textarea [(ngModel)]="description"></ion-textarea>
- </ion-item>
复制代码
选择器组件,用于从多个选项中选择一个或多个值。
- <ion-item>
- <ion-label>性别</ion-label>
- <ion-select [(ngModel)]="gender">
- <ion-select-option value="male">男</ion-select-option>
- <ion-select-option value="female">女</ion-select-option>
- <ion-select-option value="other">其他</ion-select-option>
- </ion-select>
- </ion-item>
复制代码
开关组件,用于切换开关状态。
- <ion-item>
- <ion-label>启用通知</ion-label>
- <ion-toggle [(ngModel)]="enableNotifications"></ion-toggle>
- </ion-item>
复制代码
复选框组件。
- <ion-item>
- <ion-label>记住我</ion-label>
- <ion-checkbox [(ngModel)]="rememberMe"></ion-checkbox>
- </ion-item>
复制代码
5. 页面导航和路由
在Ionic 4应用中,页面导航和路由是非常重要的部分。Ionic 4使用Angular的路由系统来实现页面之间的导航。
5.1 创建新页面
使用Ionic CLI创建新页面:
- ionic generate page pages/about
复制代码
这个命令会在src/app/pages目录下创建一个名为about的新页面,包含以下文件:
• about.page.html:页面的HTML模板
• about.page.scss:页面的样式文件
• about.page.ts:页面的TypeScript逻辑文件
• about.module.ts:页面的模块文件
5.2 配置路由
在Ionic 4中,路由配置通常在app-routing.module.ts文件中进行。
- import { NgModule } from '@angular/core';
- import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
- const routes: Routes = [
- {
- path: '',
- redirectTo: 'home',
- pathMatch: 'full'
- },
- {
- path: 'home',
- loadChildren: () => import('./pages/home/home.module').then(m => m.HomePageModule)
- },
- {
- path: 'about',
- loadChildren: () => import('./pages/about/about.module').then(m => m.AboutPageModule)
- }
- ];
- @NgModule({
- imports: [
- RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
- ],
- exports: [RouterModule]
- })
- export class AppRoutingModule { }
复制代码
5.3 页面导航
在组件中注入Router服务,并使用它进行导航:
- import { Component } from '@angular/core';
- import { Router } from '@angular/router';
- @Component({
- selector: 'app-home',
- templateUrl: 'home.page.html',
- styleUrls: ['home.page.scss'],
- })
- export class HomePage {
- constructor(private router: Router) {}
- navigateToAbout() {
- this.router.navigate(['/about']);
- }
- }
复制代码
在HTML模板中:
- <ion-button (click)="navigateToAbout()">关于我们</ion-button>
复制代码
Ionic 4提供了NavController服务,用于更高级的导航控制:
- import { Component } from '@angular/core';
- import { NavController } from '@ionic/angular';
- @Component({
- selector: 'app-home',
- templateUrl: 'home.page.html',
- styleUrls: ['home.page.scss'],
- })
- export class HomePage {
- constructor(private navCtrl: NavController) {}
- navigateToAbout() {
- this.navCtrl.navigateForward('/about');
- }
- goBack() {
- this.navCtrl.navigateBack('/home');
- }
- goToRoot() {
- this.navCtrl.navigateRoot('/home');
- }
- }
复制代码
在导航时传递参数:
- // 发送页面
- navigateToDetails(id: number) {
- this.router.navigate(['/details', { id: id }]);
- }
复制代码
在路由配置中:
- {
- path: 'details/:id',
- loadChildren: () => import('./pages/details/details.module').then(m => m.DetailsPageModule)
- }
复制代码
接收参数:
- import { Component, OnInit } from '@angular/core';
- import { ActivatedRoute } from '@angular/router';
- @Component({
- selector: 'app-details',
- templateUrl: './details.page.html',
- styleUrls: ['./details.page.scss'],
- })
- export class DetailsPage implements OnInit {
- id: number;
- constructor(private route: ActivatedRoute) { }
- ngOnInit() {
- this.id = this.route.snapshot.params['id'];
- // 或者使用订阅方式
- this.route.params.subscribe(params => {
- this.id = params['id'];
- });
- }
- }
复制代码
5.4 导航堆栈
Ionic 4维护了一个导航堆栈,用于跟踪用户的导航历史。可以使用NavController来管理这个堆栈:
- // 获取当前导航堆栈
- const stack = this.navCtrl.getCurrentNavigation();
- // 获取导航堆栈中的页面数量
- const stackLength = this.navCtrl.getLength();
- // 移除导航堆栈中的页面
- this.navCtrl.pop();
复制代码
6. 数据管理和状态
在Ionic 4应用中,数据管理和状态处理是非常重要的部分。本节将介绍如何在Ionic 4应用中管理和共享数据。
6.1 使用服务共享数据
创建一个服务来管理应用的数据:
- ionic generate service services/data
复制代码
在服务中定义数据和操作数据的方法:
- import { Injectable } from '@angular/core';
- import { BehaviorSubject, Observable } from 'rxjs';
- @Injectable({
- providedIn: 'root'
- })
- export class DataService {
- private itemsSource = new BehaviorSubject<any[]>([]);
- currentItems = this.itemsSource.asObservable();
- constructor() { }
- updateItems(items: any[]) {
- this.itemsSource.next(items);
- }
- addItem(item: any) {
- const currentItems = this.itemsSource.getValue();
- currentItems.push(item);
- this.itemsSource.next(currentItems);
- }
- removeItem(index: number) {
- const currentItems = this.itemsSource.getValue();
- currentItems.splice(index, 1);
- this.itemsSource.next(currentItems);
- }
- }
复制代码
在组件中使用这个服务:
- import { Component, OnInit } from '@angular/core';
- import { DataService } from '../../services/data.service';
- @Component({
- selector: 'app-home',
- templateUrl: 'home.page.html',
- styleUrls: ['home.page.scss'],
- })
- export class HomePage implements OnInit {
- items: any[] = [];
- constructor(private dataService: DataService) { }
- ngOnInit() {
- this.dataService.currentItems.subscribe(items => {
- this.items = items;
- });
- }
- addItem() {
- const newItem = {
- id: this.items.length + 1,
- name: `项目 ${this.items.length + 1}`
- };
- this.dataService.addItem(newItem);
- }
- removeItem(index: number) {
- this.dataService.removeItem(index);
- }
- }
复制代码
6.2 使用RxJS进行响应式编程
RxJS是响应式编程的库,在Ionic 4中被广泛使用。下面是一些常用的RxJS操作符:
- import { Component, OnInit } from '@angular/core';
- import { fromEvent, interval } from 'rxjs';
- import { map, filter, debounceTime, distinctUntilChanged } from 'rxjs/operators';
- @Component({
- selector: 'app-rxjs-demo',
- templateUrl: './rxjs-demo.page.html',
- styleUrls: ['./rxjs-demo.page.scss'],
- })
- export class RxjsDemoPage implements OnInit {
- constructor() { }
- ngOnInit() {
- // 使用interval创建一个每隔1秒发出一个递增数字的Observable
- const counter = interval(1000);
-
- // 使用pipe应用操作符
- counter.pipe(
- // 只取前10个值
- take(10),
- // 过滤掉偶数
- filter(value => value % 2 === 1),
- // 对值进行映射
- map(value => `奇数: ${value}`)
- ).subscribe(value => console.log(value));
- // 监听输入框的输入事件
- const inputElement = document.getElementById('search-input');
- if (inputElement) {
- fromEvent(inputElement, 'input').pipe(
- // 获取输入值
- map((event: any) => event.target.value),
- // 防抖,等待300毫秒没有新输入后才处理
- debounceTime(300),
- // 只有当值发生变化时才继续
- distinctUntilChanged()
- ).subscribe(searchTerm => {
- console.log('搜索词:', searchTerm);
- // 这里可以执行搜索操作
- });
- }
- }
- }
复制代码
6.3 使用NgRx进行状态管理
对于大型应用,可以使用NgRx进行状态管理。NgRx是一个基于RxJS的状态管理库,灵感来自Redux。
首先安装NgRx:
- npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
复制代码
创建一个简单的计数器状态管理:
- // src/app/actions/counter.actions.ts
- import { createAction } from '@ngrx/store';
- export const increment = createAction('[Counter] Increment');
- export const decrement = createAction('[Counter] Decrement');
- export const reset = createAction('[Counter] Reset');
复制代码- // src/app/reducers/counter.reducer.ts
- import { createReducer, on } from '@ngrx/store';
- import { increment, decrement, reset } from '../actions/counter.actions';
- export const initialState = 0;
- export const counterReducer = createReducer(
- initialState,
- on(increment, state => state + 1),
- on(decrement, state => state - 1),
- on(reset, state => 0)
- );
复制代码- // src/app/app.module.ts
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
- import { RouterModule } from '@angular/router';
- import { IonicModule } from '@ionic/angular';
- import { AppComponent } from './app.component';
- import { StoreModule } from '@ngrx/store';
- import { StoreDevtoolsModule } from '@ngrx/store-devtools';
- import { counterReducer } from './reducers/counter.reducer';
- @NgModule({
- declarations: [AppComponent],
- imports: [
- BrowserModule,
- IonicModule.forRoot(),
- RouterModule.forRoot([]),
- StoreModule.forRoot({ count: counterReducer }),
- StoreDevtoolsModule.instrument({
- maxAge: 25
- })
- ],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
复制代码- // src/app/pages/counter/counter.page.ts
- import { Component } from '@angular/core';
- import { Store, select } from '@ngrx/store';
- import { Observable } from 'rxjs';
- import { increment, decrement, reset } from '../../actions/counter.actions';
- @Component({
- selector: 'app-counter',
- templateUrl: './counter.page.html',
- styleUrls: ['./counter.page.scss'],
- })
- export class CounterPage {
- count$: Observable<number>;
- constructor(private store: Store<{ count: number }>) {
- this.count$ = store.pipe(select('count'));
- }
- increment() {
- this.store.dispatch(increment());
- }
- decrement() {
- this.store.dispatch(decrement());
- }
- reset() {
- this.store.dispatch(reset());
- }
- }
复制代码
在模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>计数器</ion-title>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <div class="ion-text-center ion-padding">
- <h1>当前计数: {{ count$ | async }}</h1>
- <ion-button (click)="increment()">增加</ion-button>
- <ion-button (click)="decrement()">减少</ion-button>
- <ion-button (click)="reset()">重置</ion-button>
- </div>
- </ion-content>
复制代码
7. 与后端API交互
在移动应用开发中,与后端API交互是非常常见的需求。Ionic 4提供了多种方式来处理HTTP请求。
7.1 使用HttpClient
Angular的HttpClient是处理HTTP请求的推荐方式。
首先,在app.module.ts中导入HttpClientModule:
- import { HttpClientModule } from '@angular/common/angular';
- @NgModule({
- // ...
- imports: [
- // ...
- HttpClientModule
- ],
- // ...
- })
- export class AppModule { }
复制代码
创建一个API服务:
- // src/app/services/api.service.ts
- import { Injectable } from '@angular/core';
- import { HttpClient, HttpHeaders } from '@angular/common/http';
- import { Observable } from 'rxjs';
- @Injectable({
- providedIn: 'root'
- })
- export class ApiService {
- private apiUrl = 'https://api.example.com'; // 替换为你的API地址
- constructor(private http: HttpClient) { }
- // GET请求
- get(endpoint: string): Observable<any> {
- return this.http.get(`${this.apiUrl}/${endpoint}`);
- }
- // POST请求
- post(endpoint: string, data: any): Observable<any> {
- return this.http.post(`${this.apiUrl}/${endpoint}`, data);
- }
- // PUT请求
- put(endpoint: string, data: any): Observable<any> {
- return this.http.put(`${this.apiUrl}/${endpoint}`, data);
- }
- // DELETE请求
- delete(endpoint: string): Observable<any> {
- return this.http.delete(`${this.apiUrl}/${endpoint}`);
- }
- // 带有自定义头的请求
- getWithAuth(endpoint: string, token: string): Observable<any> {
- const headers = new HttpHeaders().set('Authorization', `Bearer ${token}`);
- return this.http.get(`${this.apiUrl}/${endpoint}`, { headers });
- }
- }
复制代码
在组件中使用这个服务:
- import { Component, OnInit } from '@angular/core';
- import { ApiService } from '../../services/api.service';
- @Component({
- selector: 'app-posts',
- templateUrl: './posts.page.html',
- styleUrls: ['./posts.page.scss'],
- })
- export class PostsPage implements OnInit {
- posts: any[] = [];
- loading: boolean = false;
- error: string;
- constructor(private apiService: ApiService) { }
- ngOnInit() {
- this.loadPosts();
- }
- loadPosts() {
- this.loading = true;
- this.error = null;
-
- this.apiService.get('posts').subscribe(
- (response) => {
- this.posts = response;
- this.loading = false;
- },
- (err) => {
- this.error = '加载数据失败,请稍后再试。';
- this.loading = false;
- console.error('API错误:', err);
- }
- );
- }
- createPost() {
- const newPost = {
- title: '新文章',
- body: '这是文章的内容',
- userId: 1
- };
-
- this.apiService.post('posts', newPost).subscribe(
- (response) => {
- console.log('文章创建成功:', response);
- this.loadPosts(); // 重新加载文章列表
- },
- (err) => {
- console.error('创建文章失败:', err);
- }
- );
- }
- }
复制代码
在模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>文章列表</ion-title>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <ion-refresher (ionRefresh)="loadPosts()">
- <ion-refresher-content></ion-refresher-content>
- </ion-refresher>
- <div *ngIf="loading" class="ion-text-center ion-padding">
- <ion-spinner></ion-spinner>
- <p>加载中...</p>
- </div>
- <div *ngIf="error" class="ion-text-center ion-padding">
- <p color="danger">{{ error }}</p>
- <ion-button (click)="loadPosts()">重试</ion-button>
- </div>
- <ion-list>
- <ion-item *ngFor="let post of posts">
- <ion-label>
- <h2>{{ post.title }}</h2>
- <p>{{ post.body }}</p>
- </ion-label>
- </ion-item>
- </ion-list>
- <ion-fab vertical="bottom" horizontal="end" slot="fixed">
- <ion-fab-button (click)="createPost()">
- <ion-icon name="add"></ion-icon>
- </ion-fab-button>
- </ion-fab>
- </ion-content>
复制代码
7.2 处理认证和授权
大多数应用都需要用户认证,下面是一个简单的认证服务示例:
- // src/app/services/auth.service.ts
- import { Injectable } from '@angular/core';
- import { HttpClient } from '@angular/common/http';
- import { BehaviorSubject, Observable, of } from 'rxjs';
- import { map, tap, catchError } from 'rxjs/operators';
- import { Storage } from '@ionic/storage';
- import { JwtHelperService } from '@auth0/angular-jwt';
- @Injectable({
- providedIn: 'root'
- })
- export class AuthService {
- private apiUrl = 'https://api.example.com/auth';
- private currentUserSubject: BehaviorSubject<any>;
- public currentUser: Observable<any>;
- private jwtHelper = new JwtHelperService();
- constructor(
- private http: HttpClient,
- private storage: Storage
- ) {
- this.currentUserSubject = new BehaviorSubject<any>(null);
- this.currentUser = this.currentUserSubject.asObservable();
-
- // 检查存储中是否有token
- this.storage.get('token').then(token => {
- if (token && !this.jwtHelper.isTokenExpired(token)) {
- const user = this.jwtHelper.decodeToken(token);
- this.currentUserSubject.next(user);
- }
- });
- }
- public get currentUserValue(): any {
- return this.currentUserSubject.value;
- }
- login(username: string, password: string): Observable<any> {
- return this.http.post<any>(`${this.apiUrl}/login`, { username, password })
- .pipe(
- map(response => {
- // 登录成功,保存token和用户信息
- if (response && response.token) {
- this.storage.set('token', response.token);
- const user = this.jwtHelper.decodeToken(response.token);
- this.currentUserSubject.next(user);
- }
- return response;
- })
- );
- }
- logout(): void {
- this.storage.remove('token');
- this.currentUserSubject.next(null);
- }
- isAuthenticated(): boolean {
- const token = this.storage.get('token');
- return token && !this.jwtHelper.isTokenExpired(token);
- }
- getToken(): Promise<string> {
- return this.storage.get('token');
- }
- }
复制代码
创建一个登录页面:
- // src/app/pages/login/login.page.ts
- import { Component } from '@angular/core';
- import { AuthService } from '../../services/auth.service';
- import { Router } from '@angular/router';
- import { ToastController } from '@ionic/angular';
- @Component({
- selector: 'app-login',
- templateUrl: './login.page.html',
- styleUrls: ['./login.page.scss'],
- })
- export class LoginPage {
- credentials = {
- username: '',
- password: ''
- };
- constructor(
- private authService: AuthService,
- private router: Router,
- private toastController: ToastController
- ) { }
- login() {
- this.authService.login(this.credentials.username, this.credentials.password)
- .subscribe(
- async () => {
- const toast = await this.toastController.create({
- message: '登录成功',
- duration: 2000,
- position: 'bottom'
- });
- toast.present();
- this.router.navigate(['/home']);
- },
- async (error) => {
- const toast = await this.toastController.create({
- message: '登录失败,请检查用户名和密码',
- duration: 2000,
- position: 'bottom',
- color: 'danger'
- });
- toast.present();
- }
- );
- }
- }
复制代码
登录页面模板:
- <ion-header>
- <ion-toolbar>
- <ion-title>登录</ion-title>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <div class="ion-padding">
- <form (ngSubmit)="login()">
- <ion-item>
- <ion-label position="floating">用户名</ion-label>
- <ion-input type="text" [(ngModel)]="credentials.username" name="username" required></ion-input>
- </ion-item>
-
- <ion-item>
- <ion-label position="floating">密码</ion-label>
- <ion-input type="password" [(ngModel)]="credentials.password" name="password" required></ion-input>
- </ion-item>
-
- <div class="ion-margin-top">
- <ion-button expand="block" type="submit" [disabled]="!credentials.username || !credentials.password">登录</ion-button>
- </div>
- </form>
- </div>
- </ion-content>
复制代码
7.3 使用拦截器
拦截器允许我们在请求发送前或响应接收后进行一些操作,比如添加认证头、处理错误等。
创建一个认证拦截器:
- // src/app/interceptors/auth.interceptor.ts
- import { Injectable } from '@angular/core';
- import {
- HttpRequest,
- HttpHandler,
- HttpEvent,
- HttpInterceptor
- } from '@angular/common/http';
- import { Observable, from } from 'rxjs';
- import { switchMap } from 'rxjs/operators';
- import { AuthService } from '../services/auth.service';
- @Injectable()
- export class AuthInterceptor implements HttpInterceptor {
- constructor(private authService: AuthService) {}
- intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
- // 获取token
- return from(this.authService.getToken()).pipe(
- switchMap(token => {
- // 如果有token,添加到请求头
- if (token) {
- request = request.clone({
- setHeaders: {
- Authorization: `Bearer ${token}`
- }
- });
- }
-
- // 继续处理请求
- return next.handle(request);
- })
- );
- }
- }
复制代码
注册拦截器:
- // src/app/app.module.ts
- import { HTTP_INTERCEPTORS } from '@angular/common/http';
- import { AuthInterceptor } from './interceptors/auth.interceptor';
- @NgModule({
- // ...
- providers: [
- {
- provide: HTTP_INTERCEPTORS,
- useClass: AuthInterceptor,
- multi: true
- }
- ],
- // ...
- })
- export class AppModule { }
复制代码
8. 调试和测试
在开发Ionic 4应用时,调试和测试是非常重要的环节。本节将介绍如何有效地调试和测试Ionic 4应用。
8.1 浏览器调试
Ionic应用可以在浏览器中运行和调试,这是最方便的调试方式。
在Chrome或其他现代浏览器中,可以使用开发者工具进行调试:
1. 在浏览器中运行应用:ionic serve
2. 右键点击页面,选择”检查”或按F12打开开发者工具
3. 在”Console”标签页查看日志和错误
4. 在”Elements”标签页检查和修改DOM
5. 在”Network”标签页监控网络请求
6. 在”Application”标签页查看本地存储和缓存
Augury是一个Angular应用的调试工具,可以帮助你理解应用的结构和状态。
1. 在Chrome扩展商店安装Augury
2. 打开开发者工具,会出现一个新的”Augury”标签页
3. 使用Augury可以查看组件树、路由状态和NgRx状态等
8.2 设备调试
在真实设备上调试应用可以更好地了解应用在实际环境中的表现。
1. 启用开发者选项和USB调试在设备上,进入”设置” > “关于手机”连续点击”版本号”7次,启用开发者选项返回”设置”,进入”开发者选项”启用”USB调试”
2. 在设备上,进入”设置” > “关于手机”
3. 连续点击”版本号”7次,启用开发者选项
4. 返回”设置”,进入”开发者选项”
5. 启用”USB调试”
6. 连接设备并运行应用ionic cordova run android --device
7. 使用Chrome调试在Chrome中访问chrome://inspect/#devices找到你的设备,点击”inspect”这将打开一个开发者工具窗口,可以调试运行在设备上的应用
8. 在Chrome中访问chrome://inspect/#devices
9. 找到你的设备,点击”inspect”
10. 这将打开一个开发者工具窗口,可以调试运行在设备上的应用
启用开发者选项和USB调试
• 在设备上,进入”设置” > “关于手机”
• 连续点击”版本号”7次,启用开发者选项
• 返回”设置”,进入”开发者选项”
• 启用”USB调试”
连接设备并运行应用
- ionic cordova run android --device
复制代码
使用Chrome调试
• 在Chrome中访问chrome://inspect/#devices
• 找到你的设备,点击”inspect”
• 这将打开一个开发者工具窗口,可以调试运行在设备上的应用
1. 连接iOS设备到Mac
2. 在Xcode中打开项目:ionic cordova open ios
3. 在Xcode中选择你的设备作为目标
4. 点击运行按钮
5. 使用Safari调试在Mac上打开Safari进入”开发”菜单,选择你的设备和应用这将打开一个Web检查器窗口,可以调试应用
6. 在Mac上打开Safari
7. 进入”开发”菜单,选择你的设备和应用
8. 这将打开一个Web检查器窗口,可以调试应用
• 在Mac上打开Safari
• 进入”开发”菜单,选择你的设备和应用
• 这将打开一个Web检查器窗口,可以调试应用
8.3 单元测试
Ionic 4应用使用Jasmine和Karma进行单元测试。
创建一个简单的服务测试:
- // src/app/services/data.service.spec.ts
- import { TestBed } from '@angular/core/testing';
- import { DataService } from './data.service';
- describe('DataService', () => {
- let service: DataService;
- beforeEach(() => {
- TestBed.configureTestingModule({});
- service = TestBed.inject(DataService);
- });
- it('should be created', () => {
- expect(service).toBeTruthy();
- });
- it('should add item to list', () => {
- const initialLength = service.getItems().length;
- service.addItem({ id: 1, name: 'Test Item' });
- expect(service.getItems().length).toBe(initialLength + 1);
- });
- it('should remove item from list', () => {
- service.addItem({ id: 1, name: 'Test Item' });
- const initialLength = service.getItems().length;
- service.removeItem(0);
- expect(service.getItems().length).toBe(initialLength - 1);
- });
- });
复制代码
运行所有测试:
运行特定文件的测试:
- ng test --include='src/app/services/data.service.spec.ts'
复制代码
8.4 端到端测试
Ionic 4应用使用Protractor进行端到端测试。
创建一个简单的端到端测试:
- // e2e/src/app.e2e-spec.ts
- import { AppPage } from './app.po';
- describe('workspace-project App', () => {
- let page: AppPage;
- beforeEach(() => {
- page = new AppPage();
- });
- it('should display welcome message', () => {
- page.navigateTo();
- expect(page.getTitleText()).toEqual('Welcome to myFirstApp!');
- });
- it('should navigate to about page', () => {
- page.navigateTo();
- page.getAboutButton().click();
- expect(page.getAboutTitle()).toEqual('About');
- });
- });
复制代码
首先启动应用:
然后在另一个终端中运行测试:
8.5 使用Ionic Native插件
Ionic Native提供了对设备原生功能的访问,如相机、GPS等。下面是一个使用相机的例子:
首先安装插件:
- ionic cordova plugin add cordova-plugin-camera
- npm install @ionic-native/camera
复制代码
在app.module.ts中导入插件:
- import { Camera } from '@ionic-native/camera/ngx';
- @NgModule({
- // ...
- providers: [
- // ...
- Camera
- ],
- // ...
- })
- export class AppModule { }
复制代码
在组件中使用相机:
- import { Component } from '@angular/core';
- import { Camera, CameraOptions } from '@ionic-native/camera/ngx';
- @Component({
- selector: 'app-camera',
- templateUrl: './camera.page.html',
- styleUrls: ['./camera.page.scss'],
- })
- export class CameraPage {
- image: string;
- constructor(private camera: Camera) { }
- takePicture() {
- const options: CameraOptions = {
- quality: 100,
- destinationType: this.camera.DestinationType.DATA_URL,
- encodingType: this.camera.EncodingType.JPEG,
- mediaType: this.camera.MediaType.PICTURE
- };
- this.camera.getPicture(options).then((imageData) => {
- // imageData is either a base64 encoded string or a file URI
- this.image = 'data:image/jpeg;base64,' + imageData;
- }, (err) => {
- // Handle error
- console.error('Camera error:', err);
- });
- }
- }
复制代码
在模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>相机</ion-title>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <div class="ion-text-center ion-padding">
- <ion-button (click)="takePicture()">拍照</ion-button>
-
- <div *ngIf="image" class="ion-margin-top">
- <img [src]="image" style="max-width: 100%;">
- </div>
- </div>
- </ion-content>
复制代码
9. 构建和发布应用
开发完成后,我们需要构建和发布应用。本节将介绍如何为不同平台构建和发布Ionic 4应用。
9.1 构建Web应用
Ionic应用可以作为Web应用发布:
- # 构建生产版本
- ionic build --prod
- # 构建完成后,文件会在www目录中
- # 可以将这些文件部署到任何Web服务器
复制代码
9.2 构建Android应用
- ionic cordova platform add android
复制代码- # 构建调试版本
- ionic cordova build android
- # 构建发布版本
- ionic cordova build android --prod --release
复制代码
1. 生成密钥库:keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
2. 使用jarsigner签名APK:jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk alias_name
3. 使用zipalign优化APK:zipalign -v 4 platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk my-app.apk
生成密钥库:
- keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
复制代码
使用jarsigner签名APK:
- jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk alias_name
复制代码
使用zipalign优化APK:
- zipalign -v 4 platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk my-app.apk
复制代码
1. 登录Google Play Console
2. 创建应用
3. 上传APK文件
4. 填写应用信息、截图等
5. 提交审核
9.3 构建iOS应用
- ionic cordova platform add ios
复制代码- # 构建iOS应用
- ionic cordova build ios --prod
复制代码
1. 在Xcode中配置签名证书和配置文件
2. 选择”Generic iOS Device”作为目标
3. 选择”Product” > “Archive”
4. 在Organizer窗口中,选择”Upload to App Store”
5. 按照提示完成上传
1. 登录App Store Connect
2. 创建应用
3. 填写应用信息、截图等
4. 选择构建版本
5. 提交审核
9.4 使用Capacitor
Capacitor是Ionic团队开发的替代Cordova的解决方案,提供了更好的开发体验和原生功能访问。
- npm install @capacitor/core @capacitor/cli @capacitor/ios @capacitor/android
- npx cap init
复制代码- # 构建Web资源
- ionic build
- # 同步到原生项目
- npx cap sync
- # 在Android Studio中打开
- npx cap open android
- # 在Xcode中打开
- npx cap open ios
复制代码
Capacitor提供了许多原生插件,下面是一个使用Geolocation插件的例子:
- import { Component } from '@angular/core';
- import { Geolocation, Position } from '@capacitor/geolocation';
- @Component({
- selector: 'app-geolocation',
- templateUrl: './geolocation.page.html',
- styleUrls: ['./geolocation.page.scss'],
- })
- export class GeolocationPage {
- position: Position;
- constructor() { }
- async getCurrentPosition() {
- try {
- this.position = await Geolocation.getCurrentPosition();
- console.log('Current position:', this.position);
- } catch (error) {
- console.error('Error getting location', error);
- }
- }
- }
复制代码
在模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>地理位置</ion-title>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <div class="ion-text-center ion-padding">
- <ion-button (click)="getCurrentPosition()">获取当前位置</ion-button>
-
- <div *ngIf="position" class="ion-margin-top">
- <p>纬度: {{ position.coords.latitude }}</p>
- <p>经度: {{ position.coords.longitude }}</p>
- <p>精度: {{ position.coords.accuracy }} 米</p>
- </div>
- </div>
- </ion-content>
复制代码
10. 实战项目:待办事项应用
现在,让我们通过一个完整的待办事项应用来综合运用前面所学的知识。
10.1 项目概述
我们将创建一个待办事项应用,具有以下功能:
• 添加、编辑和删除待办事项
• 标记待办事项为完成或未完成
• 按类别过滤待办事项
• 本地存储待办事项
• 响应式设计,适配不同屏幕尺寸
10.2 创建项目
首先,创建一个新的Ionic项目:
- ionic start todoApp tabs --type=angular
- cd todoApp
复制代码
10.3 设计数据模型
创建一个待办事项的数据模型:
- // src/app/models/todo.model.ts
- export interface Todo {
- id: string;
- title: string;
- description: string;
- completed: boolean;
- category: string;
- createdAt: Date;
- updatedAt: Date;
- }
复制代码
10.4 创建服务
创建一个待办事项服务:
- ionic generate service services/todo
复制代码
在服务中实现待办事项的管理逻辑:
- // src/app/services/todo.service.ts
- import { Injectable } from '@angular/core';
- import { BehaviorSubject, Observable } from 'rxjs';
- import { map } from 'rxjs/operators';
- import { Todo } from '../models/todo.model';
- import { Storage } from '@ionic/storage';
- @Injectable({
- providedIn: 'root'
- })
- export class TodoService {
- private todosSubject = new BehaviorSubject<Todo[]>([]);
- private todos$: Observable<Todo[]> = this.todosSubject.asObservable();
- private STORAGE_KEY = 'todos';
- constructor(private storage: Storage) {
- this.loadTodos();
- }
- private async loadTodos() {
- const storedTodos = await this.storage.get(this.STORAGE_KEY);
- if (storedTodos) {
- this.todosSubject.next(storedTodos);
- }
- }
- private async saveTodos(todos: Todo[]) {
- await this.storage.set(this.STORAGE_KEY, todos);
- this.todosSubject.next(todos);
- }
- getTodos(): Observable<Todo[]> {
- return this.todos$;
- }
- getTodosByCategory(category: string): Observable<Todo[]> {
- return this.todos$.pipe(
- map(todos => todos.filter(todo => todo.category === category))
- );
- }
- getCompletedTodos(): Observable<Todo[]> {
- return this.todos$.pipe(
- map(todos => todos.filter(todo => todo.completed))
- );
- }
- getActiveTodos(): Observable<Todo[]> {
- return this.todos$.pipe(
- map(todos => todos.filter(todo => !todo.completed))
- );
- }
- async addTodo(todo: Omit<Todo, 'id' | 'createdAt' | 'updatedAt'>) {
- const newTodo: Todo = {
- ...todo,
- id: this.generateId(),
- createdAt: new Date(),
- updatedAt: new Date()
- };
- const currentTodos = this.todosSubject.getValue();
- const updatedTodos = [...currentTodos, newTodo];
- await this.saveTodos(updatedTodos);
-
- return newTodo;
- }
- async updateTodo(id: string, updates: Partial<Todo>) {
- const currentTodos = this.todosSubject.getValue();
- const updatedTodos = currentTodos.map(todo => {
- if (todo.id === id) {
- return {
- ...todo,
- ...updates,
- updatedAt: new Date()
- };
- }
- return todo;
- });
-
- await this.saveTodos(updatedTodos);
-
- return updatedTodos.find(todo => todo.id === id);
- }
- async deleteTodo(id: string) {
- const currentTodos = this.todosSubject.getValue();
- const updatedTodos = currentTodos.filter(todo => todo.id !== id);
- await this.saveTodos(updatedTodos);
- }
- async toggleTodoComplete(id: string) {
- const currentTodos = this.todosSubject.getValue();
- const updatedTodos = currentTodos.map(todo => {
- if (todo.id === id) {
- return {
- ...todo,
- completed: !todo.completed,
- updatedAt: new Date()
- };
- }
- return todo;
- });
-
- await this.saveTodos(updatedTodos);
-
- return updatedTodos.find(todo => todo.id === id);
- }
- private generateId(): string {
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
- }
- }
复制代码
10.5 创建页面
- ionic generate page pages/todos
复制代码
在页面组件中:
- // src/app/pages/todos/todos.page.ts
- import { Component, OnInit } from '@angular/core';
- import { Observable } from 'rxjs';
- import { Todo } from '../../models/todo.model';
- import { TodoService } from '../../services/todo.service';
- import { ModalController } from '@ionic/angular';
- import { TodoModalPage } from '../todo-modal/todo-modal.page';
- @Component({
- selector: 'app-todos',
- templateUrl: './todos.page.html',
- styleUrls: ['./todos.page.scss'],
- })
- export class TodosPage implements OnInit {
- todos$: Observable<Todo[]>;
- activeTodos$: Observable<Todo[]>;
- completedTodos$: Observable<Todo[]>;
- selectedCategory: string = 'all';
- categories = [
- { id: 'all', name: '全部' },
- { id: 'work', name: '工作' },
- { id: 'personal', name: '个人' },
- { id: 'shopping', name: '购物' }
- ];
- constructor(
- private todoService: TodoService,
- private modalController: ModalController
- ) { }
- ngOnInit() {
- this.todos$ = this.todoService.getTodos();
- this.activeTodos$ = this.todoService.getActiveTodos();
- this.completedTodos$ = this.todoService.getCompletedTodos();
- }
- async presentTodoModal(todo?: Todo) {
- const modal = await this.modalController.create({
- component: TodoModalPage,
- componentProps: {
- todo: todo || null
- }
- });
- modal.onDidDismiss().then((data) => {
- if (data.data) {
- if (todo) {
- // 更新待办事项
- this.todoService.updateTodo(todo.id, data.data);
- } else {
- // 添加新的待办事项
- this.todoService.addTodo(data.data);
- }
- }
- });
- return await modal.present();
- }
- toggleTodoComplete(id: string) {
- this.todoService.toggleTodoComplete(id);
- }
- deleteTodo(id: string) {
- this.todoService.deleteTodo(id);
- }
- filterByCategory(category: string) {
- this.selectedCategory = category;
- }
- getFilteredTodos(): Observable<Todo[]> {
- if (this.selectedCategory === 'all') {
- return this.todos$;
- }
- return this.todoService.getTodosByCategory(this.selectedCategory);
- }
- }
复制代码
在页面模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>待办事项</ion-title>
- <ion-buttons slot="end">
- <ion-button (click)="presentTodoModal()">
- <ion-icon slot="icon-only" name="add"></ion-icon>
- </ion-button>
- </ion-buttons>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <!-- 类别过滤器 -->
- <ion-segment [(ngModel)]="selectedCategory" (ionChange)="filterByCategory(selectedCategory)">
- <ion-segment-button *ngFor="let category of categories" [value]="category.id">
- {{ category.name }}
- </ion-segment-button>
- </ion-segment>
- <!-- 待办事项列表 -->
- <ion-list>
- <ion-item-sliding *ngFor="let todo of (getFilteredTodos() | async)">
- <ion-item>
- <ion-label>
- <h2 [class.completed]="todo.completed">{{ todo.title }}</h2>
- <p>{{ todo.description }}</p>
- <p>
- <ion-chip>
- <ion-label>{{ categories.find(c => c.id === todo.category)?.name }}</ion-chip>
- </ion-chip>
- </p>
- </ion-label>
- <ion-checkbox
- slot="start"
- [checked]="todo.completed"
- (ionChange)="toggleTodoComplete(todo.id)">
- </ion-checkbox>
- </ion-item>
- <ion-item-options side="end">
- <ion-item-option (click)="presentTodoModal(todo)" color="primary">
- <ion-icon slot="icon-only" name="create"></ion-icon>
- </ion-item-option>
- <ion-item-option (click)="deleteTodo(todo.id)" color="danger">
- <ion-icon slot="icon-only" name="trash"></ion-icon>
- </ion-item-option>
- </ion-item-options>
- </ion-item-sliding>
- </ion-list>
- <!-- 空状态 -->
- <div *ngIf="(getFilteredTodos() | async)?.length === 0" class="ion-text-center ion-padding">
- <ion-icon name="checkmark-circle-outline" size="large" color="medium"></ion-icon>
- <p>没有待办事项</p>
- <ion-button fill="outline" (click)="presentTodoModal()">添加第一个待办事项</ion-button>
- </div>
- </ion-content>
复制代码
在页面样式中:
- // src/app/pages/todos/todos.page.scss
- .completed {
- text-decoration: line-through;
- color: var(--ion-color-medium);
- }
复制代码- ionic generate page pages/todo-modal
复制代码
在页面组件中:
- // src/app/pages/todo-modal/todo-modal.page.ts
- import { Component, OnInit, Input } from '@angular/core';
- import { ModalController } from '@ionic/angular';
- import { Todo } from '../../models/todo.model';
- @Component({
- selector: 'app-todo-modal',
- templateUrl: './todo-modal.page.html',
- styleUrls: ['./todo-modal.page.scss'],
- })
- export class TodoModalPage implements OnInit {
- @Input() todo: Todo | null = null;
- formData = {
- title: '',
- description: '',
- category: 'personal',
- completed: false
- };
- categories = [
- { id: 'work', name: '工作' },
- { id: 'personal', name: '个人' },
- { id: 'shopping', name: '购物' }
- ];
- constructor(private modalController: ModalController) { }
- ngOnInit() {
- if (this.todo) {
- this.formData = {
- title: this.todo.title,
- description: this.todo.description,
- category: this.todo.category,
- completed: this.todo.completed
- };
- }
- }
- dismiss() {
- this.modalController.dismiss();
- }
- save() {
- this.modalController.dismiss(this.formData);
- }
- }
复制代码
在页面模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>{{ todo ? '编辑待办事项' : '添加待办事项' }}</ion-title>
- <ion-buttons slot="end">
- <ion-button (click)="dismiss()">取消</ion-button>
- </ion-buttons>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <form (ngSubmit)="save()">
- <ion-list>
- <ion-item>
- <ion-label position="floating">标题</ion-label>
- <ion-input
- type="text"
- [(ngModel)]="formData.title"
- name="title"
- required>
- </ion-input>
- </ion-item>
-
- <ion-item>
- <ion-label position="floating">描述</ion-label>
- <ion-textarea
- [(ngModel)]="formData.description"
- name="description">
- </ion-textarea>
- </ion-item>
-
- <ion-item>
- <ion-label>类别</ion-label>
- <ion-select
- [(ngModel)]="formData.category"
- name="category">
- <ion-select-option *ngFor="let category of categories" [value]="category.id">
- {{ category.name }}
- </ion-select-option>
- </ion-select>
- </ion-item>
-
- <ion-item *ngIf="todo">
- <ion-label>已完成</ion-label>
- <ion-toggle
- [(ngModel)]="formData.completed"
- name="completed">
- </ion-toggle>
- </ion-item>
- </ion-list>
-
- <div class="ion-padding">
- <ion-button expand="block" type="submit" [disabled]="!formData.title">
- {{ todo ? '更新' : '添加' }}
- </ion-button>
- </div>
- </form>
- </ion-content>
复制代码- ionic generate page pages/stats
复制代码
在页面组件中:
- // src/app/pages/stats/stats.page.ts
- import { Component, OnInit } from '@angular/core';
- import { Observable } from 'rxjs';
- import { map } from 'rxjs/operators';
- import { Todo } from '../../models/todo.model';
- import { TodoService } from '../../services/todo.service';
- @Component({
- selector: 'app-stats',
- templateUrl: './stats.page.html',
- styleUrls: ['./stats.page.scss'],
- })
- export class StatsPage implements OnInit {
- todos$: Observable<Todo[]>;
- totalTodos$: Observable<number>;
- completedTodos$: Observable<number>;
- activeTodos$: Observable<number>;
- completionRate$: Observable<number>;
- categories = [
- { id: 'work', name: '工作' },
- { id: 'personal', name: '个人' },
- { id: 'shopping', name: '购物' }
- ];
- categoryStats$: Observable<{ category: string; name: string; total: number; completed: number; completionRate: number }[]>;
- constructor(private todoService: TodoService) { }
- ngOnInit() {
- this.todos$ = this.todoService.getTodos();
- this.totalTodos$ = this.todos$.pipe(map(todos => todos.length));
- this.completedTodos$ = this.todoService.getCompletedTodos().pipe(map(todos => todos.length));
- this.activeTodos$ = this.todoService.getActiveTodos().pipe(map(todos => todos.length));
-
- this.completionRate$ = this.todos$.pipe(
- map(todos => {
- if (todos.length === 0) return 0;
- const completed = todos.filter(todo => todo.completed).length;
- return Math.round((completed / todos.length) * 100);
- })
- );
- this.categoryStats$ = this.todos$.pipe(
- map(todos => {
- return this.categories.map(category => {
- const categoryTodos = todos.filter(todo => todo.category === category.id);
- const completed = categoryTodos.filter(todo => todo.completed).length;
- const completionRate = categoryTodos.length > 0
- ? Math.round((completed / categoryTodos.length) * 100)
- : 0;
-
- return {
- category: category.id,
- name: category.name,
- total: categoryTodos.length,
- completed,
- completionRate
- };
- });
- })
- );
- }
- }
复制代码
在页面模板中:
- <ion-header>
- <ion-toolbar>
- <ion-title>统计</ion-title>
- </ion-toolbar>
- </ion-header>
- <ion-content>
- <!-- 总体统计 -->
- <ion-card>
- <ion-card-header>
- <ion-card-title>总体统计</ion-card-title>
- </ion-card-header>
- <ion-card-content>
- <ion-grid>
- <ion-row>
- <ion-col>
- <div class="stat-card">
- <div class="stat-value">{{ (totalTodos$ | async) || 0 }}</div>
- <div class="stat-label">总计</div>
- </div>
- </ion-col>
- <ion-col>
- <div class="stat-card">
- <div class="stat-value">{{ (activeTodos$ | async) || 0 }}</div>
- <div class="stat-label">进行中</div>
- </div>
- </ion-col>
- <ion-col>
- <div class="stat-card">
- <div class="stat-value">{{ (completedTodos$ | async) || 0 }}</div>
- <div class="stat-label">已完成</div>
- </div>
- </ion-col>
- </ion-row>
- </ion-grid>
-
- <div class="ion-margin-top">
- <ion-label>完成率: {{ (completionRate$ | async) || 0 }}%</ion-label>
- <ion-progress-bar [value]="(completionRate$ | async) / 100"></ion-progress-bar>
- </div>
- </ion-card-content>
- </ion-card>
- <!-- 分类统计 -->
- <ion-card>
- <ion-card-header>
- <ion-card-title>分类统计</ion-card-title>
- </ion-card-header>
- <ion-card-content>
- <ion-list>
- <ion-item *ngFor="let stat of (categoryStats$ | async)">
- <ion-label>
- <h2>{{ stat.name }}</h2>
- <p>{{ stat.completed }} / {{ stat.total }} ({{ stat.completionRate }}%)</p>
- </ion-label>
- <ion-progress-bar
- slot="end"
- [value]="stat.completionRate / 100"
- style="width: 100px;">
- </ion-progress-bar>
- </ion-item>
- </ion-list>
- </ion-card-content>
- </ion-card>
- </ion-content>
复制代码
在页面样式中:
- // src/app/pages/stats/stats.page.scss
- .stat-card {
- text-align: center;
- padding: 10px;
-
- .stat-value {
- font-size: 24px;
- font-weight: bold;
- color: var(--ion-color-primary);
- }
-
- .stat-label {
- font-size: 14px;
- color: var(--ion-color-medium);
- }
- }
复制代码
修改tabs.router.module.ts以包含新页面:
- // src/app/tabs/tabs.router.module.ts
- import { NgModule } from '@angular/core';
- import { RouterModule, Routes } from '@angular/router';
- import { TabsPage } from './tabs.page';
- const routes: Routes = [
- {
- path: 'tabs',
- component: TabsPage,
- children: [
- {
- path: 'todos',
- loadChildren: () => import('../pages/todos/todos.module').then(m => m.TodosPageModule)
- },
- {
- path: 'stats',
- loadChildren: () => import('../pages/stats/stats.module').then(m => m.StatsPageModule)
- },
- {
- path: '',
- redirectTo: '/tabs/todos',
- pathMatch: 'full'
- }
- ]
- },
- {
- path: '',
- redirectTo: '/tabs/todos',
- pathMatch: 'full'
- }
- ];
- @NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
- })
- export class TabsPageRoutingModule {}
复制代码
修改tabs.page.html以更新标签:
- <ion-tabs>
- <ion-tab-bar slot="bottom">
- <ion-tab-button tab="todos">
- <ion-icon name="list"></ion-icon>
- <ion-label>待办事项</ion-label>
- </ion-tab-button>
-
- <ion-tab-button tab="stats">
- <ion-icon name="stats-chart"></ion-icon>
- <ion-label>统计</ion-label>
- </ion-tab-button>
- </ion-tab-bar>
- </ion-tabs>
复制代码
10.6 添加存储服务
为了确保数据持久化,我们需要添加Ionic Storage服务:
- npm install @ionic/storage
- npm install @ionic/storage-angular
复制代码
在app.module.ts中导入并配置StorageModule:
- // 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 { SplashScreen } from '@ionic-native/splash-screen/ngx';
- import { StatusBar } from '@ionic-native/status-bar/ngx';
- import { AppComponent } from './app.component';
- import { AppRoutingModule } from './app-routing.module';
- import { IonicStorageModule } from '@ionic/storage';
- @NgModule({
- declarations: [AppComponent],
- entryComponents: [],
- imports: [
- BrowserModule,
- IonicModule.forRoot(),
- AppRoutingModule,
- IonicStorageModule.forRoot()
- ],
- providers: [
- StatusBar,
- SplashScreen,
- { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
- ],
- bootstrap: [AppComponent]
- })
- export class AppModule {}
复制代码
10.7 运行应用
现在,我们可以运行应用了:
10.8 构建和发布
当应用开发完成后,可以按照前面章节介绍的方法构建和发布应用。
总结
通过本教程,我们学习了Ionic 4的基础知识和实战技能,从环境搭建到项目实战,涵盖了Ionic 4开发的各个方面。我们了解了Ionic 4的组件系统、导航和路由、数据管理、与后端API交互、调试和测试,以及如何构建和发布应用。最后,我们通过一个完整的待办事项应用项目,综合运用了所学知识。
Ionic 4是一个强大的跨平台移动应用开发框架,它允许开发者使用Web技术构建高性能、美观的移动应用。通过本教程的学习,你应该已经掌握了Ionic 4开发的基本技能,可以开始构建自己的移动应用了。
继续学习和实践,探索Ionic 4的更多特性和功能,你将能够开发出更加复杂和功能丰富的移动应用。祝你在Ionic 4开发的道路上取得成功! |
|