這篇文章主要會分享在 RN 做持久化存儲的常見作法。
資料持久化是指將資料保存在非易失性儲存媒體中以便長期保留資料,意即關閉應用後重啟資料仍然存在,並可以隨時再次使用。
資料持久化可以通過不同的方式實現,包括但不限於:
接下來的內容除了雲端存儲外都會提到。
我覺得 StackOverflow 上的這個問題已經把要考慮的事項列的很全面了,大概重點如下:
在 React Native 中做資料持久化有哪些選擇?我看到有本地存儲(local storage)和非同步存儲(async storage),但我也看到了諸如 Realm 之類的工具,我很困惑這些如何與外部資料庫一起使用。
- 有哪些工具可以存儲資料在本地?
- 資料什麼時候會被清除?例如:關閉應用程序時、重啟手機時...等。
- 在 iOS 和 Android 中實現之間是否存在差異?
- 如何處理離線時訪問資料?
讚數最多的回覆已經滿詳細的回答了,下面的內容我也會盡量回答這幾點,並且分享每種方法的基本使用方式。
https://react-native-async-storage.github.io/async-storage/docs/install
主要是用於存儲字串,如果要存儲物件的話需要用 JSON.stringify
轉換為字串:
import AsyncStorage from '@react-native-async-storage/async-storage'
// 非物件型別的資料
const storeData = async (value) => {
try {
await AsyncStorage.setItem('my-key', value)
} catch (e) {
// saving error
}
}
// 物件型別的資料
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('my-key', jsonValue)
} catch (e) {
// saving error
}
}
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) {
// error reading value
}
}
import AsyncStorage from '@react-native-async-storage/async-storage'
const removeValue = async () => {
try {
await AsyncStorage.removeItem('my-key')
} catch(e) {
// remove error
}
}
官方有提到合理的存儲上限為 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
可以理解為是一個本地的資料庫,操作方式就是使用 SQL 語法。
(如果是使用 expo 開發可以使用 expo-sqlite)
const db = SQLite.openDatabase(
{
name: 'myDatabase.db',
location: 'default',
},
() => {
// 資料庫連接成功
},
error => {
console.error('資料庫連接時出錯', error)
}
)
db.transaction(tx => {
tx.executeSql(
'CREATE TABLE IF NOT EXISTS MyTable (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)'
);
})
// 插入數據
db.transaction(tx => {
tx.executeSql('INSERT INTO MyTable (name) VALUES (?)', ['John'], (_, result) => {
console.log('插入成功,行ID:', result.insertId)
})
})
// 查詢數據
db.transaction(tx => {
tx.executeSql('SELECT * FROM MyTable', [], (_, { rows }) => {
const data = rows.raw()
console.log('查詢結果:', data)
})
})
如果要存儲如 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)
// ...
}
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) {
// ...
}
}
如果是使用 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)
}
import Realm from 'realm'
// 定義 Schema 用來描述資料結構
const UserSchema = {
name: 'User',
properties: {
id: 'int',
username: 'string',
email: 'string',
createdAt: 'date'
}
}
import Realm from 'realm'
// 初始化 Realm 資料庫
const realm = new Realm({ schema: [UserSchema] })
// 新增 user
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)
})
}
// 獲取所有 user
const getUsers = () => {
return realm.objects('User')
}
// 更新 user 資料
const updateUser = (userId, newData) => {
realm.write(() => {
const user = realm.objectForPrimaryKey('User', userId)
if (user) {
user.username = newData.username
user.email = newData.email
}
})
}
// 刪除 user
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 的 userconst 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:
使用 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>
);
}
使用 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
使用 FastImage 組件來加載和緩存圖片。
source
中設置 cache
的模式
import FastImage from 'react-native-fast-image'
<FastImage
source={{
uri: data.assets.image,
cache: FastImage.cacheControl.immutable
}}
style={styles.image}
resizeMode="contain"
/>
一樣可以使用 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(() => {
// ...
})
不過 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())
})
}
}