Ionic新闻应用实战 #
一、项目概述 #
1.1 功能需求 #
text
新闻应用功能
│
├── 首页
│ ├── 新闻列表
│ ├── 分类导航
│ └── 下拉刷新
│
├── 详情
│ ├── 新闻详情
│ ├── 评论列表
│ └── 分享功能
│
├── 搜索
│ ├── 搜索历史
│ └── 搜索结果
│
└── 个人中心
├── 收藏列表
├── 阅读历史
└── 设置
1.2 技术栈 #
| 技术 | 用途 |
|---|---|
| Ionic 8 | UI框架 |
| Angular 17 | 前端框架 |
| Capacitor 6 | 原生运行时 |
| NgRx | 状态管理 |
| RxJS | 响应式编程 |
二、项目结构 #
text
news-app/
├── src/
│ ├── app/
│ │ ├── core/ # 核心模块
│ │ │ ├── services/
│ │ │ │ ├── api.service.ts
│ │ │ │ ├── auth.service.ts
│ │ │ │ └── storage.service.ts
│ │ │ ├── guards/
│ │ │ │ └── auth.guard.ts
│ │ │ ├── interceptors/
│ │ │ │ └── auth.interceptor.ts
│ │ │ └── models/
│ │ │ └── news.model.ts
│ │ │
│ │ ├── store/ # 状态管理
│ │ │ ├── news/
│ │ │ │ ├── news.actions.ts
│ │ │ │ ├── news.reducer.ts
│ │ │ │ ├── news.selectors.ts
│ │ │ │ └── news.effects.ts
│ │ │ └── user/
│ │ │
│ │ ├── features/ # 功能模块
│ │ │ ├── home/
│ │ │ ├── detail/
│ │ │ ├── search/
│ │ │ └── profile/
│ │ │
│ │ ├── shared/ # 共享模块
│ │ │ ├── components/
│ │ │ └── pipes/
│ │ │
│ │ └── tabs/
│ │
│ ├── assets/
│ ├── environments/
│ └── theme/
│
├── capacitor.config.ts
└── package.json
三、数据模型 #
3.1 新闻模型 #
typescript
// models/news.model.ts
export interface News {
id: string;
title: string;
content: string;
summary: string;
author: string;
source: string;
category: string;
imageUrl: string;
publishTime: Date;
viewCount: number;
likeCount: number;
commentCount: number;
tags: string[];
}
export interface NewsListResponse {
data: News[];
total: number;
page: number;
pageSize: number;
}
export interface NewsCategory {
id: string;
name: string;
icon: string;
}
export interface Comment {
id: string;
newsId: string;
userId: string;
userName: string;
userAvatar: string;
content: string;
createTime: Date;
likeCount: number;
}
3.2 用户模型 #
typescript
// models/user.model.ts
export interface User {
id: string;
name: string;
email: string;
avatar: string;
favorites: string[];
history: HistoryItem[];
}
export interface HistoryItem {
newsId: string;
title: string;
readTime: Date;
}
四、API服务 #
4.1 API服务 #
typescript
// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = environment.apiUrl;
constructor(private http: HttpClient) {}
get<T>(endpoint: string, params?: any): Observable<T> {
let httpParams = new HttpParams();
if (params) {
Object.keys(params).forEach(key => {
httpParams = httpParams.set(key, params[key]);
});
}
return this.http.get<T>(`${this.baseUrl}${endpoint}`, { params: httpParams });
}
post<T>(endpoint: string, data: any): Observable<T> {
return this.http.post<T>(`${this.baseUrl}${endpoint}`, data);
}
}
4.2 新闻服务 #
typescript
// services/news.service.ts
import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
import { News, NewsListResponse, Comment } from '../models/news.model';
@Injectable({
providedIn: 'root'
})
export class NewsService {
constructor(private api: ApiService) {}
getNewsList(params: { page: number; pageSize: number; category?: string }) {
return this.api.get<NewsListResponse>('/news', params);
}
getNewsDetail(id: string) {
return this.api.get<News>(`/news/${id}`);
}
getComments(newsId: string, params: { page: number; pageSize: number }) {
return this.api.get<{ data: Comment[] }>(`/news/${newsId}/comments`, params);
}
searchNews(params: { keyword: string; page: number; pageSize: number }) {
return this.api.get<NewsListResponse>('/news/search', params);
}
getCategories() {
return this.api.get<{ data: any[] }>('/categories');
}
}
五、状态管理 #
5.1 Actions #
typescript
// store/news/news.actions.ts
import { createAction, props } from '@ngrx/store';
import { News, NewsCategory } from '../../models/news.model';
export const loadNews = createAction('[News] Load News', props<{ page: number; category?: string }>());
export const loadNewsSuccess = createAction('[News] Load News Success', props<{ news: News[]; total: number }>());
export const loadNewsFailure = createAction('[News] Load News Failure', props<{ error: string }>());
export const loadCategories = createAction('[News] Load Categories');
export const loadCategoriesSuccess = createAction('[News] Load Categories Success', props<{ categories: NewsCategory[] }>');
export const loadNewsDetail = createAction('[News] Load News Detail', props<{ id: string }>());
export const loadNewsDetailSuccess = createAction('[News] Load News Detail Success', props<{ news: News }>());
export const addToFavorites = createAction('[News] Add to Favorites', props<{ newsId: string }>());
export const removeFromFavorites = createAction('[News] Remove from Favorites', props<{ newsId: string }>());
5.2 Reducer #
typescript
// store/news/news.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { NewsState, initialState } from './news.state';
import * as NewsActions from './news.actions';
export const newsReducer = createReducer(
initialState,
on(NewsActions.loadNews, (state) => ({
...state,
loading: true,
error: null
})),
on(NewsActions.loadNewsSuccess, (state, { news, total }) => ({
...state,
news: [...state.news, ...news],
total,
loading: false
})),
on(NewsActions.loadNewsFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(NewsActions.loadCategoriesSuccess, (state, { categories }) => ({
...state,
categories
})),
on(NewsActions.loadNewsDetailSuccess, (state, { news }) => ({
...state,
currentNews: news
}))
);
5.3 Effects #
typescript
// store/news/news.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { NewsService } from '../../services/news.service';
import * as NewsActions from './news.actions';
@Injectable()
export class NewsEffects {
loadNews$ = createEffect(() => this.actions$.pipe(
ofType(NewsActions.loadNews),
mergeMap(({ page, category }) => this.newsService.getNewsList({ page, pageSize: 10, category }).pipe(
map(response => NewsActions.loadNewsSuccess({ news: response.data, total: response.total })),
catchError(error => of(NewsActions.loadNewsFailure({ error: error.message })))
))
));
loadCategories$ = createEffect(() => this.actions$.pipe(
ofType(NewsActions.loadCategories),
mergeMap(() => this.newsService.getCategories().pipe(
map(response => NewsActions.loadCategoriesSuccess({ categories: response.data }))
))
));
loadNewsDetail$ = createEffect(() => this.actions$.pipe(
ofType(NewsActions.loadNewsDetail),
mergeMap(({ id }) => this.newsService.getNewsDetail(id).pipe(
map(news => NewsActions.loadNewsDetailSuccess({ news }))
))
));
constructor(
private actions$: Actions,
private newsService: NewsService
) {}
}
六、页面实现 #
6.1 首页 #
typescript
// features/home/home.page.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { News, NewsCategory } from '../../models/news.model';
import * as NewsActions from '../../store/news/news.actions';
import * as NewsSelectors from '../../store/news/news.selectors';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {
news$: Observable<News[]>;
categories$: Observable<NewsCategory[]>;
loading$: Observable<boolean>;
selectedCategory = '';
page = 1;
constructor(private store: Store) {
this.news$ = this.store.select(NewsSelectors.selectNews);
this.categories$ = this.store.select(NewsSelectors.selectCategories);
this.loading$ = this.store.select(NewsSelectors.selectLoading);
}
ngOnInit() {
this.store.dispatch(NewsActions.loadCategories());
this.loadNews();
}
loadNews() {
this.store.dispatch(NewsActions.loadNews({
page: this.page,
category: this.selectedCategory
}));
}
onCategoryChange(category: string) {
this.selectedCategory = category;
this.page = 1;
this.loadNews();
}
doRefresh(event: any) {
this.page = 1;
this.loadNews();
event.target.complete();
}
loadMore(event: any) {
this.page++;
this.loadNews();
event.target.complete();
}
}
html
<!-- features/home/home.page.html -->
<ion-header>
<ion-toolbar>
<ion-title>新闻</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-segment (ionChange)="onCategoryChange($event.detail.value)">
<ion-segment-button value="">
<ion-label>全部</ion-label>
</ion-segment-button>
<ion-segment-button
*ngFor="let category of categories$ | async"
[value]="category.id">
<ion-label>{{ category.name }}</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<ion-list>
<ion-item
*ngFor="let item of news$ | async"
[routerLink]="['/tabs/home/detail', item.id]">
<ion-thumbnail slot="start">
<ion-img [src]="item.imageUrl"></ion-img>
</ion-thumbnail>
<ion-label>
<h2>{{ item.title }}</h2>
<p>{{ item.summary }}</p>
<ion-note>{{ item.publishTime | date:'MM-dd HH:mm' }}</ion-note>
</ion-label>
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
6.2 详情页 #
typescript
// features/detail/detail.page.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { News } from '../../models/news.model';
import * as NewsActions from '../../store/news/news.actions';
import * as NewsSelectors from '../../store/news/news.selectors';
@Component({
selector: 'app-detail',
templateUrl: 'detail.page.html',
styleUrls: ['detail.page.scss']
})
export class DetailPage implements OnInit {
news$: Observable<News | null>;
constructor(
private route: ActivatedRoute,
private store: Store
) {
this.news$ = this.store.select(NewsSelectors.selectCurrentNews);
}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.store.dispatch(NewsActions.loadNewsDetail({ id }));
}
}
async share() {
// 分享功能
}
toggleFavorite() {
// 收藏功能
}
}
html
<!-- features/detail/detail.page.html -->
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/tabs/home"></ion-back-button>
</ion-buttons>
<ion-title>详情</ion-title>
<ion-buttons slot="end">
<ion-button (click)="share()">
<ion-icon name="share-outline" slot="icon-only"></ion-icon>
</ion-button>
<ion-button (click)="toggleFavorite()">
<ion-icon name="heart-outline" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ng-container *ngIf="news$ | async as news">
<ion-img [src]="news.imageUrl"></ion-img>
<ion-card-header>
<ion-card-title>{{ news.title }}</ion-card-title>
<ion-card-subtitle>
{{ news.author }} · {{ news.publishTime | date:'yyyy-MM-dd HH:mm' }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<div [innerHTML]="news.content"></div>
</ion-card-content>
<ion-chip *ngFor="let tag of news.tags">
<ion-label>{{ tag }}</ion-label>
</ion-chip>
</ng-container>
</ion-content>
七、发布流程 #
7.1 构建命令 #
bash
# 开发构建
ionic build
# 生产构建
ionic build --prod
# 同步原生平台
npx cap sync
# 运行iOS
npx cap run ios
# 运行Android
npx cap run android
7.2 发布检查清单 #
- [ ] 更新版本号
- [ ] 更新应用图标
- [ ] 测试所有功能
- [ ] 检查性能
- [ ] 准备截图
- [ ] 更新应用描述
八、总结 #
8.1 项目要点 #
| 要点 | 说明 |
|---|---|
| 项目结构 | 模块化组织 |
| 状态管理 | NgRx状态管理 |
| API服务 | 统一API封装 |
| UI组件 | Ionic组件 |
8.2 下一步 #
完成了新闻应用后,继续学习 电商应用,掌握更复杂的应用开发!
最后更新:2026-03-28