React Native 奇幻之旅(19)-資料的存儲和快取

React Native logo

這篇文章主要會分享在 RN 做持久化存儲的常見作法。

什麼是資料持久化?

資料持久化是指將資料保存在非易失性儲存媒體中以便長期保留資料,意即關閉應用後重啟資料仍然存在,並可以隨時再次使用。

資料持久化可以通過不同的方式實現,包括但不限於:

  • 資料庫:將資料存儲在關聯式資料庫中,以便以結構化的方式組織和檢索資料。
  • 檔案系統:將資料以檔案的形式存儲在設備的檔案系統中。
  • 鍵值存儲:使用鍵值對(key-value pairs)的方式將資料存儲在設備中(Android: SharedPreferences, iOS: NSUserDefaults)。
  • 雲端存儲:將資料存儲在雲端伺服器上,以實現跨設備同步和備份。常見的雲存儲服務包括但不限於:AWS S3、Google Cloud Storage、Firebase
  • 快取:將資料存儲成 cache

接下來的內容除了雲端存儲外都會提到。

資料持久化需要考慮什麼?

我覺得 StackOverflow 上的這個問題已經把要考慮的事項列的很全面了,大概重點如下:

在 React Native 中做資料持久化有哪些選擇?我看到有本地存儲(local storage)和非同步存儲(async storage),但我也看到了諸如 Realm 之類的工具,我很困惑這些如何與外部資料庫一起使用。

  • 有哪些工具可以存儲資料在本地?
  • 資料什麼時候會被清除?例如:關閉應用程序時、重啟手機時…等。
  • 在 iOS 和 Android 中實現之間是否存在差異?
  • 如何處理離線時訪問資料?

讚數最多的回覆已經滿詳細的回答了,下面的內容我也會盡量回答這幾點,並且分享每種方法的基本使用方式。

AsyncStorage

https://react-native-async-storage.github.io/async-storage/docs/install

  • 官方支持的本地存儲庫,好上手、API 完善、輕量。
  • 非同步存取資料(key-value)。
  • 適合存儲小型資料,例如:配置信息、設置選項…等。
  • 資料存儲的限制為 6MB (但可以設置更高,後面會提到)。
  • 資料的存儲並無加密,所以不建議用於存儲隱私資料。
  • 資料存儲在裝置本地,卸載應用手動清除時刪除。
  • 不具有內置的自動備份和恢復功能,需要自己考慮如何備份資料。
PvvzBtI

存儲資料

主要是用於存儲字串,如果要存儲物件的話需要用 JSON.stringify 轉換為字串:

import AsyncStorage from '@react-native-async-storage/async-storage'

<em>// 非物件型別的資料</em>
const storeData = async (value) => {
  try {
    await AsyncStorage.setItem('my-key', value)
  } catch (e) {
    <em>// saving error</em>
  }
}

<em>// 物件型別的資料</em>
const storeData = async (value) => {
  try {
    const jsonValue = JSON.stringify(value)
    await AsyncStorage.setItem('my-key', jsonValue)
  } catch (e) {
    <em>// saving error</em>
  }
}

讀取資料

import AsyncStorage from '@react-native-async-storage/async-storage'

const getData = async () => {
  try {
    const jsonValue = await AsyncStorage.getItem('my-key');
    return jsonValue != null ? JSON.parse(jsonValue) : null
  } catch (e) {
    <em>// error reading value</em>
  }
}

清除資料

import AsyncStorage from '@react-native-async-storage/async-storage'

const removeValue = async () => {
  try {
    await AsyncStorage.removeItem('my-key')
  } catch(e) {
    <em>// remove error</em>
  }
}

突破存儲上限

官方有提到合理的存儲上限為 6MB,如果有需要提高可以在 android/gradle.properties 新增 AsyncStorage_db_size_in_MB 並指定上限的大小(MB):

AsyncStorage_db_size_in_MB=10

不過要注意的是,如果有開啟 Next Stroage 功能(AsyncStorage_useNextStorage=true)的話設置上限會不起作用。

https://react-native-async-storage.github.io/async-storage/docs/advanced/db_size

SQLite

https://www.npmjs.com/package/react-native-sqlite-storage

  • 適合存儲大量結構化的資料。
  • 使用 SQL 語法 來 CRUD。
  • 資料在卸載應用手動清除時刪除。
  • 不具有內置的自動備份和恢復功能,需要自己考慮如何備份資料。
  • 效能和穩定性相對較差。

可以理解為是一個本地的資料庫,操作方式就是使用 SQL 語法。

(如果是使用 expo 開發可以使用 expo-sqlite)

建立資料庫連線

const db = SQLite.openDatabase(
  {
    name: 'myDatabase.db',
    location: 'default',
  },
  () => {
    <em>// 資料庫連接成功</em>
  },
  error => {
    console.error('資料庫連接時出錯', error)
  }
)

建立資料表

db.transaction(tx => {
  tx.executeSql(
    'CREATE TABLE IF NOT EXISTS MyTable (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)'
  );
})

操作資料

<em>// 插入數據</em>
db.transaction(tx => {
  tx.executeSql('INSERT INTO MyTable (name) VALUES (?)', ['John'], (_, result) => {
    console.log('插入成功,行ID:', result.insertId)
  })
})

<em>// 查詢數據</em>
db.transaction(tx => {
  tx.executeSql('SELECT * FROM MyTable', [], (_, { rows }) => {
    const data = rows.raw()
    console.log('查詢結果:', data)
  })
})

react-native-keychain

如果要存儲如 token 這種敏感的資料,可以使用 react-native-keychain

  • 使用系統提供的安全存儲機制,如 iOS 的 Keychain 和 Android 的 Keystore,以確保資料的安全性。
  • 資料在卸載應用手動清除時刪除。

存儲資料

import * as Keychain from 'react-native-keychain'

const login = async () => {
    const token = 'xxxxxxx'
    const username = "Demo"
    await Keychain.setGenericPassword(username, token)
    <em>// ...</em>
}

讀取資料

import * as Keychain from 'react-native-keychain'

const getCredentials = async () => {
    try {
        const credentials = await Keychain.getGenericPassword()
        if (credentials) {
            console.log(credentials)
        } else {
            console.log("No credentials stored")
        }
    } catch (error) {
        console.log("Keychain couldn't be accessed!", error)
    }
}

清除資料

import * as Keychain from 'react-native-keychain'

const logout = async () => {
    const logout = await Keychain.resetGenericPassword()

    if (!!logout) {
        <em>// ...</em>
    }
}

Expo SecureStore

如果是使用 expo 開發,可以使用 SecureStore,可以在設備本地加密和安全存儲資料。

  • 值的大小限制為 2KB (貌似是沒有辦法突破限制)
  • Android 5 或更低版本的設備不兼容。
  • 資料在卸載應用手動清除時刪除。
  • 不具有內置的自動備份和恢復功能,需要自己考慮如何備份資料。

存儲資料

import * as SecureStore from 'expo-secure-store'

const saveValue = async (key, value) => {
  await SecureStore.setItemAsync(key, value)
}

讀取資料

import * as SecureStore from 'expo-secure-store'

const getValue = async (key) => {
  let result = await SecureStore.getItemAsync(key)
  if (result) {
    alert("🔐 Here's your value 🔐 \n" + result)
  } else {
    alert('No values stored under that key.')
  }
}

清除資料

import * as SecureStore from 'expo-secure-store'

const deleteValue = async (key) => {
    await SecureStore.deleteItemAsync(key)
}

Realm

  • 近些年較熱門的選擇,移動端的資料庫,支援移動式設備跨平台存儲。
  • 以物件導向為基礎,可定義Model和Schema能更好的管理資料。
  • 適合存儲比較龐大或者結構複雜的資料。
  • 訂閱式,查詢過的資料一旦改變會自動更新,也有事件可以監聽。
  • 移動端優化,性能優於 SQLite, AsyncStorage。
  • 存儲的資料可以雲端同步。

定義 Schema

import Realm from 'realm'

<em>// 定義 Schema 用來描述資料結構</em>
const UserSchema = {
  name: 'User',
  properties: {
    id: 'int',
    username: 'string',
    email: 'string',
    createdAt: 'date'
  }
}

初始化資料庫

import Realm from 'realm'

<em>// 初始化 Realm 資料庫</em>
const realm = new Realm({ schema: [UserSchema] })

資料的CRUD

<em>// 新增 user </em>
const createUsers = (users) => {
  realm.write(() => {
    const createdAt = new Date()
    const data = users.map(user => ({
      id: user.id,
      username: user.username,
      email: user.email,
      createdAt
    }))
    realm.create('User', data)
  })
}

<em>// 獲取所有 user</em>
const getUsers = () => {
  return realm.objects('User')
}

<em>// 更新 user 資料</em>
const updateUser = (userId, newData) => {
  realm.write(() => {
    const user = realm.objectForPrimaryKey('User', userId)
    if (user) {
      user.username = newData.username
      user.email = newData.email
    }
  })
}

<em>// 刪除 user</em>
const deleteUser = (userId) => {
  realm.write(() => {
    const user = realm.objectForPrimaryKey('User', userId)
    if (user) {
      realm.delete(user)
    }
  })
}

使用範例

const users = [
    {
      id: 1,
      username: 'tom',
      email: 'tom@gmail.com'
    },
    {
      id: 2,
      username: 'allen',
      email: 'allen@gmail.com'
    }
]

createUser(users)

const allUsers = getUsers()
console.log('所有用戶:', allUsers)

updateUser(1, { username: 'alex', email: 'alex@gmail.com' })
console.log('更新後的用戶:', getUsers())

deleteUser(2)
console.log('刪除後的用戶:', getUsers())

查詢資料

使用 filtered

const filteredUsers = realm.objects('User').filtered('email ENDSWITH "gmail.com"')

console.log('符合條件的用戶:', filteredUsers)

查詢時傳遞參數:

  • $0 為 1, 即搜索 id >= 1 的 user
const filteredUsers = items.filtered("id >= $0", 1)

console.log('符合條件的用戶:', filteredUsers)

https://www.mongodb.com/docs/realm-sdks/js/latest/Realm.Results.html#filtered

訂閱資料

const users = realm.objects('User')

users.addListener((collection, changes) => {
  if (changes.insertions.length > 0) {
    console.log('新增用戶:', changes.insertions)
  }
  if (changes.modifications.length > 0) {
    console.log('更新用戶:', changes.modifications)
  }
  if (changes.deletions.length > 0) {
    console.log('刪除用戶:', changes.deletions)
  }
})

https://www.mongodb.com/docs/realm/sdk/react-native/react-to-changes/

Realm 還有太多太多用法沒有提到,有興趣的可以自行閱讀官方文檔。

實現圖片快取

如果希望圖片在加載過後就不用再加載,就需要對圖片進行快取。

RN內建的 Image 組件其實也有 cache 的 prop 可以使用,但是這個 prop 僅對 iOS 有效,所以這邊要分享的是幾個可以做圖片快取的庫,支持iOS和Android:

  • react-native-blob-util
  • @georstat/react-native-image-cache
  • react-native-fast-image

react-native-blob-util

使用 RNFetchBlob 來加載和緩存圖片

import React, { useEffect, useState } from 'react'
import { Image, View, Platform } from 'react-native'
import RNFetchBlob from 'react-native-blob-util'

export const ImageCacheExample = () => {
  const [path, setPath] = useState(null)

  useEffect(() => {
    const imageUrl = 'https://unsplash.it/350/150'

    RNFetchBlob.config({
      fileCache: true,
      appendExt: 'png'
    })
      .fetch('GET', imageUrl)
      .then((res) => {
        setPath(res.path())
      })
  }, [])

  return (
    <View>
      {path && (
        <Image
          source={{ uri: Platform.OS === 'android' ? 'file://' + path : path }}
          style={{ width: 350, height: 150 }}
        />
      )}
    </View>
  );
}

https://www.npmjs.com/package/react-native-blob-util

@georstat/react-native-image-cache

使用 CacheableImage 組件來加載和緩存圖片

import { CachedImage } from '@georstat/react-native-image-cache'

<CachedImage
  source="https://unsplash.it/350/150"
  style={{ height: 350, width: 150 }}
/>

https://www.npmjs.com/package/@georstat/react-native-image-cache

react-native-fast-image

使用 FastImage 組件來加載和緩存圖片。

  • 在 source 中設置 cache 的模式
    • immutable: 預設,僅在圖片 url 更改時更新
    • web: 使用 headers 並遵循正常的快取過程
    • cacheOnly: 僅顯示快取的圖片,不發出任何請求(如果沒有快取資料那就不會顯示圖片)
  • react-native-fast-image 並不支持在 expo go 上使用,需要用 development build 測試
import FastImage from 'react-native-fast-image'

<FastImage
  source={{
    uri: data.assets.image,
    cache: FastImage.cacheControl.immutable
  }}
  style={styles.image}
  resizeMode="contain"
/>

https://www.npmjs.com/package/react-native-fast-image

實現其他類型檔案快取

一樣可以使用 react-native-blob-util 實現

  • 使用 fileCache: true 會將請求結果作為檔案保存起來,並返回路徑
  • 預設沒有副檔名,所以可以使用 appendExt 設置副檔名
RNFetchBlob.config({
  fileCache: true,
  appendExt: 'png'
})
  .fetch('GET', 'https://www.example.com/file/file.zip',{
    Authorization : 'Bearer access-token...',
  })
  .then((res) => {
    setPath(res.path())
  })

使用 RNFetchBlob.fs.unlink('file-path') 可以清除緩存:

RNFetchBlob.fs.unlink(path).then(() => {
    <em>// ...</em>
})

不過 react-native-blob-util 並不會保留緩存紀錄,所以在重啟 App 後無法獲取到之前的緩存紀錄,如果需要保留紀錄的話可以搭配 AsyncStorage 使用,將緩存後的路徑保存到本地然後使用 RNFetchBlock.fs.readFile 讀取快取檔案:

const [data, setData] = useState([])

useEffect(() => {
  loadData()
}, [])

const loadData = async () => {
  const path = await AsyncStorage.getItem('path')

  if (path) {
    RNFetchBlob.fs.readFile(path, 'utf8')
      .then((res) => {
        const data = JSON.parse(res)
        setData(data)
      })
  } else {
    RNFetchBlob
      .config({ fileCache: true, appendExt: 'json' })
      .fetch('GET', 'https://www.example.com/api/data.json')
      .then(async (res) => {
        const data = await res.json()
        setData(data)
        await AsyncStorage.setItem('path', res.path())
      })
  }
}

總結

  • 如果只是要存儲小型資料,那麼只需要使用 AsyncStorage 即可
  • 若要存儲的資料比較複雜龐大,那推薦使用 Realm, SQLite (更推薦 Realm)
  • 若是要存儲敏感資料,那推薦使用 Expo SecureStore, react-native-keychain
  • 如果要做圖片緩存推薦使用 react-native-fast-image
  • 如果要緩存多種類型的檔案,推薦使用 react-native-blob-util

參考資料

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *