在上一章中,我们介绍了整体架构设计。本章将深入探讨 Expo Router 在医疗应用中的路由架构设计,包括基于文件系统的路由管理、跨页面状态同步、权限控制等核心实践。
2.1 基于文件系统的路由设计
2.1.1 Expo Router 核心概念
Expo Router 采用了"文件系统即路由"的设计理念,这种设计让路由结构变得直观且易于维护。在我们的医疗应用中,路由结构直接反映了业务模块的层次关系。
// 项目路由结构
app/
├── (tabs)/ # Tab 导航容器
│ ├── index.tsx # 首页 - 工作台
│ └── profile.tsx # 个人中心
├── patient/ # 患者管理模块
│ ├── index.tsx # 患者搜索
│ ├── create.tsx # 新建患者
│ ├── detail/[id].tsx # 患者详情(动态路由)
│ ├── edit/[id].tsx # 编辑患者
│ └── follow-up-records/ # 回访记录子模块
├── appointment-module/ # 预约模块
│ ├── add-appointment/ # 新增预约
│ └── edit-appointment/ # 编辑预约
└── reservation/ # 预约管理
2.1.2 动态路由的实现机制
在医疗应用中,患者详情、编辑页面等都需要动态参数。Expo Router 通过 [id].tsx 的命名约定实现动态路由:
// app/patient/detail/[id].tsx
import { useLocalSearchParams, router } from 'expo-router';
export default function PatientDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
// 使用患者ID获取详情数据
const { data: patientDetail, isLoading } = usePatientDetailQuery(Number(id));
const handleEdit = () => {
// 导航到编辑页面,传递相同的ID参数
router.push({
pathname: '/patient/edit/[id]',
params: { id }
});
};
return (
// 患者详情UI组件
);
}
2.1.3 嵌套路由的层级管理
医疗应用的复杂业务逻辑要求我们设计多层级的路由结构。每个业务模块都有独立的 _layout.tsx 文件来管理其子路由:
// app/patient/_layout.tsx
import { Stack } from 'expo-router';
export default function PatientLayout() {
return (
<Stack
screenOptions={{
animation: 'slide_from_right', // iOS风格的滑动动画
gestureDirection: 'horizontal', // 水平手势
gestureEnabled: true, // 启用手势返回
}}
>
<Stack.Screen
name="index"
options={{
title: '患者搜索',
headerShown: false,
}}
/>
<Stack.Screen
name="detail/[id]"
options={{
title: '患者详情',
headerShown: false,
}}
/>
<Stack.Screen
name="edit/[id]"
options={{
title: '编辑患者',
headerShown: false,
}}
/>
{/* 更多子路由配置 */}
</Stack>
);
}
2.2 导航状态管理与用户体验
2.2.1 多层级导航设计
在医疗应用中,用户经常需要在不同页面间跳转,比如从患者列表到患者详情,再到编辑页面。我们通过统一的动画配置确保用户体验的一致性:
// app/_layout.tsx - 根布局配置
export default function RootLayout() {
return (
<Stack
screenOptions={{
animation: 'slide_from_right', // 统一的页面切换动画
gestureDirection: 'horizontal', // 水平手势方向
gestureEnabled: true, // 启用手势返回
animationTypeForReplace: 'push', // 替换动画类型
animationDuration: 100, // 动画持续时间
}}
>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="patient" options={{ headerShown: false }} />
<Stack.Screen name="appointment-module" options={{ headerShown: false }} />
</Stack>
);
}
2.2.2 患者选择流程的状态管理
医疗应用中最复杂的导航场景之一是患者选择流程。用户可能从预约创建页面跳转到患者搜索,选择患者后再返回原页面。我们通过 Zustand 实现了跨页面的状态管理:
// stores/patientSelectionStore.ts
export interface SelectedPatient {
id: string;
name: string;
gender: string;
age: string | number;
phone: string;
recordId?: string;
consultItem?: string;
source?: string;
}
export type SourcePageType = 'add-appointment' | 'edit-appointment' | 'patient-detail' | null;
interface PatientSelectionState {
// 选中的患者信息
selectedPatient: SelectedPatient | null;
// 来源页面标识
sourcePageType: SourcePageType;
// 保存的表单数据(用于新建预约页面)
savedFormData: Record<string, any> | null;
// 编辑预约的ID(用于编辑页面)
editReservationId: string | null;
// 是否正在处理患者选择(防止重复处理)
isProcessingPatientSelection: boolean;
// Actions
setSelectedPatient: (patient: SelectedPatient | null) => void;
setSourcePage: (pageType: SourcePageType) => void;
setSavedFormData: (formData: Record<string, any> | null) => void;
// 获取并清除患者信息(一次性使用)
consumeSelectedPatient: () => SelectedPatient | null;
// 设置患者信息并保存表单数据
setPatientWithFormData: (
patient: SelectedPatient,
formData?: Record<string, any>,
sourceType?: SourcePageType
) => void;
// 准备患者搜索(保存当前状态)
preparePatientSearch: (
formData: Record<string, any>,
sourceType: SourcePageType,
editId?: string
) => void;
}
2.2.3 页面间数据传递机制
在患者选择流程中,我们需要在多个页面间传递复杂的数据:
// 在预约创建页面准备患者搜索
const handleSelectPatient = () => {
const formData = getValues(); // 获取当前表单数据
// 保存当前状态到全局store
preparePatientSearch(formData, 'add-appointment');
// 跳转到患者搜索页面
router.push('/appointment-module/patient-search');
};
// 在患者搜索页面选择患者后返回
const handlePatientSelect = (patient: SelectedPatient) => {
// 将选中的患者和表单数据保存到store
setPatientWithFormData(patient, savedFormData, sourcePageType);
// 根据来源页面类型返回
if (sourcePageType === 'add-appointment') {
router.back(); // 返回预约创建页面
}
};
// 在预约创建页面恢复数据
useEffect(() => {
const selectedPatient = consumeSelectedPatient();
if (selectedPatient) {
// 恢复表单数据
if (savedFormData) {
reset(savedFormData);
}
// 设置选中的患者
setValue('patientId', selectedPatient.id);
setValue('patientName', selectedPatient.name);
}
}, []);
2.3 权限控制与路由守卫
2.3.1 应用启动流程
医疗应用需要严格的权限控制。我们在应用启动时实现了完整的用户状态验证机制:
// app/index.tsx - 应用入口
export default function Index() {
const { user, restoreUserFromStorage } = useUserStore();
const {
setSplashLoading,
setBooted,
setHasTriedAutoLogin,
setLastLoginTime,
setLastUserId
} = useAppStore();
const [isReady, setIsReady] = useState(false);
useEffect(() => {
// 启动时设置 splashLoading
setSplashLoading(true);
setHasTriedAutoLogin(true);
// 添加短暂延迟,确保 Root Layout 完全挂载
const timer = setTimeout(() => {
setIsReady(true);
}, 100);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!isReady) return;
const initializeApp = async () => {
try {
// 使用用户状态管理工具验证用户状态
const isUserStateValid = await ensureUserState();
const userSummary = getUserStateSummary();
if (isUserStateValid && userSummary.hasUser) {
// 用户状态有效,跳转到主页
setLastLoginTime(dayjs().valueOf());
setLastUserId(userSummary.userId!);
setSplashLoading(false);
setBooted(true);
router.replace('/(tabs)');
} else {
// 用户状态无效,跳转到登录页
setSplashLoading(false);
setBooted(true);
router.replace('/login');
}
} catch (error) {
setSplashLoading(false);
setBooted(true);
router.replace('/login');
}
};
initializeApp();
}, [isReady]);
return null;
}
2.3.2 用户状态验证机制
我们实现了完整的用户状态管理工具,确保应用的安全性:
// utils/auth/userStateManager.ts
export interface UserStateStatus {
hasToken: boolean;
hasUser: boolean;
isValid: boolean;
needsRestore: boolean;
}
/**
* 检查用户状态
*/
export async function checkUserState(): Promise<UserStateStatus> {
try {
const [accessToken, refreshToken, storedUser] = await Promise.all([
getAccessToken(),
getRefreshToken(),
getUser()
]);
const currentUser = useUserStore.getState().user;
const hasToken = !!(accessToken || refreshToken);
const hasUser = !!(currentUser && currentUser.id);
const hasStoredUser = !!(storedUser && storedUser.user && storedUser.user.id);
const isValid = hasToken && hasUser;
const needsRestore = hasToken && !hasUser && hasStoredUser;
return {
hasToken,
hasUser,
isValid,
needsRestore
};
} catch (error) {
return {
hasToken: false,
hasUser: false,
isValid: false,
needsRestore: false
};
}
}
/**
* 确保用户状态有效
* 如果无效则尝试恢复
*/
export async function ensureUserState(): Promise<boolean> {
try {
const status = await checkUserState();
if (status.isValid) {
return true;
}
if (status.needsRestore) {
return await restoreUserState();
}
return false;
} catch (error) {
return false;
}
}
2.3.3 页面访问控制
在登录页面,我们实现了完整的登录流程和权限验证:
// app/login.tsx
export default function LoginScreen() {
const [showErrorModal, setShowErrorModal] = useState(false);
// 表单提交处理
const onSubmit = async (data: LoginFormData) => {
try {
const res = await login(data.username, data.password);
clearError();
setShowErrorModal(false);
Toast.success('登录成功', 1);
// 登录成功后跳转到主页
router.replace('/(tabs)');
} catch (error: any) {
const errorMessage = error?.message || '账户或密码错误,建议企微登录!';
Toast.fail(errorMessage);
}
};
// 企业微信登录处理
const handleWeChatLogin = async () => {
try {
// 企业微信登录逻辑
const result = await handleWeWorkLoginResult();
if (result.success) {
Toast.success('登录成功', 1);
router.replace('/(tabs)');
}
} catch (error) {
console.error('企业微信登录失败:', error);
}
};
return (
// 登录页面UI
);
}
2.4 性能优化与用户体验
2.4.1 页面预加载策略
在医疗应用中,关键页面的快速响应至关重要。我们实现了智能的页面预加载机制:
// 在关键页面预加载相关数据
const PatientDetailScreen = () => {
const { id } = useLocalSearchParams<{ id: string }>();
// 预加载患者相关的其他数据
const { data: medicalRecords } = useMedicalRecordsQuery(Number(id), {
enabled: !!id,
staleTime: 5 * 60 * 1000, // 5分钟内数据保持新鲜
});
const { data: followUpRecords } = useFollowUpRecordsQuery(Number(id), {
enabled: !!id,
staleTime: 5 * 60 * 1000,
});
return (
// 患者详情UI
);
};
2.4.2 导航动画优化
我们通过原生动画API实现了流畅的页面切换效果:
// 统一的导航动画配置
const screenOptions = {
animation: 'slide_from_right', // 新页面从右侧滑入(iOS风格)
gestureDirection: 'horizontal', // 手势方向设为水平
gestureEnabled: true, // 启用手势返回
animationTypeForReplace: 'push', // 替换动画类型
animationDuration: 100, // 动画持续时间
};
// 在需要特殊动画的页面可以覆盖默认配置
<Stack.Screen
name="detail/[id]"
options={{
...screenOptions,
animation: 'fade', // 使用淡入淡出动画
}}
/>
2.4.3 手势返回优化
我们为所有页面启用了手势返回功能,提升用户体验:
// 在布局文件中统一配置手势返回
<Stack
screenOptions={{
gestureEnabled: true, // 启用手势返回
gestureDirection: 'horizontal', // 水平手势
gestureResponseDistance: 50, // 手势响应距离
}}
>
{/* 页面配置 */}
</Stack>
2.5 实际应用场景
2.5.1 患者管理流程
在患者管理模块中,我们实现了完整的CRUD操作流程:
// 患者列表 -> 患者详情 -> 编辑患者
const PatientListScreen = () => {
const handlePatientPress = (patientId: string) => {
router.push({
pathname: '/patient/detail/[id]',
params: { id: patientId }
});
};
return (
<FlatList
data={patients}
renderItem={({ item }) => (
<PatientCard
patient={item}
onPress={() => handlePatientPress(item.id)}
/>
)}
/>
);
};
// 患者详情页面
const PatientDetailScreen = () => {
const { id } = useLocalSearchParams<{ id: string }>();
const handleEdit = () => {
router.push({
pathname: '/patient/edit/[id]',
params: { id }
});
};
return (
<View>
{/* 患者详情内容 */}
<Button title="编辑患者" onPress={handleEdit} />
</View>
);
};
2.5.2 预约管理流程
预约管理涉及多个页面的复杂交互:
// 预约创建流程
const AddAppointmentScreen = () => {
const { preparePatientSearch } = usePatientSelectionStore();
const handleSelectPatient = () => {
const formData = getValues(); // 获取当前表单数据
// 保存表单数据并跳转到患者搜索
preparePatientSearch(formData, 'add-appointment');
router.push('/appointment-module/patient-search');
};
return (
<View>
<Button title="选择患者" onPress={handleSelectPatient} />
{/* 其他表单字段 */}
</View>
);
};
2.6 最佳实践总结
2.6.1 路由设计原则
- 文件系统即路由:保持路由结构与业务模块的一致性
- 动态路由命名:使用
[id].tsx约定实现动态参数 - 嵌套布局管理:每个业务模块使用独立的
_layout.tsx - 统一的动画配置:确保用户体验的一致性
2.6.2 状态管理策略
- 跨页面状态同步:使用 Zustand 管理复杂的页面间状态
- 一次性数据消费:通过
consumeSelectedPatient避免状态污染 - 状态持久化:关键状态需要持久化到本地存储
- 状态清理机制:及时清理不再需要的状态
2.6.3 性能优化要点
- 页面预加载:关键数据提前加载
- 动画优化:使用原生动画API提升性能
- 手势返回:提供直观的导航体验
- 内存管理:及时清理页面状态和缓存
2.7 总结
通过本章的深入探讨,我们了解了 Expo Router 在医疗应用中的完整实践:
- 文件系统路由:直观的路由结构设计
- 状态管理:复杂的跨页面状态同步机制
- 权限控制:完整的用户状态验证和路由守卫
- 性能优化:流畅的导航体验和动画效果
这些实践不仅提升了用户体验,也为后续的功能扩展奠定了坚实的基础。在下一章中,我们将深入探讨状态管理架构设计,包括 Zustand 和 React Query 的最佳实践。
下一章预告:第三章将详细介绍 Zustand 状态管理实践和 React Query 数据管理策略,包括全局状态与局部状态的设计原则、缓存策略与数据同步等核心内容。