Expo开发App实战指南:从技术选型到架构设计

0 阅读41分钟

引言

在移动应用开发领域,跨平台开发框架一直是开发者关注的焦点。Expo作为React Native的官方推荐开发工具链,凭借其卓越的开发体验和丰富的生态系统,已经成为构建移动应用的首选方案之一。本文将从实战角度出发,详细介绍如何使用Expo开发一个完整的移动应用程序,涵盖技术选型、业务分析、架构设计和代码实现的完整流程。

Expo不仅仅是一个开发框架,更是一整套完善的移动应用开发生态系统。它提供了从项目创建、代码编写、调试测试到应用发布的完整工具链支持。开发者无需深入了解原生开发知识,就能够快速构建出功能丰富、体验优秀的移动应用程序。这种低门槛的特性使得Expo特别适合初创团队、独立开发者以及需要快速验证产品 idea 的项目。

在本文中,我们将以一个任务管理应用作为实战案例,带领读者深入了解Expo开发的各个方面。这个案例将涵盖用户认证、数据管理、离线存储、推送通知等核心功能模块,帮助读者建立起完整的Expo开发知识体系。通过这个实战案例,读者不仅能够掌握Expo的基本使用方法,更能够学习到如何运用最佳实践来构建生产级别的移动应用。

第一部分:技术选型与决策

1.1 为什么选择Expo

在选择移动应用开发技术栈时,我们需要综合考虑多个维度的因素。开发效率、维护成本、用户体验、性能表现、生态系统成熟度等都是需要权衡的重要指标。Expo正是为了解决传统React Native开发中的诸多痛点而诞生的,它通过提供统一的开发生态和丰富的内置功能,大大简化了移动应用开发的复杂度。

从开发效率角度来看,Expo提供了即时预览功能,开发者可以在编写代码的同时实时看到应用界面的变化。这种所见即所得的开发体验极大地提升了开发效率,让开发者能够快速迭代和优化产品。此外,Expo还提供了完善的TypeScript支持,使得代码的类型安全得到了保障,减少了运行时错误的发生。Expo的JavaScript/TypeScript开发体验与Web开发非常相似,这对于前端开发者来说大大降低了学习成本。

在维护成本方面,Expo的Over-The-Air更新功能允许开发者无需重新提交应用商店审核就能更新应用代码。这意味着我们可以快速修复线上Bug,发布新功能,极大地缩短了产品迭代周期。同时,Expo会自动处理React Native版本升级和原生依赖管理,让我们无需深入了解原生开发细节就能保持应用的先进性。这种低维护成本的特性对于资源有限的团队来说尤为重要。

从生态系统角度来看,Expo拥有丰富的官方和社区维护的库,涵盖了相机、文件系统、位置服务、传感器、推送通知等各个方面。这些库都经过了良好的测试和优化,开箱即用,大大减少了开发者在第三方库选型和集成上的工作量。Expo的生态系统还在不断壮大,社区活跃度高,遇到问题容易找到解决方案。

1.2 技术栈完整解析

在确定了使用Expo作为开发框架后,我们需要进一步选择与之配套的技术栈。每一个技术选择都应该服务于项目的具体需求,在功能、性能、开发效率之间找到最佳平衡点。以下是我们为实战项目选择的技术栈及其理由。

React Navigation是我们选择的导航解决方案,它是React Native生态中最成熟、使用最广泛的导航库。React Navigation提供了声明式的API设计,与React的组件化思想完美契合。它支持栈导航、标签页导航、抽屉导航等多种导航模式,能够满足各种复杂的应用导航需求。在Expo项目中使用React Navigation非常简单,Expo SDK已经内置了对其的优化支持,确保了良好的性能和兼容性。

在状态管理方面,我们选择使用Zustand作为主要的状态管理方案。Zustand是一个轻量级但功能强大的状态管理库,它采用了React Hooks的API设计,简洁直观,学习成本极低。相比Redux等传统状态管理方案,Zustand的代码量更少,模板代码几乎为零,同时支持中间件扩展,能够满足各种复杂的状态管理需求。Zustand还提供了出色的TypeScript支持,类型推断自然流畅。

对于数据持久化需求,我们选择使用AsyncStorage结合SQLite的方案。AsyncStorage适合存储简单的键值对数据,如用户偏好设置、认证令牌等。对于结构化程度较高的数据,如任务列表、用户信息等,我们使用expo-sqlite来实现SQLite数据库操作。SQLite作为一种嵌入式数据库,在移动设备上运行高效可靠,非常适合离线优先的应用场景。

对于HTTP请求和API交互,我们选择使用axios配合React Query。axios提供了简洁优雅的API设计和完善的错误处理机制,而React Query则为我们提供了强大的服务端状态管理能力,包括缓存、自动重试、分页等功能的开箱即用。这种组合让我们能够以声明式的方式处理数据获取和同步,大大简化了异步数据管理的复杂度。

1.3 开发工具链配置

完善的开发工具链配置是保证开发效率和代码质量的重要基础。在Expo项目中,我们需要配置开发服务器、TypeScript编译、代码检查、格式化工具等多个方面。合理配置这些工具能够让我们在开发过程中及时发现问题,保证代码风格的一致性,提升团队协作效率。

首先是开发服务器的配置。Expo提供了expo-cli作为命令行工具,我们可以配置开发服务器的相关参数,如端口号、加密设置、局域网访问等。对于团队协作场景,配置局域网访问非常重要,这样团队成员可以在同一网络下直接通过IP地址访问开发中的应用。开发服务器还支持热模块替换(HMR),能够在代码修改时快速更新应用界面而无需重新加载整个应用。

TypeScript配置是Expo项目的核心组成部分。我们需要在tsconfig.json中仔细配置编译选项,包括严格模式、路径别名、装饰器支持等。严格模式能够帮助我们在编译阶段发现更多潜在问题,提升代码质量。路径别名配置能够让我们使用更简洁的导入路径,如使用@/components代替相对路径的复杂引用。对于需要使用装饰器的库如MobX,我们需要确保experimentalDecorators和emitDecoratorMetadata选项正确配置。

ESLint和Prettier的配置同样不可或缺。ESLint负责代码质量检查,能够发现语法错误、潜在bug、不良编码实践等问题。Prettier则负责代码格式化,确保团队成员的代码风格保持一致。我们需要为这两个工具创建统一的配置文件,并将其集成到编辑器和持续集成流程中。建议使用eslint-config-airbnb或eslint-config-standard等成熟的配置作为基础,根据项目需求进行定制调整。

第二部分:业务分析与需求梳理

2.1 任务管理应用需求概述

为了更好地展示Expo开发的完整流程,我们将以一个任务管理应用作为实战案例。任务管理是一个经典的应用场景,几乎每个人都需要管理日常任务、待办事项的场景。这个应用将涵盖用户管理、任务管理、项目管理、标签分类等核心功能,能够充分展示Expo开发的各个方面。

从用户角度来看,一个实用的任务管理应用需要具备以下核心能力:创建、编辑、删除任务的基本操作能力;将任务分配到不同项目进行分类管理的能力;为任务设置截止日期、优先级、提醒时间的能力;使用标签对任务进行多维度分类的能力;支持子任务和任务依赖的能力;任务搜索和筛选的能力。这些功能看似简单,但要做好每一个细节都需要精心设计。

从技术角度来看,我们需要解决以下挑战:如何在没有网络的情况下正常使用应用;如何保证数据在多设备间的同步;如何处理大量数据的性能问题;如何优雅地管理复杂的应用状态;如何提供流畅的用户交互体验。这些技术挑战的解决方案将贯穿我们的整个开发过程,帮助我们深入理解Expo开发的最佳实践。

2.2 功能模块划分

在明确了整体需求后,我们需要将应用划分为若干个功能模块,每个模块负责相对独立的业务逻辑。这种模块化设计能够提升代码的可维护性和可测试性,便于团队协作和功能扩展。根据任务管理应用的特点,我们将其划分为以下主要模块。

用户认证模块负责用户的注册、登录、密码重置、权限验证等功能。虽然是一个相对独立的功能模块,但它与其他所有模块都有数据层面的关联。用户认证模块需要处理各种异常情况,如网络错误、账户不存在、密码错误、Token过期等,并给出友好的用户提示。在实现上,我们采用JWT进行身份认证,Token存储在安全的存储区域,并实现自动刷新和过期处理机制。

任务管理模块是应用的核心模块,负责任务的全生命周期管理。这包括任务的创建、编辑、删除、状态变更等基本操作,还包括任务的排序、筛选、批量操作等高级功能。任务管理模块需要与后端API进行数据同步,同时维护本地缓存以支持离线操作。我们采用乐观更新策略,在用户操作后立即更新UI,同时在后台同步数据到服务器,当同步失败时进行适当的错误处理和重试。

项目管理模块负责项目的创建、编辑、删除和成员管理。每个项目可以包含多个任务,项目的完成情况反映了整体进度。项目管理还需要处理项目权限问题,如哪些用户可以查看、编辑特定项目的内容。在界面上,项目通常以列表或看板的形式呈现,我们选择使用列表形式以保持界面简洁。

标签系统为任务提供了多维度的分类能力。一个任务可以同时属于多个标签,标签之间可以有关联关系。标签系统需要支持自定义颜色、图标等视觉元素,让用户能够直观地识别不同类型的任务。标签的增删改查操作需要实时同步到所有相关界面。

通知提醒模块负责向用户推送任务相关的提醒通知。这包括截止日期提醒、任务分配通知、每日任务摘要等。通知模块需要与系统的通知服务集成,处理通知权限请求,并提供通知偏好设置界面。用户应该能够选择接收哪些类型的通知,以及通知的触发时间。

2.3 数据模型设计

良好的数据模型是应用稳定性和可扩展性的基础。在设计数据模型时,我们需要考虑数据的完整性、一致性、查询效率等多个方面。以下是我们为任务管理应用设计的数据模型,包括主要实体及其关系。

用户实体是整个应用的基础,包含用户的基本信息和认证数据。用户表的主要字段包括:用户ID作为主键、用户名用于登录和显示、邮箱作为唯一标识和联系方式、密码哈希保证安全性、创建时间记录账户创建日期、最后登录时间用于活跃度分析、头像URL用于界面展示。用户的设置偏好可以存储在关联的设置表中,以JSON格式存储以支持灵活扩展。

任务实体是数据模型的核心,包含任务的所有属性和状态信息。任务表的主要字段包括:任务ID作为唯一标识、标题简要描述任务内容、详细描述存储任务的完整信息、所属项目ID建立与项目的关联、创建者ID记录任务创建人、负责人ID表示当前处理人、截止日期用于时间管理、优先级用数字表示重要程度、状态包括待处理、进行中、已完成等、创建时间和更新时间用于数据同步和排序。任务还可能包含子任务,通过父子任务ID关联实现。

项目实体用于组织和管理任务集合。项目表的主要字段包括:项目ID、项目名称、项目描述、项目图标或颜色、创建者ID、项目成员列表、创建时间和更新时间。项目成员关系通过关联表维护,支持不同的角色如创建者、管理员、成员等。

标签实体提供了灵活的任务分类能力。标签表的主要字段包括:标签ID、标签名称、标签颜色、标签图标、创建者ID。任务与标签的关系通过多对多关联表维护,允许一个任务有多个标签,一个标签包含多个任务。

提醒实体记录用户设置的任务提醒。提醒表的主要字段包括:提醒ID、关联的任务ID、提醒时间、提醒类型、是否已触发、是否已确认。

第三部分:架构设计与模式

3.1 应用架构概览

在移动应用开发中,良好的架构设计是确保应用长期可维护性的关键。我们采用分层架构结合功能模块组织的混合架构模式,既保证了代码的清晰结构,又便于功能扩展和维护。整体架构分为表现层、业务层、数据层和基础设施层四个主要层次。

表现层负责用户界面的呈现和交互处理。这一层采用React的组件化设计,将界面拆分为原子组件、分子组件和有机组件的层次结构。原子组件如按钮、输入框、图标等是最基本的UI元素,具有完整的样式和行为定义。分子组件由原子组件组合而成,如搜索框由输入框和图标按钮组成。有机组件则代表了完整的业务界面,如任务列表项、项目卡片等。表现层通过React Hooks与业务层通信,触发业务操作并响应状态变化。

业务层负责处理应用的业务逻辑和数据流转。这一层包含应用的核心业务服务,如认证服务、任务服务、项目服务等。每个服务类封装了特定业务领域的相关操作,提供清晰的服务接口。业务层还包含数据转换器,将API返回的原始数据转换为应用内部使用的数据模型,反之亦然。中间件机制用于在请求处理流程中注入日志、错误处理、性能监控等横切关注点。

数据层负责与各种数据源进行交互,包括远程API、本地数据库和缓存系统。数据层实现了仓储模式,为业务层提供统一的数据访问接口。每个仓储类对应一个数据实体,封装了该实体相关的所有CRUD操作。数据层实现了数据同步机制,处理离线操作的队列管理和冲突解决。对于频繁访问的数据,我们实现了多级缓存策略,包括内存缓存和持久化缓存。

基础设施层提供了应用运行所需的基础服务和工具函数。这包括日志记录、错误处理、网络请求、本地存储、推送通知等通用功能。基础设施层被其他所有层次依赖,是整个应用的技术基石。我们将基础设施层的服务实现为单例模式,确保全局唯一的实例和一致的配置。

3.2 目录结构设计

合理的目录结构是架构落地的具体体现。我们采用基于功能的目录组织方式,将相关代码集中管理,便于查找和维护。以下是项目的目录结构和各部分的职责说明。

src/
├── components/          # 可复用UI组件
│   ├── atoms/          # 原子组件
│   ├── molecules/       # 分子组件
│   └── organisms/      # 有机组件
├── screens/            # 页面组件
├── navigation/         # 导航配置
├── services/           # 业务服务层
├── repositories/       # 数据仓储层
├── stores/             # 状态管理
├── models/             # 数据模型
├── hooks/              # 自定义Hooks
├── utils/              # 工具函数
├── constants/          # 常量定义
├── types/              # TypeScript类型
├── api/                # API客户端
└── assets/             # 静态资源

components目录包含了应用中的可复用UI组件。我们采用原子设计方法论,将组件分为原子、分子和有机三个层级。atoms目录包含最基础的组件,如按钮、文本、图标等;molecules目录包含由原子组件组合而成的中等复杂度组件,如搜索栏、卡片头部等;organisms目录包含完整的业务组件,如任务卡片、项目列表项等。每个组件都包含组件文件、样式文件和测试文件,保持代码的完整性。

screens目录包含了应用的各个页面组件。每个页面通常对应导航系统中的一个路由,页面组件负责整合各种组件来构建完整的页面视图。页面组件应该保持轻薄,将复杂的业务逻辑委托给services层处理。

services目录包含了应用的业务服务类。每个服务类专注于一个业务领域,如AuthService处理认证相关逻辑,TaskService处理任务相关操作。服务类通过调用repositories层来访问数据,同时可以包含业务规则验证和数据转换逻辑。

repositories目录包含了数据访问层的实现。每个仓储类对应一个主要数据实体,封装了该实体相关的所有数据操作。仓储类负责与API客户端或本地数据库进行交互,对上层屏蔽数据存储的细节。

stores目录包含了Zustand状态管理相关的文件。我们将不同领域的状态分别管理,如authStore、taskStore、projectStore等。每个状态store包含状态定义、actions定义和计算属性。状态store不直接处理数据持久化,而是通过调用services来完成数据操作。

3.3 状态管理模式

状态管理是React应用开发中的核心话题,良好的状态管理能够让应用的行为更加可预测和可调试。在我们的Expo项目中,我们采用Zustand作为主要的状态管理方案,结合React Query进行服务端状态管理,形成了清晰的状态管理层次。

Zustand的使用非常简单直观。我们定义一个store时,首先声明store包含的状态,然后定义修改状态的actions。Zustand的actions可以直接修改状态,不像Redux那样需要返回新的状态副本,这使得代码更加简洁。对于需要异步操作的场景,Zustand支持在actions中执行异步代码,我们可以方便地调用services来完成数据操作。

import { create } from 'zustand';
import { Task, TaskStatus } from '@/types';
import { TaskService } from '@/services/TaskService';

interface TaskState {
  tasks: Task[];
  selectedTask: Task | null;
  isLoading: boolean;
  error: string | null;

  // Actions
  fetchTasks: (projectId?: string) => Promise<void>;
  createTask: (task: Partial<Task>) => Promise<Task>;
  updateTask: (id: string, updates: Partial<Task>) => Promise<void>;
  deleteTask: (id: string) => Promise<void>;
  selectTask: (task: Task | null) => void;
  clearError: () => void;
}

export const useTaskStore = create<TaskState>((set, get) => ({
  tasks: [],
  selectedTask: null,
  isLoading: false,
  error: null,

  fetchTasks: async (projectId?: string) => {
    set({ isLoading: true, error: null });
    try {
      const tasks = await TaskService.getTasks(projectId);
      set({ tasks, isLoading: false });
    } catch (error) {
      set({ error: (error as Error).message, isLoading: false });
    }
  },

  createTask: async (taskData: Partial<Task>) => {
    set({ isLoading: true, error: null });
    try {
      const newTask = await TaskService.createTask(taskData);
      set((state) => ({
        tasks: [...state.tasks, newTask],
        isLoading: false
      }));
      return newTask;
    } catch (error) {
      set({ error: (error as Error).message, isLoading: false });
      throw error;
    }
  },

  updateTask: async (id: string, updates: Partial<Task>) => {
    // 乐观更新:立即更新UI
    const originalTasks = get().tasks;
    set((state) => ({
      tasks: state.tasks.map((task) =>
        task.id === id ? { ...task, ...updates } : task
      )
    }));

    try {
      await TaskService.updateTask(id, updates);
    } catch (error) {
      // 回滚到原始状态
      set({ tasks: originalTasks, error: (error as Error).message });
      throw error;
    }
  },

  deleteTask: async (id: string) => {
    const originalTasks = get().tasks;
    set((state) => ({
      tasks: state.tasks.filter((task) => task.id !== id)
    }));

    try {
      await TaskService.deleteTask(id);
    } catch (error) {
      set({ tasks: originalTasks, error: (error as Error).message });
      throw error;
    }
  },

  selectTask: (task: Task | null) => {
    set({ selectedTask: task });
  },

  clearError: () => {
    set({ error: null });
  }
}));

对于需要与后端同步的数据,我们使用React Query来管理服务端状态。React Query提供了自动缓存、后台更新、乐观更新等功能,大大简化了服务端数据管理的复杂度。我们为每个API操作定义对应的query或mutation,React Query会自动处理缓存、更新和错误处理。

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { TaskService } from '@/services/TaskService';
import { useTaskStore } from '@/stores/taskStore';
import { Task, CreateTaskDTO } from '@/types';

// 查询Hook
export function useTasks(projectId?: string) {
  const store = useTaskStore();

  return useQuery({
    queryKey: ['tasks', projectId],
    queryFn: () => TaskService.getTasks(projectId),
    initialData: store.tasks,
    onSuccess: (data) => {
      store.fetchTasks(projectId);
    }
  });
}

// 创建任务Mutation
export function useCreateTask() {
  const queryClient = useQueryClient();
  const store = useTaskStore();

  return useMutation({
    mutationFn: (newTask: CreateTaskDTO) => TaskService.createTask(newTask),
    onMutate: async (newTask) => {
      // 取消所有出站查询
      await queryClient.cancelQueries({ queryKey: ['tasks'] });

      // 保存旧数据用于回滚
      const previousTasks = queryClient.getQueryData(['tasks']);

      // 添加新任务到缓存
      queryClient.setQueryData(['tasks'], (old: Task[] | undefined) => [
        ...(old || []),
        { ...newTask, id: 'temp-id', status: 'pending' } as Task
      ]);

      return { previousTasks };
    },
    onError: (err, newTask, context) => {
      // 回滚到之前的数据
      queryClient.setQueryData(['tasks'], context?.previousTasks);
    },
    onSettled: () => {
      // 重新获取数据以确保一致性
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      store.fetchTasks();
    }
  });
}

3.4 错误处理与边界情况

健壮的错误处理是应用可靠性的重要保障。在移动应用中,网络不稳定、服务器异常、用户操作失误等情况时有发生,我们需要优雅地处理这些情况,给用户良好的体验,同时收集有用的错误信息用于调试和监控。

我们建立了分层的错误处理机制。在基础设施层,我们定义了统一的错误类型和错误处理中间件。在业务层,每个服务方法都应该捕获和处理预期的错误,对于未预期的错误应该记录日志并抛出统一格式的错误。在表现层,我们通过错误边界组件和状态管理来处理错误状态,给用户友好的错误提示。

// 错误类型定义
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode?: number,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = 'AppError';
    Error.captureStackTrace(this, this.constructor);
  }
}

// 预定义的业务错误
export const ErrorCodes = {
  NETWORK_ERROR: 'NETWORK_ERROR',
  UNAUTHORIZED: 'UNAUTHORIZED',
  NOT_FOUND: 'NOT_FOUND',
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  SERVER_ERROR: 'SERVER_ERROR',
  OFFLINE_ERROR: 'OFFLINE_ERROR'
} as const;

// 错误工厂函数
export function createError(
  code: keyof typeof ErrorCodes,
  message: string,
  statusCode?: number
): AppError {
  return new AppError(message, ErrorCodes[code], statusCode);
}

// 网络错误处理
export async function handleNetworkError(error: unknown): Promise<never> {
  if (error instanceof AppError) {
    throw error;
  }

  if (isNetworkError(error)) {
    throw createError(
      'NETWORK_ERROR',
      '网络连接失败,请检查您的网络设置',
      0
    );
  }

  throw createError(
    'SERVER_ERROR',
    '服务器错误,请稍后再试',
    500
  );
}

// 错误边界组件
export class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback?: React.ComponentType<ErrorProps> },
  { hasError: boolean; error: Error | null }
> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // 记录错误日志
    logger.error('React Error Boundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      const FallbackComponent = this.props.fallback || DefaultErrorFallback;
      return (
        <FallbackComponent
          error={this.state.error!}
          resetError={() => this.setState({ hasError: false, error: null })}
        />
      );
    }

    return this.props.children;
  }
}

对于离线操作的处理,我们采用队列管理的策略。当设备离线时,用户的所有数据操作会进入一个待同步队列,当网络恢复后自动同步。这种设计让应用在离线环境下也能正常使用,同时保证了数据的最终一致性。

// 离线操作队列
interface QueuedOperation {
  id: string;
  type: 'CREATE' | 'UPDATE' | 'DELETE';
  entity: string;
  entityId: string;
  payload: any;
  timestamp: number;
  retryCount: number;
}

class OfflineQueueManager {
  private queue: QueuedOperation[] = [];
  private isOnline: boolean = true;
  private syncInProgress: boolean = false;

  constructor() {
    // 监听网络状态变化
    Network.useNetworkState((state) => {
      this.isOnline = state.isConnected ?? false;
      if (this.isOnline && !this.syncInProgress) {
        this.processQueue();
      }
    });
  }

  async enqueue(operation: Omit<QueuedOperation, 'id' | 'timestamp' | 'retryCount'>) {
    const queuedOp: QueuedOperation = {
      ...operation,
      id: generateId(),
      timestamp: Date.now(),
      retryCount: 0
    };

    this.queue.push(queuedOp);
    await this.persistQueue();

    if (this.isOnline) {
      this.processQueue();
    }
  }

  private async processQueue() {
    if (this.syncInProgress || this.queue.length === 0) return;

    this.syncInProgress = true;

    while (this.queue.length > 0 && this.isOnline) {
      const operation = this.queue[0];

      try {
        await this.executeOperation(operation);
        this.queue.shift();
        await this.persistQueue();
      } catch (error) {
        operation.retryCount++;

        if (operation.retryCount >= MAX_RETRY_COUNT) {
          this.queue.shift();
          await this.notifySyncFailure(operation, error);
        } else {
          // 指数退避等待
          await this.delay(Math.pow(2, operation.retryCount) * 1000);
        }
      }
    }

    this.syncInProgress = false;
  }

  private async executeOperation(operation: QueuedOperation) {
    switch (operation.type) {
      case 'CREATE':
        return TaskService.createTask(operation.payload);
      case 'UPDATE':
        return TaskService.updateTask(operation.entityId, operation.payload);
      case 'DELETE':
        return TaskService.deleteTask(operation.entityId);
    }
  }

  private async persistQueue() {
    await AsyncStorage.setItem('offline_queue', JSON.stringify(this.queue));
  }

  private async loadQueue() {
    const data = await AsyncStorage.getItem('offline_queue');
    if (data) {
      this.queue = JSON.parse(data);
    }
  }
}

第四部分:核心代码实现

4.1 项目初始化与配置

项目初始化是Expo开发的起点,一个配置完善的初始项目能够为后续开发奠定良好基础。我们将详细讲解如何创建Expo项目、配置TypeScript、以及设置开发环境和工具链。

创建Expo项目非常简单,使用npx create-expo-app命令即可。这个命令会创建一个包含Expo SDK和必要依赖的新项目。我们建议选择TypeScript模板,这样可以获得完整的类型检查支持。如果创建的是JavaScript项目,可以使用npx tsc init命令初始化TypeScript配置。

# 创建新的Expo项目
npx create-expo-app@latest TaskMaster --template blank-typescript

# 进入项目目录
cd TaskMaster

# 安装核心依赖
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context
npm install zustand @tanstack/react-query
npm install axios
npm install expo-sqlite expo-notifications expo-device
npm install @react-native-async-storage/async-storage
npm install date-fns uuid
npm install expo-constants expo-linking

# 安装开发依赖
npm install -D @types/uuid eslint prettier eslint-config-universe

项目创建完成后,我们需要配置app.json来定义应用的元数据和配置。app.json是Expo项目的配置文件,包含了应用名称、图标、Splash屏幕、SDK版本等重要信息。我们可以根据需要配置不同的平台设置,如iOS的bundleIdentifier和Android的applicationId。

{
  "expo": {
    "name": "TaskMaster",
    "slug": "taskmaster",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.taskmaster.app",
      "infoPlist": {
        "NSCameraUsageDescription": "需要使用相机来扫描二维码",
        "NSPhotoLibraryUsageDescription": "需要访问相册来添加任务附件"
      }
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.taskmaster.app",
      "permissions": [
        "CAMERA",
        "READ_EXTERNAL_STORAGE",
        "WRITE_EXTERNAL_STORAGE",
        "RECEIVE_BOOT_COMPLETED",
        "VIBRATE",
        "INTERNET",
        "ACCESS_NETWORK_STATE"
      ]
    },
    "plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#4A90D9"
        }
      ],
      "expo-sqlite"
    ],
    "extra": {
      "eas": {
        "projectId": "your-project-id"
      }
    }
  }
}

TypeScript配置是保障代码质量的重要工具。我们需要在tsconfig.json中仔细配置编译选项。以下是推荐的TypeScript配置,它启用了严格模式、路径别名和最新的ECMAScript特性支持。

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "lib": ["ESNext"],
    "jsx": "react-native",
    "strict": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "noEmit": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@screens/*": ["src/screens/*"],
      "@services/*": ["src/services/*"],
      "@stores/*": ["src/stores/*"],
      "@hooks/*": ["src/hooks/*"],
      "@types/*": ["src/types/*"],
      "@utils/*": ["src/utils/*"],
      "@constants/*": ["src/constants/*"],
      "@api/*": ["src/api/*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules", "dist", ".expo"]
}

为了让路径别名生效,我们需要在babel.config.js中配置babel-plugin-module-resolver插件。这个插件会在编译时将路径别名转换为实际的相对路径。

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      [
        'module-resolver',
        {
          root: ['./src'],
          alias: {
            '@': './src',
            '@components': './src/components',
            '@screens': './src/screens',
            '@services': './src/services',
            '@stores': './src/stores',
            '@hooks': './src/hooks',
            '@types': './src/types',
            '@utils': './src/utils',
            '@constants': './src/constants',
            '@api': './src/api'
          }
        }
      ]
    ]
  };
};

4.2 导航系统实现

导航系统是移动应用的核心骨架,决定了用户在应用中的浏览体验。React Navigation是React Native生态中最成熟的导航解决方案,它提供了声明式的API设计和丰富的导航模式支持。我们将为任务管理应用配置完整的导航系统。

首先,我们需要定义应用的路由类型,确保类型安全。TypeScript的强类型支持能够在编译时发现导航相关的错误,提升代码质量。

// navigation/types.ts
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';

// 认证流程
export type AuthStackParamList = {
  Welcome: undefined;
  Login: undefined;
  Register: undefined;
  ForgotPassword: undefined;
};

// 主应用底部标签
export type MainTabParamList = {
  Home: undefined;
  Projects: NavigatorScreenParams<ProjectStackParamList>;
  Tasks: NavigatorScreenParams<TaskStackParamList>;
  Profile: undefined;
};

// 项目模块
export type ProjectStackParamList = {
  ProjectList: undefined;
  ProjectDetail: { projectId: string };
  CreateProject: undefined;
  EditProject: { projectId: string };
};

// 任务模块
export type TaskStackParamList = {
  TaskList: undefined;
  TaskDetail: { taskId: string };
  CreateTask: { projectId?: string };
  EditTask: { taskId: string };
};

// 组合类型定义
export type RootStackParamList = {
  Auth: NavigatorScreenParams<AuthStackParamList>;
  Main: NavigatorScreenParams<MainTabParamList>;
};

// Screen Props类型
export type AuthScreenProps<T extends keyof AuthStackParamList> = NativeStackScreenProps<
  AuthStackParamList,
  T
>;

export type MainTabScreenProps<T extends keyof MainTabParamList> = CompositeScreenProps<
  BottomTabScreenProps<MainTabParamList, T>,
  RootStackScreenProps<keyof RootStackParamList>
>;

export type ProjectScreenProps<T extends keyof ProjectStackParamList> = CompositeScreenProps<
  NativeStackScreenProps<ProjectStackParamList, T>,
  RootStackScreenProps<keyof RootStackParamList>
>;

export type TaskScreenProps<T extends keyof TaskStackParamList> = CompositeScreenProps<
  NativeStackScreenProps<TaskStackParamList, T>,
  RootStackScreenProps<keyof RootStackParamList>
>;

// Navigation Prop类型(用于组件内部导航)
declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

接下来,我们创建导航容器的配置。导航容器是整个应用的导航状态管理者,它包装了整个应用,提供了统一的导航上下文。

// navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useAuth } from '@/hooks/useAuth';
import { Colors } from '@constants/theme';

// 导入各层导航器
import { AuthNavigator } from './AuthNavigator';
import { MainTabNavigator } from './MainTabNavigator';

// 导入类型
import { RootStackParamList } from './types';

// 创建导航器实例
const RootStack = createNativeStackNavigator<RootStackParamList>();

export function AppNavigator() {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) {
    // 显示启动画面或加载指示器
    return null;
  }

  return (
    <NavigationContainer>
      <RootStack.Navigator
        screenOptions={{
          headerShown: false,
          animation: 'fade'
        }}
      >
        {isAuthenticated ? (
          <RootStack.Screen name="Main" component={MainTabNavigator} />
        ) : (
          <RootStack.Screen name="Auth" component={AuthNavigator} />
        )}
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

底部标签导航器是主应用的主要导航结构,它包含了应用的四个主要模块入口。我们为每个标签配置了图标和标题,使得导航更加直观。

// navigation/MainTabNavigator.tsx
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Colors } from '@constants/theme';

// 导入屏幕组件
import { HomeScreen } from '@screens/HomeScreen';
import { ProjectListScreen } from '@screens/ProjectListScreen';
import { ProjectDetailScreen } from '@screens/ProjectDetailScreen';
import { CreateProjectScreen } from '@screens/CreateProjectScreen';
import { TaskListScreen } from '@screens/TaskListScreen';
import { TaskDetailScreen } from '@screens/TaskDetailScreen';
import { CreateTaskScreen } from '@screens/CreateTaskScreen';
import { ProfileScreen } from '@screens/ProfileScreen';

// 导入类型
import { MainTabParamList, ProjectStackParamList, TaskStackParamList } from './types';
import { TabBarIcon } from '@components/molecules/TabBarIcon';

const Tab = createBottomTabNavigator<MainTabParamList>();
const ProjectStack = createNativeStackNavigator<ProjectStackParamList>();
const TaskStack = createNativeStackNavigator<TaskStackParamList>();

// 项目堆栈导航器
function ProjectStackNavigator() {
  return (
    <ProjectStack.Navigator
      screenOptions={{
        headerStyle: { backgroundColor: Colors.surface },
        headerTintColor: Colors.text,
        headerTitleStyle: { fontWeight: '600' }
      }}
    >
      <ProjectStack.Screen
        name="ProjectList"
        component={ProjectListScreen}
        options={{ title: '项目' }}
      />
      <ProjectStack.Screen
        name="ProjectDetail"
        component={ProjectDetailScreen}
        options={{ title: '项目详情' }}
      />
      <ProjectStack.Screen
        name="CreateProject"
        component={CreateProjectScreen}
        options={{ title: '创建项目', presentation: 'modal' }}
      />
    </ProjectStack.Navigator>
  );
}

// 任务堆栈导航器
function TaskStackNavigator() {
  return (
    <TaskStack.Navigator
      screenOptions={{
        headerStyle: { backgroundColor: Colors.surface },
        headerTintColor: Colors.text,
        headerTitleStyle: { fontWeight: '600' }
      }}
    >
      <TaskStack.Screen
        name="TaskList"
        component={TaskListScreen}
        options={{ title: '所有任务' }}
      />
      <TaskStack.Screen
        name="TaskDetail"
        component={TaskDetailScreen}
        options={{ title: '任务详情' }}
      />
      <TaskStack.Screen
        name="CreateTask"
        component={CreateTaskScreen}
        options={{ title: '创建任务', presentation: 'modal' }}
      />
    </TaskStack.Navigator>
  );
}

// 主标签导航器
export function MainTabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={{
        tabBarActiveTintColor: Colors.primary,
        tabBarInactiveTintColor: Colors.textSecondary,
        tabBarStyle: styles.tabBar,
        headerShown: false
      }}
    >
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        options={{
          title: '首页',
          tabBarIcon: ({ color, size }) => (
            <TabBarIcon name="home" color={color} size={size} />
          )
        }}
      />
      <Tab.Screen
        name="Projects"
        component={ProjectStackNavigator}
        options={{
          title: '项目',
          tabBarIcon: ({ color, size }) => (
            <TabBarIcon name="folder" color={color} size={size} />
          )
        }}
      />
      <Tab.Screen
        name="Tasks"
        component={TaskStackNavigator}
        options={{
          title: '任务',
          tabBarIcon: ({ color, size }) => (
            <TabBarIcon name="checkbox" color={color} size={size} />
          )
        }}
      />
      <Tab.Screen
        name="Profile"
        component={ProfileScreen}
        options={{
          title: '我的',
          tabBarIcon: ({ color, size }) => (
            <TabBarIcon name="user" color={color} size={size} />
          )
        }}
      />
    </Tab.Navigator>
  );
}

const styles = StyleSheet.create({
  tabBar: {
    backgroundColor: Colors.surface,
    borderTopColor: Colors.border,
    borderTopWidth: StyleSheet.hairlineWidth,
    paddingTop: 8,
    paddingBottom: 8,
    height: 60
  }
});

4.3 核心组件实现

组件是React应用的构建块,良好的组件设计能够让代码更加可复用和可维护。我们采用原子设计方法论,将组件分为原子、分子和有机三个层级,每个层级都有明确的职责边界。

首先是原子组件的实现。原子组件是最基础的UI元素,它们不依赖其他组件,但必须有完整的样式和行为定义。以下是几个核心原子组件的实现。

// components/atoms/Button/Button.tsx
import React from 'react';
import {
  TouchableOpacity,
  Text,
  StyleSheet,
  ActivityIndicator,
  ViewStyle,
  TextStyle
} from 'react-native';
import { Colors } from '@constants/theme';
import { ButtonVariant, ButtonSize } from './Button.types';

interface ButtonProps {
  title: string;
  onPress: () => void;
  variant?: ButtonVariant;
  size?: ButtonSize;
  disabled?: boolean;
  loading?: boolean;
  fullWidth?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  style?: ViewStyle;
  textStyle?: TextStyle;
}

export function Button({
  title,
  onPress,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  loading = false,
  fullWidth = false,
  leftIcon,
  rightIcon,
  style,
  textStyle
}: ButtonProps) {
  const isDisabled = disabled || loading;

  return (
    <TouchableOpacity
      style={[
        styles.base,
        styles[variant],
        styles[size],
        fullWidth && styles.fullWidth,
        isDisabled && styles.disabled,
        style
      ]}
      onPress={onPress}
      disabled={isDisabled}
      activeOpacity={0.7}
    >
      {loading ? (
        <ActivityIndicator
          color={variant === 'primary' ? Colors.white : Colors.primary}
          size="small"
        />
      ) : (
        <>
          {leftIcon && <>{leftIcon}</>}
          <Text
            style={[
              styles.text,
              styles[`${variant}Text`],
              styles[`${size}Text`],
              textStyle
            ]}
          >
            {title}
          </Text>
          {rightIcon && <>{rightIcon}</>}
        </>
      )}
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  base: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 12,
    gap: 8
  },
  primary: {
    backgroundColor: Colors.primary
  },
  secondary: {
    backgroundColor: Colors.background,
    borderWidth: 1,
    borderColor: Colors.border
  },
  outline: {
    backgroundColor: 'transparent',
    borderWidth: 1,
    borderColor: Colors.primary
  },
  ghost: {
    backgroundColor: 'transparent'
  },
  danger: {
    backgroundColor: Colors.error
  },
  small: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    minHeight: 36
  },
  medium: {
    paddingVertical: 12,
    paddingHorizontal: 24,
    minHeight: 48
  },
  large: {
    paddingVertical: 16,
    paddingHorizontal: 32,
    minHeight: 56
  },
  fullWidth: {
    width: '100%'
  },
  disabled: {
    opacity: 0.5
  },
  text: {
    fontWeight: '600'
  },
  primaryText: {
    color: Colors.white
  },
  secondaryText: {
    color: Colors.text
  },
  outlineText: {
    color: Colors.primary
  },
  ghostText: {
    color: Colors.primary
  },
  dangerText: {
    color: Colors.white
  },
  smallText: {
    fontSize: 14
  },
  mediumText: {
    fontSize: 16
  },
  largeText: {
    fontSize: 18
  }
});
// components/atoms/Input/Input.tsx
import React, { forwardRef } from 'react';
import {
  TextInput,
  View,
  Text,
  StyleSheet,
  TextInputProps,
  ViewStyle
} from 'react-native';
import { Colors } from '@constants/theme';

interface InputProps extends TextInputProps {
  label?: string;
  error?: string;
  hint?: string;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  containerStyle?: ViewStyle;
}

export const Input = forwardRef<TextInput, InputProps>(
  (
    {
      label,
      error,
      hint,
      leftIcon,
      rightIcon,
      containerStyle,
      style,
      ...props
    },
    ref
  ) => {
    const hasError = !!error;

    return (
      <View style={[styles.container, containerStyle]}>
        {label && <Text style={styles.label}>{label}</Text>}
        <View
          style={[
            styles.inputContainer,
            hasError && styles.inputError,
            props.editable === false && styles.inputDisabled
          ]}
        >
          {leftIcon && <View style={styles.iconLeft}>{leftIcon}</View>}
          <TextInput
            ref={ref}
            style={[
              styles.input,
              leftIcon && styles.inputWithLeftIcon,
              rightIcon && styles.inputWithRightIcon,
              style
            ]}
            placeholderTextColor={Colors.placeholder}
            {...props}
          />
          {rightIcon && <View style={styles.iconRight}>{rightIcon}</View>}
        </View>
        {error && <Text style={styles.error}>{error}</Text>}
        {hint && !error && <Text style={styles.hint}>{hint}</Text>}
      </View>
    );
  }
);

Input.displayName = 'Input';

const styles = StyleSheet.create({
  container: {
    marginBottom: 16
  },
  label: {
    fontSize: 14,
    fontWeight: '500',
    color: Colors.text,
    marginBottom: 8
  },
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: Colors.background,
    borderRadius: 12,
    borderWidth: 1,
    borderColor: Colors.border
  },
  inputError: {
    borderColor: Colors.error
  },
  inputDisabled: {
    backgroundColor: Colors.disabled,
    opacity: 0.7
  },
  input: {
    flex: 1,
    paddingVertical: 14,
    paddingHorizontal: 16,
    fontSize: 16,
    color: Colors.text
  },
  inputWithLeftIcon: {
    paddingLeft: 8
  },
  inputWithRightIcon: {
    paddingRight: 8
  },
  iconLeft: {
    paddingLeft: 14
  },
  iconRight: {
    paddingRight: 14
  },
  error: {
    fontSize: 12,
    color: Colors.error,
    marginTop: 6,
    marginLeft: 4
  },
  hint: {
    fontSize: 12,
    color: Colors.textSecondary,
    marginTop: 6,
    marginLeft: 4
  }
});

接下来是分子组件的实现。分子组件由原子组件组合而成,代表了更完整的UI功能单元。以下是几个常用的分子组件。

// components/molecules/TaskCard/TaskCard.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Colors } from '@constants/theme';
import { Task, TaskPriority, TaskStatus } from '@/types';
import { PriorityBadge } from '@components/atoms/PriorityBadge';
import { StatusIndicator } from '@components/atoms/StatusIndicator';
import { format, isToday, isTomorrow, isPast, parseISO } from 'date-fns';
import { zhCN } from 'date-fns/locale';

interface TaskCardProps {
  task: Task;
  onPress: () => void;
  onLongPress?: () => void;
  showProject?: boolean;
  compact?: boolean;
}

export function TaskCard({
  task,
  onPress,
  onLongPress,
  showProject = false,
  compact = false
}: TaskCardProps) {
  const dueDate = task.dueDate ? parseISO(task.dueDate) : null;
  const isOverdue = dueDate && isPast(dueDate) && task.status !== 'completed';
  const isDueToday = dueDate && isToday(dueDate);
  const isDueTomorrow = dueDate && isTomorrow(dueDate);

  const formatDueDate = () => {
    if (!dueDate) return null;
    if (isDueToday) return '今天';
    if (isDueTomorrow) return '明天';
    return format(dueDate, 'M月d日', { locale: zhCN });
  };

  return (
    <TouchableOpacity
      style={[styles.container, compact && styles.containerCompact]}
      onPress={onPress}
      onLongPress={onLongPress}
      activeOpacity={0.7}
    >
      <View style={styles.header}>
        <StatusIndicator status={task.status} size="medium" />
        <View style={styles.titleContainer}>
          <Text
            style={[
              styles.title,
              task.status === 'completed' && styles.titleCompleted
            ]}
            numberOfLines={compact ? 1 : 2}
          >
            {task.title}
          </Text>
          {showProject && task.project && (
            <Text style={styles.projectName} numberOfLines={1}>
              {task.project.name}
            </Text>
          )}
        </View>
        <PriorityBadge priority={task.priority} />
      </View>

      {!compact && task.description && (
        <Text style={styles.description} numberOfLines={2}>
          {task.description}
        </Text>
      )}

      {dueDate && (
        <View style={styles.footer}>
          <Text
            style={[
              styles.dueDate,
              isOverdue && styles.dueDateOverdue,
              isDueToday && styles.dueDateToday
            ]}
          >
            {formatDueDate()}
          </Text>
          {task.subtasks && task.subtasks.length > 0 && (
            <Text style={styles.subtasks}>
              {task.subtasks.filter((st) => st.completed).length}/
              {task.subtasks.length} 子任务
            </Text>
          )}
        </View>
      )}

      {task.tags && task.tags.length > 0 && (
        <View style={styles.tags}>
          {task.tags.slice(0, 3).map((tag) => (
            <View
              key={tag.id}
              style={[styles.tag, { backgroundColor: tag.color + '20' }]}
            >
              <Text style={[styles.tagText, { color: tag.color }]}>
                {tag.name}
              </Text>
            </View>
          ))}
          {task.tags.length > 3 && (
            <Text style={styles.moreTags}>+{task.tags.length - 3}</Text>
          )}
        </View>
      )}
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: Colors.surface,
    borderRadius: 16,
    padding: 16,
    marginBottom: 12,
    shadowColor: Colors.black,
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.05,
    shadowRadius: 8,
    elevation: 2
  },
  containerCompact: {
    padding: 12
  },
  header: {
    flexDirection: 'row',
    alignItems: 'flex-start',
    gap: 12
  },
  titleContainer: {
    flex: 1
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    color: Colors.text,
    lineHeight: 22
  },
  titleCompleted: {
    textDecorationLine: 'line-through',
    color: Colors.textSecondary
  },
  projectName: {
    fontSize: 12,
    color: Colors.textSecondary,
    marginTop: 4
  },
  description: {
    fontSize: 14,
    color: Colors.textSecondary,
    marginTop: 8,
    lineHeight: 20
  },
  footer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginTop: 12,
    paddingTop: 12,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: Colors.border
  },
  dueDate: {
    fontSize: 13,
    color: Colors.textSecondary
  },
  dueDateOverdue: {
    color: Colors.error
  },
  dueDateToday: {
    color: Colors.warning
  },
  subtasks: {
    fontSize: 13,
    color: Colors.textSecondary
  },
  tags: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginTop: 12,
    gap: 8
  },
  tag: {
    paddingHorizontal: 10,
    paddingVertical: 4,
    borderRadius: 6
  },
  tagText: {
    fontSize: 12,
    fontWeight: '500'
  },
  moreTags: {
    fontSize: 12,
    color: Colors.textSecondary,
    paddingVertical: 4
  }
});

4.4 页面组件实现

页面组件是应用的具体视图层,负责将数据和UI组件组装成完整的页面。每个页面组件都应该保持相对轻薄,将复杂的业务逻辑委托给services层和hooks。以下是几个核心页面组件的实现。

// screens/TaskListScreen/TaskListScreen.tsx
import React, { useCallback, useMemo } from 'react';
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  RefreshControl,
  TouchableOpacity
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useTaskStore } from '@stores/taskStore';
import { useAuthStore } from '@/stores/authStore';
import { Colors } from '@constants/theme';
import { TaskCard } from '@components/molecules/TaskCard';
import { EmptyState } from '@components/molecules/EmptyState';
import { FilterBar } from '@components/molecules/FilterBar';
import { TaskStackParamList, RootStackParamList } from '@/navigation/types';
import { Task, TaskFilter } from '@/types';
import { useFilterTasks } from '@/hooks/useFilterTasks';

type NavigationProp = NativeStackNavigationProp<TaskStackParamList & RootStackParamList>;

export function TaskListScreen() {
  const navigation = useNavigation<NavigationProp>();
  const { tasks, isLoading, fetchTasks, selectedTask, selectTask } = useTaskStore();
  const { user } = useAuthStore();
  const [filter, setFilter] = React.useState<TaskFilter>({
    status: 'all',
    priority: 'all',
    projectId: null,
    tagIds: []
  });

  // 应用筛选逻辑
  const filteredTasks = useFilterTasks(tasks, filter);

  // 按状态分组
  const groupedTasks = useMemo(() => {
    const overdue: Task[] = [];
    const today: Task[] = [];
    const upcoming: Task[] = [];
    const completed: Task[] = [];

    filteredTasks.forEach((task) => {
      if (task.status === 'completed') {
        completed.push(task);
      } else if (task.dueDate) {
        const dueDate = new Date(task.dueDate);
        const now = new Date();
        const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);

        if (dueDate < now) {
          overdue.push(task);
        } else if (dueDate <= todayEnd) {
          today.push(task);
        } else {
          upcoming.push(task);
        }
      } else {
        upcoming.push(task);
      }
    });

    return { overdue, today, upcoming, completed };
  }, [filteredTasks]);

  const handleRefresh = useCallback(async () => {
    await fetchTasks();
  }, [fetchTasks]);

  const handleTaskPress = useCallback(
    (task: Task) => {
      selectTask(task);
      navigation.navigate('TaskDetail', { taskId: task.id });
    },
    [navigation, selectTask]
  );

  const handleCreateTask = useCallback(() => {
    navigation.navigate('CreateTask', {});
  }, [navigation]);

  const renderSection = (title: string, taskList: Task[], empty?: string) => {
    if (taskList.length === 0) return null;

    return (
      <View style={styles.section}>
        <View style={styles.sectionHeader}>
          <Text style={styles.sectionTitle}>{title}</Text>
          <Text style={styles.sectionCount}>{taskList.length}</Text>
        </View>
        {taskList.map((task) => (
          <TaskCard
            key={task.id}
            task={task}
            onPress={() => handleTaskPress(task)}
            showProject
          />
        ))}
      </View>
    );
  };

  return (
    <View style={styles.container}>
      <FilterBar filter={filter} onFilterChange={setFilter} />

      <FlatList
        data={[]}
        renderItem={() => null}
        ListHeaderComponent={
          <>
            {renderSection('已逾期', groupedTasks.overdue)}
            {renderSection('今日待办', groupedTasks.today)}
            {renderSection('即将到期', groupedTasks.upcoming)}
            {renderSection('已完成', groupedTasks.completed)}
          </>
        }
        ListEmptyComponent={
          <EmptyState
            icon="clipboard"
            title="暂无任务"
            description="点击下方按钮创建第一个任务"
            action={{
              label: '创建任务',
              onPress: handleCreateTask
            }}
          />
        }
        refreshControl={
          <RefreshControl
            refreshing={isLoading}
            onRefresh={handleRefresh}
            tintColor={Colors.primary}
          />
        }
        contentContainerStyle={styles.listContent}
        showsVerticalScrollIndicator={false}
      />

      <TouchableOpacity style={styles.fab} onPress={handleCreateTask}>
        <Text style={styles.fabIcon}>+</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.background
  },
  listContent: {
    paddingHorizontal: 16,
    paddingBottom: 100
  },
  section: {
    marginBottom: 24
  },
  sectionHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: Colors.text
  },
  sectionCount: {
    fontSize: 14,
    color: Colors.textSecondary,
    marginLeft: 8,
    backgroundColor: Colors.surface,
    paddingHorizontal: 8,
    paddingVertical: 2,
    borderRadius: 10
  },
  fab: {
    position: 'absolute',
    right: 20,
    bottom: 20,
    width: 56,
    height: 56,
    borderRadius: 28,
    backgroundColor: Colors.primary,
    alignItems: 'center',
    justifyContent: 'center',
    shadowColor: Colors.primary,
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 8
  },
  fabIcon: {
    fontSize: 28,
    color: Colors.white,
    fontWeight: '300'
  }
});
// screens/CreateTaskScreen/CreateTaskScreen.tsx
import React, { useState, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  KeyboardAvoidingView,
  Platform,
  Alert
} from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors } from '@constants/theme';
import { Input } from '@components/atoms/Input';
import { Button } from '@components/atoms/Button';
import { DatePicker } from '@components/molecules/DatePicker';
import { PrioritySelector } from '@components/molecules/PrioritySelector';
import { ProjectSelector } from '@/components/molecules/ProjectSelector';
import { TagSelector } from '@/components/molecules/TagSelector';
import { useTaskStore } from '@/stores/taskStore';
import { TaskStackParamList } from '@/navigation/types';
import { Task, CreateTaskDTO } from '@/types';

type RouteProps = RouteProp<TaskStackParamList, 'CreateTask'>;
type NavigationProp = NativeStackNavigationProp<TaskStackParamList>;

export function CreateTaskScreen() {
  const navigation = useNavigation<NavigationProp>();
  const route = useRoute<RouteProps>();
  const { createTask, isLoading } = useTaskStore();

  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [dueDate, setDueDate] = useState<Date | null>(null);
  const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
  const [projectId, setProjectId] = useState<string | null>(
    route.params?.projectId || null
  );
  const [selectedTags, setSelectedTags] = useState<string[]>([]);

  const [errors, setErrors] = useState<Record<string, string>>({});

  const validate = useCallback(() => {
    const newErrors: Record<string, string> = {};

    if (!title.trim()) {
      newErrors.title = '请输入任务标题';
    } else if (title.length > 200) {
      newErrors.title = '任务标题不能超过200个字符';
    }

    if (description.length > 2000) {
      newErrors.description = '任务描述不能超过2000个字符';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }, [title, description]);

  const handleSubmit = useCallback(async () => {
    if (!validate()) return;

    try {
      const taskData: CreateTaskDTO = {
        title: title.trim(),
        description: description.trim() || undefined,
        dueDate: dueDate?.toISOString(),
        priority,
        projectId,
        tagIds: selectedTags
      };

      await createTask(taskData);
      navigation.goBack();
    } catch (error) {
      Alert.alert('创建失败', '请稍后重试');
    }
  }, [title, description, dueDate, priority, projectId, selectedTags, validate, createTask, navigation]);

  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
    >
      <ScrollView
        style={styles.scrollView}
        contentContainerStyle={styles.scrollContent}
        keyboardShouldPersistTaps="handled"
        showsVerticalScrollIndicator={false}
      >
        <Input
          label="任务标题"
          placeholder="请输入任务标题"
          value={title}
          onChangeText={setTitle}
          error={errors.title}
          maxLength={200}
          autoFocus
        />

        <Input
          label="任务描述"
          placeholder="详细描述任务内容..."
          value={description}
          onChangeText={setDescription}
          error={errors.description}
          multiline
          numberOfLines={4}
          maxLength={2000}
          textAlignVertical="top"
          style={styles.descriptionInput}
        />

        <View style={styles.field}>
          <Text style={styles.label}>截止日期</Text>
          <DatePicker
            value={dueDate}
            onChange={setDueDate}
            placeholder="选择截止日期"
            minDate={new Date()}
          />
        </View>

        <PrioritySelector value={priority} onChange={setPriority} />

        <View style={styles.field}>
          <Text style={styles.label}>所属项目</Text>
          <ProjectSelector
            value={projectId}
            onChange={setProjectId}
            placeholder="选择项目(可选)"
          />
        </View>

        <View style={styles.field}>
          <Text style={styles.label}>标签</Text>
          <TagSelector
            value={selectedTags}
            onChange={setSelectedTags}
            placeholder="选择标签(可选)"
          />
        </View>
      </ScrollView>

      <View style={styles.footer}>
        <Button
          title="取消"
          variant="secondary"
          onPress={() => navigation.goBack()}
          style={styles.cancelButton}
        />
        <Button
          title="创建任务"
          onPress={handleSubmit}
          loading={isLoading}
          style={styles.submitButton}
        />
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.background
  },
  scrollView: {
    flex: 1
  },
  scrollContent: {
    padding: 16,
    paddingBottom: 100
  },
  field: {
    marginBottom: 16
  },
  label: {
    fontSize: 14,
    fontWeight: '500',
    color: Colors.text,
    marginBottom: 8
  },
  descriptionInput: {
    minHeight: 120
  },
  footer: {
    flexDirection: 'row',
    padding: 16,
    paddingBottom: Platform.OS === 'ios' ? 34 : 16,
    backgroundColor: Colors.surface,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: Colors.border,
    gap: 12
  },
  cancelButton: {
    flex: 1
  },
  submitButton: {
    flex: 2
  }
});

4.5 服务层实现

服务层封装了应用的业务逻辑,是连接数据层和表现层的桥梁。良好的服务层设计能够让业务逻辑得到复用,同时保持代码的清晰和可测试性。以下是核心服务类的实现。

// services/TaskService.ts
import { API_CLIENT } from '@/api/client';
import { Task, CreateTaskDTO, UpdateTaskDTO, TaskFilters } from '@/types';
import { handleApiError } from '@/utils/errorHandling';

class TaskService {
  private readonly baseUrl = '/api/v1/tasks';

  async getTasks(filters?: TaskFilters): Promise<Task[]> {
    try {
      const params = new URLSearchParams();

      if (filters?.projectId) {
        params.append('projectId', filters.projectId);
      }
      if (filters?.status && filters.status !== 'all') {
        params.append('status', filters.status);
      }
      if (filters?.priority && filters.priority !== 'all') {
        params.append('priority', filters.priority);
      }
      if (filters?.tagIds?.length) {
        params.append('tagIds', filters.tagIds.join(','));
      }
      if (filters?.assigneeId) {
        params.append('assigneeId', filters.assigneeId);
      }

      const queryString = params.toString();
      const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;

      const response = await API_CLIENT.get<Task[]>(url);
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async getTaskById(id: string): Promise<Task> {
    try {
      const response = await API_CLIENT.get<Task>(`${this.baseUrl}/${id}`);
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async createTask(data: CreateTaskDTO): Promise<Task> {
    try {
      const response = await API_CLIENT.post<Task>(this.baseUrl, data);
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async updateTask(id: string, data: UpdateTaskDTO): Promise<Task> {
    try {
      const response = await API_CLIENT.patch<Task>(
        `${this.baseUrl}/${id}`,
        data
      );
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async deleteTask(id: string): Promise<void> {
    try {
      await API_CLIENT.delete(`${this.baseUrl}/${id}`);
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async toggleTaskStatus(id: string): Promise<Task> {
    try {
      const response = await API_CLIENT.post<Task>(
        `${this.baseUrl}/${id}/toggle-status`
      );
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async addSubtask(
    taskId: string,
    subtask: { title: string }
  ): Promise<Task> {
    try {
      const response = await API_CLIENT.post<Task>(
        `${this.baseUrl}/${taskId}/subtasks`,
        subtask
      );
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async toggleSubtask(
    taskId: string,
    subtaskId: string
  ): Promise<Task> {
    try {
      const response = await API_CLIENT.post<Task>(
        `${this.baseUrl}/${taskId}/subtasks/${subtaskId}/toggle`
      );
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async deleteSubtask(taskId: string, subtaskId: string): Promise<Task> {
    try {
      const response = await API_CLIENT.delete<Task>(
        `${this.baseUrl}/${taskId}/subtasks/${subtaskId}`
      );
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async bulkUpdateStatus(
    taskIds: string[],
    status: 'pending' | 'in_progress' | 'completed'
  ): Promise<Task[]> {
    try {
      const response = await API_CLIENT.patch<Task[]>('/api/v1/tasks/bulk', {
        taskIds,
        status
      });
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async bulkDelete(taskIds: string[]): Promise<void> {
    try {
      await API_CLIENT.post('/api/v1/tasks/bulk-delete', { taskIds });
    } catch (error) {
      throw handleApiError(error);
    }
  }

  async getTaskStatistics(): Promise<TaskStatistics> {
    try {
      const response = await API_CLIENT.get<TaskStatistics>(
        `${this.baseUrl}/statistics`
      );
      return response.data;
    } catch (error) {
      throw handleApiError(error);
    }
  }
}

export interface TaskStatistics {
  total: number;
  pending: number;
  inProgress: number;
  completed: number;
  overdue: number;
  byPriority: {
    high: number;
    medium: number;
    low: number;
  };
  byProject: {
    projectId: string;
    projectName: string;
    count: number;
  }[];
}

export const taskService = new TaskService();

4.6 API客户端配置

API客户端是应用与后端服务器通信的入口点,良好的API客户端设计能够统一处理认证、错误处理、日志记录等横切关注点。以下是我们为项目配置的API客户端实现。

// api/client.ts
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError
} from 'axios';
import * as SecureStore from 'expo-secure-store';
import { API_BASE_URL, API_TIMEOUT } from '@constants/config';
import { AuthTokenService } from '@/services/AuthTokenService';
import { handleApiError, ApiError } from '@/utils/errorHandling';
import { logger } from '@/utils/logger';

class ApiClient {
  private client: AxiosInstance;
  private isRefreshing: boolean = false;
  private refreshSubscribers: ((token: string) => void)[] = [];

  constructor() {
    this.client = axios.create({
      baseURL: API_BASE_URL,
      timeout: API_TIMEOUT,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json'
      }
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // 请求拦截器:添加认证Token
    this.client.interceptors.request.use(
      async (config) => {
        const token = await AuthTokenService.getAccessToken();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }

        // 添加请求ID用于追踪
        config.headers['X-Request-ID'] = this.generateRequestId();

        // 开发环境添加日志
        if (__DEV__) {
          logger.debug(`[API Request] ${config.method?.toUpperCase()} ${config.url}`);
        }

        return config;
      },
      (error) => {
        logger.error('[API Request Error]', error);
        return Promise.reject(error);
      }
    );

    // 响应拦截器:处理错误和Token刷新
    this.client.interceptors.response.use(
      (response) => {
        if (__DEV__) {
          logger.debug(`[API Response] ${response.status} ${response.config.url}`);
        }
        return response;
      },
      async (error: AxiosError) => {
        const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };

        // 处理401错误:尝试刷新Token
        if (error.response?.status === 401 && !originalRequest._retry) {
          if (this.isRefreshing) {
            // 等待Token刷新完成
            return new Promise((resolve) => {
              this.refreshSubscribers.push((token: string) => {
                if (originalRequest.headers) {
                  originalRequest.headers.Authorization = `Bearer ${token}`;
                }
                resolve(this.client(originalRequest));
              });
            });
          }

          originalRequest._retry = true;
          this.isRefreshing = true;

          try {
            const newToken = await this.refreshToken();
            this.refreshSubscribers.forEach((callback) => callback(newToken));
            this.refreshSubscribers = [];

            if (originalRequest.headers) {
              originalRequest.headers.Authorization = `Bearer ${newToken}`;
            }
            return this.client(originalRequest);
          } catch (refreshError) {
            // Token刷新失败,清除登录状态
            await AuthTokenService.clearTokens();
            // 可以在这里触发全局的登出事件
            return Promise.reject(refreshError);
          } finally {
            this.isRefreshing = false;
          }
        }

        // 其他错误转换为统一的ApiError
        return Promise.reject(handleApiError(error));
      }
    );
  }

  private async refreshToken(): Promise<string> {
    const refreshToken = await AuthTokenService.getRefreshToken();
    if (!refreshToken) {
      throw new Error('No refresh token available');
    }

    const response = await axios.post(`${API_BASE_URL}/api/v1/auth/refresh`, {
      refreshToken
    });

    const { accessToken, refreshToken: newRefreshToken } = response.data;

    await AuthTokenService.setTokens(accessToken, newRefreshToken);

    return accessToken;
  }

  private generateRequestId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  async get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.client.get<T>(url, config);
  }

  async post<T>(
    url: string,
    data?: any,
    config?: AxiosRequestConfig
  ): Promise<AxiosResponse<T>> {
    return this.client.post<T>(url, data, config);
  }

  async put<T>(
    url: string,
    data?: any,
    config?: AxiosRequestConfig
  ): Promise<AxiosResponse<T>> {
    return this.client.put<T>(url, data, config);
  }

  async patch<T>(
    url: string,
    data?: any,
    config?: AxiosRequestConfig
  ): Promise<AxiosResponse<T>> {
    return this.client.patch<T>(url, data, config);
  }

  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.client.delete<T>(url, config);
  }
}

export const API_CLIENT = new ApiClient();

第五部分:性能优化与最佳实践

5.1 性能优化策略

移动应用的性能直接影响用户体验,良好的性能优化能够让应用运行更加流畅,减少电量消耗。以下是我们总结的Expo应用性能优化策略和实践方法。

组件渲染优化是React应用性能优化的基础。React的虚拟DOM虽然已经做了很多优化,但在复杂应用中,不必要的组件重新渲染仍然会造成性能问题。我们使用React.memo来包装纯展示组件,避免在父组件状态变化时重新渲染没有变化的子组件。对于需要比较props的组件,我们提供自定义的比较函数来精确控制何时需要重新渲染。

// 使用React.memo进行组件优化
const TaskCard = React.memo(
  ({ task, onPress }: TaskCardProps) => {
    return (
      <TouchableOpacity onPress={() => onPress(task.id)}>
        <Text>{task.title}</Text>
      </TouchableOpacity>
    );
  },
  (prevProps, nextProps) => {
    // 自定义比较函数:当这些属性都没变时才跳过渲染
    return (
      prevProps.task.id === nextProps.task.id &&
      prevProps.task.title === nextProps.task.title &&
      prevProps.task.status === nextProps.task.status &&
      prevProps.onPress === nextProps.onPress
    );
  }
);

列表渲染优化对于展示大量数据的应用尤为重要。FlatList是React Native中用于高效渲染列表的组件,它只会渲染当前屏幕可见的元素,大大减少了内存占用和渲染时间。我们需要正确配置keyExtractor、getItemLayout等属性来实现最佳性能。

// 高性能FlatList配置
<FlatList
  data={tasks}
  keyExtractor={(item) => item.id}
  renderItem={({ item, index }) => (
    <TaskCard
      task={item}
      onPress={handleTaskPress}
      index={index}
    />
  )}
  // 使用getItemLayout提供固定高度的列表项
  getItemLayout={(data, index) => ({
    length: TASK_CARD_HEIGHT,
    offset: TASK_CARD_HEIGHT * index,
    index
  })}
  // 预加载附近区域的内容
  windowSize={5}
  // 最大一次性渲染的条目数
  maxToRenderPerBatch={10}
  // 批量更新之间的间隔
  updateCellsBatchingPeriod={50}
  // 移除不可见的元素
  removeClippedSubviews={true}
  // 初始渲染数量
  initialNumToRender={10}
  // 下拉刷新
  refreshing={isLoading}
  onRefresh={onRefresh}
  // 上拉加载更多
  onEndReached={loadMore}
  onEndReachedThreshold={0.5}
/>

图片优化是移动应用性能的重要组成部分。我们使用expo-image组件来实现图片的自动优化,包括缓存、格式转换、尺寸调整等。expo-image支持多种图片格式和加载策略,能够显著提升图片加载速度和用户体验。

// 使用expo-image进行图片优化
import { Image } from 'expo-image';

<Image
  source={{ uri: task.thumbnailUrl }}
  style={styles.thumbnail}
  // 内容模式
  contentFit="cover"
  // 过渡动画
  transition={200}
  // 占位图
  placeholder={{ blurhash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj' }}
  // 缓存策略
  cachePolicy="memory-disk"
/>

5.2 离线支持实现

离线支持是现代移动应用的重要特性,它允许用户在网络不稳定或无网络的情况下正常使用应用。我们采用本地数据库加同步队列的策略来实现完整的离线支持。

首先,我们需要配置SQLite数据库来存储离线数据。expo-sqlite提供了便捷的SQLite操作接口,我们创建数据库schema和仓储类来管理本地数据。

// repositories/LocalTaskRepository.ts
import * as SQLite from 'expo-sqlite';
import { Task, LocalTask } from '@/types';
import { generateId } from '@/utils/idGenerator';

class LocalTaskRepository {
  private db: SQLite.SQLiteDatabase | null = null;

  async initialize() {
    this.db = await SQLite.openDatabaseAsync('taskmaster.db');
    await this.createTables();
  }

  private async createTables() {
    if (!this.db) throw new Error('Database not initialized');

    await this.db.execAsync(`
      CREATE TABLE IF NOT EXISTS tasks (
        id TEXT PRIMARY KEY,
        server_id TEXT,
        title TEXT NOT NULL,
        description TEXT,
        status TEXT DEFAULT 'pending',
        priority TEXT DEFAULT 'medium',
        project_id TEXT,
        due_date TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        is_synced INTEGER DEFAULT 0,
        is_deleted INTEGER DEFAULT 0
      );

      CREATE TABLE IF NOT EXISTS task_tags (
        task_id TEXT,
        tag_id TEXT,
        PRIMARY KEY (task_id, tag_id)
      );

      CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
      CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
      CREATE INDEX IF NOT EXISTS idx_tasks_sync ON tasks(is_synced);
    `);
  }

  async saveTask(task: LocalTask): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');

    await this.db.runAsync(
      `INSERT OR REPLACE INTO tasks
       (id, server_id, title, description, status, priority, project_id, due_date, created_at, updated_at, is_synced, is_deleted)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        task.id,
        task.serverId,
        task.title,
        task.description,
        task.status,
        task.priority,
        task.projectId,
        task.dueDate,
        task.createdAt,
        task.updatedAt,
        task.isSynced ? 1 : 0,
        task.isDeleted ? 1 : 0
      ]
    );
  }

  async getUnsyncedTasks(): Promise<LocalTask[]> {
    if (!this.db) throw new Error('Database not initialized');

    const rows = await this.db.getAllAsync<any>(
      'SELECT * FROM tasks WHERE is_synced = 0'
    );

    return rows.map(this.mapRowToTask);
  }

  async markAsSynced(localId: string, serverId: string): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');

    await this.db.runAsync(
      'UPDATE tasks SET server_id = ?, is_synced = 1 WHERE id = ?',
      [serverId, localId]
    );
  }

  async getTasks(projectId?: string): Promise<LocalTask[]> {
    if (!this.db) throw new Error('Database not initialized');

    let query = 'SELECT * FROM tasks WHERE is_deleted = 0';
    const params: any[] = [];

    if (projectId) {
      query += ' AND project_id = ?';
      params.push(projectId);
    }

    query += ' ORDER BY created_at DESC';

    const rows = await this.db.getAllAsync<any>(query, params);
    return rows.map(this.mapRowToTask);
  }

  async deleteTask(id: string): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');

    // 软删除
    await this.db.runAsync(
      'UPDATE tasks SET is_deleted = 1, is_synced = 0, updated_at = ? WHERE id = ?',
      [new Date().toISOString(), id]
    );
  }

  private mapRowToTask(row: any): LocalTask {
    return {
      id: row.id,
      serverId: row.server_id,
      title: row.title,
      description: row.description,
      status: row.status,
      priority: row.priority,
      projectId: row.project_id,
      dueDate: row.due_date,
      createdAt: row.created_at,
      updatedAt: row.updated_at,
      isSynced: row.is_synced === 1,
      isDeleted: row.is_deleted === 1
    };
  }
}

export const localTaskRepository = new LocalTaskRepository();

5.3 推送通知集成

推送通知是移动应用与用户保持连接的重要渠道,它能够帮助我们及时向用户传达重要信息,提升应用的活跃度和用户粘性。Expo提供了expo-notifications库来简化推送通知的集成。

// services/NotificationService.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import { Task, Reminder } from '@/types';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';

// 配置通知处理器
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
    shouldShowBanner: true,
    shouldShowList: true
  })
});

class NotificationService {
  private permissionGranted: boolean = false;

  async initialize(): Promise<boolean> {
    if (!Device.isDevice) {
      console.log('Push notifications require a physical device');
      return false;
    }

    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;

    if (existingStatus !== 'granted') {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }

    this.permissionGranted = finalStatus === 'granted';

    if (this.permissionGranted && Platform.OS === 'android') {
      // 为Android设置默认通道
      await Notifications.setNotificationChannelAsync('default', {
        name: 'default',
        importance: Notifications.AndroidImportance.MAX,
        vibrationPattern: [0, 250, 250, 250],
        lightColor: '#4A90D9'
      });

      // 为任务提醒设置单独通道
      await Notifications.setNotificationChannelAsync('task-reminders', {
        name: '任务提醒',
        importance: Notifications.AndroidImportance.HIGH,
        vibrationPattern: [0, 250, 250, 250],
        sound: 'default'
      });
    }

    // 添加通知点击监听器
    this.setupNotificationListeners();

    return this.permissionGranted;
  }

  private setupNotificationListeners() {
    // 前台通知接收
    Notifications.addNotificationReceivedListener((notification) => {
      console.log('Notification received:', notification);
    });

    // 通知点击处理
    Notifications.addNotificationResponseReceivedListener((response) => {
      const data = response.notification.request.content.data;
      // 根据数据导航到相应页面
      if (data.taskId) {
        // 可以通过事件总线或其他方式通知导航
        console.log('User wants to view task:', data.taskId);
      }
    });
  }

  async scheduleTaskReminder(task: Task, reminderTime: Date): Promise<string> {
    if (!this.permissionGranted) {
      console.log('Notification permission not granted');
      return '';
    }

    const identifier = await Notifications.scheduleNotificationAsync({
      content: {
        title: '任务提醒',
        body: task.title,
        data: {
          taskId: task.id,
          projectId: task.projectId,
          type: 'task-reminder'
        },
        sound: 'default'
      },
      trigger: {
        date: reminderTime,
        type: Notifications.SchedulableTriggerInputTypes.DATE
      }
    });

    return identifier;
  }

  async scheduleDailySummary(hour: number = 9, minute: number = 0): Promise<string> {
    if (!this.permissionGranted) {
      return '';
    }

    const now = new Date();
    const scheduledDate = new Date();
    scheduledDate.setHours(hour, minute, 0, 0);

    if (scheduledDate <= now) {
      scheduledDate.setDate(scheduledDate.getDate() + 1);
    }

    return Notifications.scheduleNotificationAsync({
      content: {
        title: '每日任务概览',
        body: '查看今天的待办任务',
        data: { type: 'daily-summary' },
        sound: 'default'
      },
      trigger: {
        type: Notifications.SchedulableTriggerInputTypes.DATE,
        date: scheduledDate
      }
    });
  }

  async cancelNotification(identifier: string): Promise<void> {
    await Notifications.cancelScheduledNotificationAsync(identifier);
  }

  async cancelAllNotifications(): Promise<void> {
    await Notifications.cancelAllScheduledNotificationsAsync();
  }

  async getBadgeCount(): Promise<number> {
    return await Notifications.getBadgeCountAsync();
  }

  async setBadgeCount(count: number): Promise<void> {
    await Notifications.setBadgeCountAsync(count);
  }
}

export const notificationService = new NotificationService();

总结与展望

通过本文的实战讲解,我们完整地了解了使用Expo开发移动应用的完整流程。从技术选型开始,我们分析了为什么选择Expo以及配套的技术栈;在业务分析阶段,我们以任务管理应用为例进行了需求梳理和功能模块划分;架构设计部分我们详细讲解了分层架构、目录结构、状态管理模式;代码实现部分我们提供了核心组件、服务层、API客户端的详细实现;最后我们还探讨了性能优化和离线支持的最佳实践。

Expo作为React Native的官方推荐开发工具链,已经发展成为一个成熟稳定的移动应用开发平台。它不仅简化了开发流程,降低了入门门槛,还提供了丰富的生态系统和工具支持。随着Expo的持续迭代和社区的活跃发展,相信它会在跨平台移动应用开发领域发挥越来越重要的作用。

在实际开发中,我们还需要持续关注以下几个方面:一是保持对Expo SDK更新的跟进,及时迁移到新版本以获得更好的性能和新的功能;二是建立完善的测试体系,包括单元测试、集成测试和E2E测试,确保应用质量;三是重视可访问性设计,让应用能够服务于更广泛的用户群体;四是持续优化应用性能,关注内存占用、启动时间、交互响应等关键指标。

希望本文能够帮助读者建立起Expo开发的完整知识体系,并在实际项目中应用这些最佳实践,开发出优秀的移动应用产品。