إنشاء تطبيقات الأجهزة المحمولة للمؤسسات باستخدام Expo وReact Native
لقد نضج المعرض من أداة النماذج الأولية السريعة إلى منصة مؤسسية مشروعة. باستخدام Expo SDK 52 والبنية الجديدة (Fabric renderer + JSI)، يمكنك إنشاء تطبيقات الهاتف المحمول عبر الأنظمة الأساسية التي تتوافق مع أداء ومظهر التطبيقات الأصلية - أثناء مشاركة التعليمات البرمجية مع تطبيق الويب React الخاص بك والشحن إلى كل من iOS وAndroid من قاعدة بيانات TypeScript واحدة.
يغطي هذا الدليل الأنماط التي تفصل مشروع هواية عن تطبيق مؤسسة الإنتاج: تكوين EAS Build، والبنية الأساسية لإشعارات الدفع، ومزامنة البيانات دون اتصال أولاً، والمصادقة البيومترية، وسير عمل إرسال App Store.
الوجبات الرئيسية
- Expo Go مخصص للعروض التوضيحية؛ إن EAS Build مخصص للإنتاج — ابدأ باستخدام EAS من اليوم الأول
- البنية الجديدة (Fabric + JSI) قابلة للاشتراك ولكنها توفر مكاسب أداء قابلة للقياس
- يتطلب
expo-notificationsإعدادًا دقيقًا للتعامل مع المقدمة والخلفية- يحتاج الدعم دون اتصال إلى قاعدة بيانات محلية (WatermelonDB أو SQLite) مع منطق المزامنة
- المصادقة البيومترية باستخدام
expo-local-authenticationعبارة عن بضعة أسطر من التعليمات البرمجية- يتطلب الارتباط العميق التكوين في كل من app.json وعناوين URL لرد الاتصال الخاصة بموفر المصادقة الخاص بك
- تعمل الأسماء المستعارة لمسار TypeScript في Expo عبر
babel-plugin-module-resolver- لا تقم مطلقًا بتخزين البيانات الحساسة في
AsyncStorage— استخدمexpo-secure-storeللرموز المميزة
إعداد المشروع
ابدأ كل مشروع Expo باستخدام سير العمل Bare (غير المُدار) إذا كنت تتوقع أي متطلبات للوحدة الأصلية. يعد سير العمل المُدار ملائمًا ولكنه يقيدك — يتطلب التبديل لاحقًا الإخراج، وهو أمر مزعج.
# 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.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 (خدمات تطبيقات Expo) إنشاء تطبيقك وإرساله وتحديثه. قم بتكوينه باستخدام 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 - فهي غير مشفرة ويمكن الوصول إليها من قبل التطبيقات الأخرى على الأجهزة الجذر. استخدم 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:
// 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 تحديثات حزمة JavaScript دون مراجعة App Store، وهو أمر بالغ الأهمية لإصلاح الأخطاء:
// 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% من متطلبات المؤسسة - المصادقة البيومترية، والإشعارات الفورية، والربط العميق، والكاميرا، ونظام الملفات، والتخزين الآمن. استخدم React Native فقط إذا كنت بحاجة إلى وحدات أصلية مخصصة غير متوفرة في Expo، أو إذا كنت تتكامل مع التعليمات البرمجية الأصلية القديمة. تعد تجربة التطوير مع Expo أفضل بكثير، ويتعامل EAS Build مع تعقيد الإصدارات الأصلية.
كيف يمكنني مشاركة التعليمات البرمجية بين تطبيق الويب Next.js وتطبيق Expo للهاتف المحمول؟
استخدم Turborepo monorepo مع الحزم المشتركة لمنطق الأعمال والأنواع وعملاء واجهة برمجة التطبيقات. لا يدعم React Native جميع واجهات برمجة تطبيقات المتصفح/Node.js، لذا كن حذرًا بشأن ما يحدث في الحزم المشتركة. نمط يعمل بشكل جيد: أنواع المشاركة، ومخططات التحقق من الصحة (Zod)، ووظائف عميل واجهة برمجة التطبيقات (API)، مع إبقاء مكونات واجهة المستخدم منفصلة. يستخدم كل من App Router وExpo Router الخاصين بـ Next.js 16 التوجيه المستند إلى الملفات مع نماذج ذهنية مماثلة، مما يسهل الحفاظ على عمليات التنفيذ المتوازية.
كيف أتعامل مع الارتباط العميق لعمليات رد اتصال المصادقة؟
قم بتكوين مخطط URI الخاص بك في app.json ضمن scheme (على سبيل المثال، "scheme": "ecosire"). سجل ecosire://auth/callback كمعرف URI لإعادة التوجيه في موفر المصادقة الخاص بك. استخدم expo-linking للتعامل مع عناوين URL الواردة في تطبيقك. بالنسبة للروابط العامة (iOS) وروابط التطبيقات (Android) التي تعمل مثل عناوين URL الحقيقية، فإنك تحتاج إلى التحقق من النطاق الإضافي — ملفات apple-app-site-association وassetlinks.json المقدمة من نطاقك.
ما هي أفضل طريقة للتعامل مع إصدار التطبيق في EAS؟
قم بتعيين "appVersionSource": "remote" في eas.json وقم بتمكين "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 Build وتخزين الرمز المميز الآمن والبنية الأساسية لإشعارات الدفع والمزامنة دون اتصال إعدادًا دقيقًا من اليوم الأول.
قامت ECOSIRE ببناء تطبيقات الهاتف المحمول الإنتاجية باستخدام Expo مع 11 شاشة، والتمرير اللانهائي، وإشعارات الدفع، وتكوين نشر EAS. إذا كنت بحاجة إلى خبرة في تطوير الأجهزة المحمولة، استكشف خدمات التطوير لدينا.
بقلم
ECOSIRE Research and Development Team
بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.
مقالات ذات صلة
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.