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