引言
在移动应用开发领域,跨平台开发框架一直是开发者关注的焦点。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 目录结构设计
合理的目录结构是架构落地的具体体现。我们采用基于功能的目录组织方式,将相关代码集中管理,便于查找和维护。以下是项目的目录结构和各部分的职责说明。
bash
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来完成数据操作。
typescript
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会自动处理缓存、更新和错误处理。
typescript
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 错误处理与边界情况
健壮的错误处理是应用可靠性的重要保障。在移动应用中,网络不稳定、服务器异常、用户操作失误等情况时有发生,我们需要优雅地处理这些情况,给用户良好的体验,同时收集有用的错误信息用于调试和监控。
我们建立了分层的错误处理机制。在基础设施层,我们定义了统一的错误类型和错误处理中间件。在业务层,每个服务方法都应该捕获和处理预期的错误,对于未预期的错误应该记录日志并抛出统一格式的错误。在表现层,我们通过错误边界组件和状态管理来处理错误状态,给用户友好的错误提示。
typescript
// 错误类型定义
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;
}
}
对于离线操作的处理,我们采用队列管理的策略。当设备离线时,用户的所有数据操作会进入一个待同步队列,当网络恢复后自动同步。这种设计让应用在离线环境下也能正常使用,同时保证了数据的最终一致性。
typescript
// 离线操作队列
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配置。
bash
# 创建新的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。
json
{
"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特性支持。
json
{
"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插件。这个插件会在编译时将路径别名转换为实际的相对路径。
javascript
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的强类型支持能够在编译时发现导航相关的错误,提升代码质量。
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 {}
}
}
接下来,我们创建导航容器的配置。导航容器是整个应用的导航状态管理者,它包装了整个应用,提供了统一的导航上下文。
typescript
// 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>
);
}
底部标签导航器是主应用的主要导航结构,它包含了应用的四个主要模块入口。我们为每个标签配置了图标和标题,使得导航更加直观。
typescript
// 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元素,它们不依赖其他组件,但必须有完整的样式和行为定义。以下是几个核心原子组件的实现。
typescript
// 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
}
});
typescript
// 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功能单元。以下是几个常用的分子组件。
typescript
// 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。以下是几个核心页面组件的实现。
typescript
// 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'
}
});
typescript
// 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 服务层实现
服务层封装了应用的业务逻辑,是连接数据层和表现层的桥梁。良好的服务层设计能够让业务逻辑得到复用,同时保持代码的清晰和可测试性。以下是核心服务类的实现。
typescript
// 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客户端实现。
typescript
// 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的组件,我们提供自定义的比较函数来精确控制何时需要重新渲染。
typescript
// 使用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等属性来实现最佳性能。
typescript
// 高性能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支持多种图片格式和加载策略,能够显著提升图片加载速度和用户体验。
typescript
// 使用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和仓储类来管理本地数据。
typescript
// 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库来简化推送通知的集成。
typescript
// 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开发的完整知识体系,并在实际项目中应用这些最佳实践,开发出优秀的移动应用产品。