使用 Expo 和 React Native 构建企业移动应用程序
Expo 已经从一个快速原型设计工具发展成为一个合法的企业平台。借助 Expo SDK 52 和新架构(Fabric 渲染器 + JSI),您可以构建与本机应用程序的性能和感觉相匹配的跨平台移动应用程序,同时与您的 React Web 应用程序共享代码,并从单个 TypeScript 代码库传送到 iOS 和 Android。
本指南涵盖了将业余爱好项目与生产企业应用程序分开的模式:EAS 构建配置、推送通知基础架构、离线优先数据同步、生物识别身份验证和 App Store 提交工作流程。
要点
- Expo Go 用于演示; EAS Build 用于生产 — 从第一天开始使用 EAS
- 新架构(Fabric + JSI)是可选的,但可带来可衡量的性能提升
expo-notifications需要仔细设置前台和后台处理- 离线支持需要具有同步逻辑的本地数据库(WatermelonDB 或 SQLite)
- 使用
expo-local-authentication进行生物识别认证只需几行代码- 深层链接需要在 app.json 和您的身份验证提供商的回调 URL 中进行配置
- TypeScript 路径别名通过
babel-plugin-module-resolver在 Expo 中工作- 切勿将敏感数据存储在
AsyncStorage中 — 使用expo-secure-store作为令牌
项目设置
如果您预计有任何本机模块需求,请使用 Bare 工作流程(非托管)启动每个 Expo 项目。托管工作流程很方便,但限制了您——稍后切换需要弹出,这会造成破坏。
# Create new Expo project
npx create-expo-app@latest MyApp --template
# Or with TypeScript template
npx create-expo-app@latest MyApp -t expo-template-blank-typescript
# Install core enterprise dependencies
npx expo install \
expo-router \
expo-secure-store \
expo-local-authentication \
expo-notifications \
expo-updates \
expo-constants \
@react-navigation/native \
@react-navigation/bottom-tabs \
@tanstack/react-query
expo-router 基于文件的路由系统是现代方法 - 它反映了 Next.js 的 App Router 模式,使 Web 和移动设备之间的上下文切换变得更加容易。
app.json 配置
企业应用程序从一开始就需要仔细的 app.json 配置:
{
"expo": {
"name": "ECOSIRE",
"slug": "ecosire-mobile",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#0a0a0a"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.ecosire.mobile",
"buildNumber": "1",
"infoPlist": {
"NSFaceIDUsageDescription": "Use Face ID to securely sign in",
"NSCameraUsageDescription": "Take photos for your profile"
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0a0a0a"
},
"package": "com.ecosire.mobile",
"versionCode": 1,
"permissions": [
"USE_BIOMETRIC",
"USE_FINGERPRINT",
"RECEIVE_BOOT_COMPLETED",
"VIBRATE"
]
},
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png",
"color": "#f59e0b",
"sounds": ["./assets/notification.wav"]
}
],
"expo-router",
"expo-secure-store",
"expo-local-authentication"
],
"updates": {
"url": "https://u.expo.dev/YOUR-PROJECT-ID"
},
"runtimeVersion": {
"policy": "sdkVersion"
},
"extra": {
"eas": {
"projectId": "YOUR-PROJECT-ID"
}
}
}
}
EAS 构建配置
EAS(世博应用服务)负责构建、提交和更新您的应用程序。使用 eas.json 配置它:
{
"cli": {
"version": ">= 10.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
},
"android": {
"buildType": "apk"
},
"env": {
"API_URL": "http://localhost:3001",
"NODE_ENV": "development"
}
},
"preview": {
"distribution": "internal",
"channel": "preview",
"env": {
"API_URL": "https://staging-api.ecosire.com",
"NODE_ENV": "staging"
}
},
"production": {
"channel": "production",
"autoIncrement": true,
"env": {
"API_URL": "https://api.ecosire.com",
"NODE_ENV": "production"
},
"ios": {
"credentialsSource": "remote"
},
"android": {
"credentialsSource": "remote"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "[email protected]",
"ascAppId": "YOUR-APP-STORE-CONNECT-APP-ID",
"appleTeamId": "YOUR-TEAM-ID"
},
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "internal"
}
}
}
}
# Build for different environments
eas build --profile development --platform ios
eas build --profile preview --platform all
eas build --profile production --platform all
# Submit to stores
eas submit --profile production --platform ios
eas submit --profile production --platform android
安全认证
企业应用程序绝不能将身份验证令牌存储在 AsyncStorage 中 - 它是未加密的,并且可供 root 设备上的其他应用程序访问。使用 expo-secure-store ,它使用 Keychain (iOS) 和 Keystore (Android):
// lib/auth/token-storage.ts
import * as SecureStore from 'expo-secure-store';
const ACCESS_TOKEN_KEY = 'ecosire_access_token';
const REFRESH_TOKEN_KEY = 'ecosire_refresh_token';
export const tokenStorage = {
async saveTokens(accessToken: string, refreshToken: string) {
await Promise.all([
SecureStore.setItemAsync(ACCESS_TOKEN_KEY, accessToken, {
keychainService: 'com.ecosire.mobile.auth',
keychainAccessible: SecureStore.WHEN_UNLOCKED,
}),
SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refreshToken, {
keychainService: 'com.ecosire.mobile.auth',
keychainAccessible: SecureStore.WHEN_UNLOCKED,
}),
]);
},
async getAccessToken(): Promise<string | null> {
return SecureStore.getItemAsync(ACCESS_TOKEN_KEY, {
keychainService: 'com.ecosire.mobile.auth',
});
},
async getRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync(REFRESH_TOKEN_KEY, {
keychainService: 'com.ecosire.mobile.auth',
});
},
async clearTokens() {
await Promise.all([
SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY),
SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY),
]);
},
};
生物特征认证
添加Face ID / Touch ID /指纹认证:
// lib/auth/biometric.ts
import * as LocalAuthentication from 'expo-local-authentication';
export async function isBiometricAvailable(): Promise<boolean> {
const compatible = await LocalAuthentication.hasHardwareAsync();
if (!compatible) return false;
const enrolled = await LocalAuthentication.isEnrolledAsync();
return enrolled;
}
export async function authenticateWithBiometrics(
reason = 'Confirm your identity'
): Promise<boolean> {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: reason,
fallbackLabel: 'Use passcode',
disableDeviceFallback: false,
cancelLabel: 'Cancel',
});
return result.success;
}
// Usage in a component
async function handleSensitiveAction() {
const available = await isBiometricAvailable();
if (available) {
const authenticated = await authenticateWithBiometrics(
'Confirm this transaction'
);
if (!authenticated) return;
}
// Proceed with the action
await processSensitiveOperation();
}
推送通知
推送通知需要后端基础设施和仔细的权限处理:
// lib/notifications/setup.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
// Configure how notifications appear when app is in foreground
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications(): Promise<string | null> {
// Push notifications don't work on simulators
if (!Device.isDevice) {
console.warn('Push notifications require a physical device');
return null;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return null; // User denied permissions
}
// Android requires a notification channel
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#f59e0b',
});
}
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = await Notifications.getExpoPushTokenAsync({ projectId });
return token.data;
}
// hooks/use-push-notifications.ts
import { useEffect, useRef } from 'react';
import { AppState } from 'react-native';
import * as Notifications from 'expo-notifications';
import { registerForPushNotifications } from '@/lib/notifications/setup';
import { updatePushToken } from '@/lib/api/notifications';
export function usePushNotifications() {
const notificationListener = useRef<Notifications.Subscription>();
const responseListener = useRef<Notifications.Subscription>();
useEffect(() => {
// Register and save token to backend
registerForPushNotifications().then((token) => {
if (token) {
updatePushToken(token).catch(console.error);
}
});
// Handle notifications received while app is open
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
console.log('Notification received:', notification);
});
// Handle notification tap
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data;
// Navigate based on notification data
handleNotificationNavigation(data);
});
return () => {
notificationListener.current?.remove();
responseListener.current?.remove();
};
}, []);
}
WatermelonDB 的离线支持
企业应用程序需要离线工作。 WatermelonDB 是 React Native 性能最高的本地数据库:
npx expo install @nozbe/watermelondb @nozbe/with-observables
// db/schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export const schema = appSchema({
version: 1,
tables: [
tableSchema({
name: 'contacts',
columns: [
{ name: 'server_id', type: 'string', isOptional: true },
{ name: 'name', type: 'string' },
{ name: 'email', type: 'string', isOptional: true },
{ name: 'organization_id', type: 'string' },
{ name: 'synced_at', type: 'number', isOptional: true },
{ name: 'is_deleted', type: 'boolean' },
{ name: 'created_at', type: 'number' },
{ name: 'updated_at', type: 'number' },
],
}),
],
});
// lib/sync/contacts-sync.ts
import { synchronize } from '@nozbe/watermelondb/sync';
import { database } from '@/db';
export async function syncContacts(authToken: string) {
await synchronize({
database,
pullChanges: async ({ lastPulledAt }) => {
const response = await fetch(
`${API_URL}/sync/contacts?since=${lastPulledAt}`,
{
headers: { Authorization: `Bearer ${authToken}` },
}
);
const { changes, timestamp } = await response.json();
return { changes, timestamp };
},
pushChanges: async ({ changes }) => {
await fetch(`${API_URL}/sync/contacts`, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ changes }),
});
},
migrationsEnabledAtVersion: 1,
});
}
TanStack API 数据查询
TanStack Query 在 React Native 中的工作方式与在 React Web 应用程序中的工作方式完全相同:
// lib/api/contacts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tokenStorage } from '@/lib/auth/token-storage';
async function apiRequest<T>(path: string, options?: RequestInit): Promise<T> {
const token = await tokenStorage.getAccessToken();
const response = await fetch(`${process.env.API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
export function useContacts() {
return useQuery({
queryKey: ['contacts'],
queryFn: () => apiRequest<Contact[]>('/contacts'),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useCreateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateContactDto) =>
apiRequest<Contact>('/contacts', {
method: 'POST',
body: JSON.stringify(data),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contacts'] });
},
});
}
通过展会更新进行无线更新
EAS 更新无需 App Store 审核即可提供 JavaScript 捆绑包更新 — 对于错误修复至关重要:
// hooks/use-app-updates.ts
import { useEffect } from 'react';
import * as Updates from 'expo-updates';
import { Alert } from 'react-native';
export function useAppUpdates() {
useEffect(() => {
async function checkForUpdates() {
if (__DEV__) return; // Skip in development
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
Alert.alert(
'Update Available',
'A new version of the app is ready. Restart to apply.',
[
{ text: 'Later', style: 'cancel' },
{
text: 'Restart Now',
onPress: () => Updates.reloadAsync(),
},
]
);
}
} catch (error) {
// Update check failed — app continues running current version
console.warn('Update check failed:', error);
}
}
checkForUpdates();
}, []);
}
常见问题
什么时候我应该选择 Expo 而不是裸露的 React Native?
绝大多数企业应用程序都选择 Expo。 EAS生态系统涵盖了95%的企业需求——生物识别身份验证、推送通知、深度链接、摄像头、文件系统、安全存储。仅当您需要 Expo 中不可用的自定义本机模块,或者您要与旧本机代码集成时,才使用裸 React Native。 Expo 的开发体验明显更好,EAS Build 可以处理本机构建的复杂性。
如何在 Next.js Web 应用程序和 Expo 移动应用程序之间共享代码?
将 Turborepo monorepo 与业务逻辑、类型和 API 客户端的共享包结合使用。 React Native 并不支持所有浏览器/Node.js API,因此请注意共享包中的内容。一种运行良好的模式:共享类型、验证架构 (Zod) 和 API 客户端函数,但保持 UI 组件独立。 Next.js 16 的 App Router 和 Expo Router 都使用具有相似心智模型的基于文件的路由,从而更容易维护并行实现。
如何处理身份验证回调的深层链接?
在 scheme 下的 app.json 中配置您的 URI 方案(例如 "scheme": "ecosire")。在您的身份验证提供程序中将 ecosire://auth/callback 注册为重定向 URI。使用 expo-linking 处理应用程序中的传入 URL。对于像真实 URL 一样工作的通用链接 (iOS) 和应用程序链接 (Android),您需要额外的域验证 - 从您的域提供的 apple-app-site-association 和 assetlinks.json 文件。
在 EAS 中处理应用程序版本控制的最佳方法是什么?
在 eas.json 中设置 "appVersionSource": "remote" 并为生产版本启用 "autoIncrement": true。这使得 EAS 可以自动管理内部版本号,从而防止忘记手动递增的常见错误。将您的人类可读版本 (1.2.3) 保留在 app.json 中,并让 EAS 处理内部版本号(iOS buildNumber、Android versionCode)。
如何在开发中测试推送通知?
将 Expo 的推送通知测试工具(位于 https://expo.dev/notifications)与您设备的 Expo 推送令牌结合使用。对于本地开发,expo-notifications 提供了 scheduleNotificationAsync 函数,无需后端即可触发本地通知。对于端到端测试,请使用 eas build --profile development 构建开发客户端并使用物理设备。
后续步骤
构建可靠、安全且可大规模维护的企业移动应用程序需要打好基础 — EAS 构建配置、安全令牌存储、推送通知基础设施和离线同步都需要从第一天起就进行仔细设置。
ECOSIRE 使用 Expo 构建了具有 11 个屏幕、无限滚动、推送通知和 EAS 部署配置的生产移动应用程序。如果您需要移动开发专业知识,请探索我们的开发服务。
作者
ECOSIRE Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。
相关文章
Data Mesh Architecture: Decentralized Data for Enterprise
A comprehensive guide to data mesh architecture—principles, implementation patterns, organizational requirements, and how it enables scalable, domain-driven data ownership.
ECOSIRE vs Big 4 Consultancies: Enterprise Quality, Startup Speed
How ECOSIRE delivers enterprise-grade ERP and digital transformation outcomes without Big 4 pricing, overhead, or timeline bloat. A direct comparison.
Generative AI in Enterprise Applications: Beyond Chatbots
Discover how generative AI is transforming enterprise applications beyond chatbots—from code generation to synthetic data, document intelligence, and process automation.