Site icon May's Notes

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

React Native logo

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

什麼是資料持久化?

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

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

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

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

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

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

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

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

AsyncStorage

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

存儲資料

主要是用於存儲字串,如果要存儲物件的話需要用 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 語法。

(如果是使用 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

存儲資料

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,可以在設備本地加密和安全存儲資料。

存儲資料

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

定義 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)

查詢時傳遞參數:

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

使用 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 組件來加載和緩存圖片。

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 實現

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())
      })
  }
}

總結

參考資料

Exit mobile version