EduFlow

الوحدة 4 · البيانات والجهاز

تخزين البيانات على الجهاز

21 دقراءة 4 داختر بحكمة

الدرس في لمحة

اختر بين AsyncStorage وexpo-sqlite وexpo-secure-store لكل نوع من البيانات في Trailhead، واستخدم كلًّا منها بشكل صحيح، من التفضيلات إلى قاعدة بيانات الرحلات مع الـ migrations إلى token تسجيل الدخول.

  1. 1AsyncStorage للتفضيلات
  2. 2expo-sqlite لسجلّ الرحلات
  3. 3SecureStore للأسرار
  4. 4مكان الملفات على القرص

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

تطبيق مشي ينسى سجلّك حين تفقد الإشارة، أو يطلب منك تسجيل الدخول كل صباح، مصيره الحذف. لدى Trailhead ثلاثة أنواع من البيانات يجب حفظها على الجهاز، ولكلٍّ منها أداة مختلفة:

البيانات مثال الأداة
تفضيلات صغيرة الوحدات (km أو mi)، آخر tab مستخدم AsyncStorage
سجلّات منظّمة تستعلم عنها سجلّ الرحلات، مرتّبًا ومصفّى expo-sqlite
أسرار الـ refresh token لتسجيل الدخول expo-secure-store

اختيار الأداة الخاطئة نادرًا ما يفشل بصوت عالٍ. بل يفشل في صورة شاشات بطيئة، أو بيانات ضائعة بعد تحديث، أو مراجعة أمنية لا تجتازها.

يخزّن Trailhead التفضيلات في AsyncStorage نصوصًا غير مشفّرة، والرحلات في قاعدة بيانات SQLite يستطيع الاستعلام عنها، والـ refresh token في SecureStore مشفّرًا عبر Keychain أو Keystore. والصور ملفات على القرص تُخزَّن مساراتها في SQLite. Trailhead AsyncStorage نصوص، غير مشفّرة units = "km" expo-sqlite جداول، استعلامات hikes, photo paths SecureStore Keychain / Keystore refresh token
ثلاثة مخازن، وثلاث وظائف

AsyncStorage للتفضيلات

AsyncStorage مخزن مفتاح-قيمة دائم لـ النصوص. كل استدعاء فيه غير متزامن، ولا شيء مشفّر:

src/lib/prefs.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

export type Prefs = { units: 'km' | 'mi'; remindersOn: boolean };
const KEY = 'trailhead.prefs.v1';
const defaults: Prefs = { units: 'km', remindersOn: false };

export async function loadPrefs(): Promise<Prefs> {
  const raw = await AsyncStorage.getItem(KEY);
  if (!raw) return defaults;
  try {
    return { ...defaults, ...JSON.parse(raw) };
  } catch {
    return defaults;
  }
}

export function savePrefs(prefs: Prefs) {
  return AsyncStorage.setItem(KEY, JSON.stringify(prefs));
}

خزّن قيمة JSON واحدة تحت مفتاح مرقّم بإصدار بدلًا من مفاتيح صغيرة كثيرة، وادمجها مع القيم الافتراضية عند القراءة (حتى يحصل تفضيل جديد أُضيف في الإصدار الثاني على قيمة معقولة لدى المستخدمين القدامى)، واصمد أمام البيانات التالفة باستخدام try. ثبّته بالأمر npx expo install @react-native-async-storage/async-storage. وإن كان تطبيقك يستخدم expo-sqlite أصلًا، فإن expo-sqlite/kv-store يقدّم الواجهة البرمجية نفسها مدعومة بـ SQLite، فتستغني عن الاعتمادية الإضافية.

اقرأ التفضيلات مرة واحدة عند بدء التشغيل، واحفظها في context مثل الرحلات، واكتبها مجددًا حين تتغيّر. استدعاء getItem داخل الـ components في كل render يحوّل شاشة سريعة إلى شاشة تومض بالقيم الافتراضية أولًا.

ويصبح AsyncStorage بطيئًا ومربكًا حين تبدأ بتخزين قوائم من السجلّات فيه: كل قراءة تحلّل الكتلة كلها، ولا طريقة لطلب «الرحلات التي تتجاوز 10 km في 2026» دون تحميل كل شيء.

expo-sqlite لسجلّ الرحلات

سجلّ الرحلات بيانات منظّمة ترتّبها وتصفّيها وتجمعها، وهذا ما وُجدت قاعدة البيانات من أجله. يعطيك expo-sqlite ملف قاعدة بيانات SQLite حقيقيًا على الجهاز. يفتحه SQLiteProvider ويشغّل الإعداد الخاص بك قبل أن يُرسم أي ابن:

src/app/_layout.tsx
import { Stack } from 'expo-router';
import { SQLiteProvider, type SQLiteDatabase } from 'expo-sqlite';

async function migrate(db: SQLiteDatabase) {
  const row = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
  let version = row?.user_version ?? 0;

  if (version === 0) {
    await db.execAsync(`
      PRAGMA journal_mode = WAL;
      CREATE TABLE hikes (
        id TEXT PRIMARY KEY NOT NULL,
        name TEXT NOT NULL,
        date TEXT NOT NULL,
        distanceKm REAL NOT NULL,
        favorite INTEGER NOT NULL DEFAULT 0
      );
    `);
    version = 1;
  }
  if (version === 1) {
    await db.execAsync('ALTER TABLE hikes ADD COLUMN photoUri TEXT');
    version = 2;
  }
  await db.execAsync(`PRAGMA user_version = ${version}`);
}

export default function RootLayout() {
  return (
    <SQLiteProvider databaseName="trailhead.db" onInit={migrate}>
      <Stack />
    </SQLiteProvider>
  );
}

PRAGMA user_version رقم يخزّنه SQLite في الملف نيابةً عنك. كل كتلة if ترقّي خطوة واحدة، فالمستخدم على الإصدار 0 يشغّل الاثنتين، والمستخدم على الإصدار 1 يشغّل الثانية فقط. لا تعدّل أبدًا خطوة قديمة بعد إطلاقها؛ بل أضف خطوة جديدة.

بعد ذلك تستطيع أي شاشة استخدام قاعدة البيانات:

TSX
import { useSQLiteContext } from 'expo-sqlite';

const db = useSQLiteContext(); // in a component or custom hook

// …later, inside an async function such as an effect or a save handler:
const hikes = await db.getAllAsync<Hike>('SELECT * FROM hikes ORDER BY date DESC');
await db.runAsync(
  'INSERT INTO hikes (id, name, date, distanceKm) VALUES (?, ?, ?, ?)',
  hike.id, hike.name, hike.date, hike.distanceKm,
);

عمليًا ستستدعي هذه من الـ provider الخاص بالـ reducer: حمّل الرحلات مرة واحدة إلى الـ state، واكتب إلى SQLite كلما غيّرها action.

SecureStore للأسرار

يتيح الـ refresh token لـ Trailhead تسجيل الدخول بصمت. ومن يقرؤه يستطيع التصرّف بوصفه المستخدم، لذا يوضع في expo-secure-store، الذي يشفّر القيم باستخدام Keychain في iOS وKeystore في Android:

src/lib/session.ts
import * as SecureStore from 'expo-secure-store';

const TOKEN_KEY = 'trailhead.refreshToken';

export const saveToken = (token: string) => SecureStore.setItemAsync(TOKEN_KEY, token);
export const readToken = () => SecureStore.getItemAsync(TOKEN_KEY);
export const clearToken = () => SecureStore.deleteItemAsync(TOKEN_KEY);

هذا ما كانت حالة isLoading في useSession من القسم السابق تنتظره: عند بدء التشغيل، يقرّر readToken() هل يصل المستخدم إلى الـ tabs أم إلى شاشة تسجيل الدخول. صُمّم SecureStore للقيم الصغيرة؛ وقد رفضت بعض إصدارات المنصّات قيمًا تتجاوز نحو 2 KB. احفظ فيه الـ tokens والمفاتيح، ولا تحفظ فيه كائنات كاملة أو ملفات أبدًا. وهو لا يعمل على الويب، فتحتاج نسخة الويب إلى نهج مختلف.

مكان الملفات على القرص

صورة الدرب عدّة ميغابايتات. لا تضعها في SQLite ولا في AsyncStorage. احتفظ بالملف على القرص (يدير expo-file-system مجلّد المستندات الخاص بتطبيقك)، وخزّن الـ URI الخاص به فقط في عمود photoUri. تبقى قاعدة بياناتك صغيرة وسريعة، ويحمّل component الصورة الملف مباشرة.

كل هذه البيانات تصمد أمام تحديثات التطبيق، بما فيها التحديثات عبر الهواء، وتُحذف حين يُزيل المستخدم التطبيق، مع استثناء غريب: على iOS قد تبقى قيم الـ Keychain المحفوظة عبر SecureStore بعد إعادة التثبيت، فلا تعتبر غياب قاعدة البيانات دليلًا على عدم وجود token. وإن كان فقدانها مؤلمًا، فزامنها مع خادمك، وهذا بالضبط سبب وجود الحسابات في Trailhead.

التالي: أدقّ جزء في التعامل مع الجهاز، أي طلب الإذن لاستخدام موقعه وكاميرته.

أهم النقاط

  • AsyncStorage مخزن مفتاح-قيمة للنصوص، غير مشفّر وغير متزامن، مناسب للتفضيلات الصغيرة وغير مناسب للأسرار.
  • يخزّن expo-sqlite البيانات المنظّمة التي تستعلم عنها وترتّبها وتصفّيها؛ شغّل الـ migrations الخاصة بالمخطّط (schema) داخل onInit مستخدمًا PRAGMA user_version.
  • مرّر قيم SQL دائمًا بوصفها معاملات (?)، ولا تبنِ نص SQL بنفسك أبدًا.
  • يشفّر expo-secure-store القيم الصغيرة باستخدام Keychain أو Keystore الخاص بالمنصّة؛ ومكان الـ tokens والمفاتيح هناك.
  • خزّن الملفات مثل الصور في نظام الملفات، واحتفظ بمساراتها فقط في قاعدة البيانات.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

أين يجب أن يحتفظ Trailhead بالـ refresh token الذي يتلقّاه بعد تسجيل الدخول؟

السؤال 2 من 3

أيّ سطر آمن لحفظ اسم رحلة كتبه المستخدم؟

السؤال 3 من 3

يضيف الإصدار الثاني من Trailhead عمود elevationM إلى جدول الرحلات. المستخدمون الحاليون لديهم جدول الإصدار الأول أصلًا. ما الطريقة الموثوقة؟

تدريب

اختر بحكمة

ضع كل قطعة بيانات في المخزن الصحيح

كتب زميلك مسوّدة خطّة التخزين في Trailhead. اختر كل سطر في الخطّة كنت ستوافق عليه في مراجعة الكود.

ضع كل قطعة بيانات في المخزن الصحيح