Firebase
Firebase
2021/06/14 (更新內容)
簡介
Firebase是個雲端服務,現在已經被google收購。原本firebase (realtime database)是以key-value為主的NoSQL,在google收購後,與原本google的data store整合,推出firestore。Firestore的主要是以兩個概念構成:collection及document,一個collection中可以有很多document,每個document中還可以有document或collection。
現在firebase建議開發者使用firestore,應該不久的將來realtime database會停止服務。
Firebase提供的服務很多,除了兩個NoSQL服務: realtime database、firestore,還有cloud storage、authentication、cloud message。
Firebase
新增專案
新增應用程式
建立資料庫
新增集合
新增文件
設定
目前雖然有react-native-firebase,但是,因為我們使用expo,所以,不能使用react-native-firebase,必須使用firebase套件。
Using Firebase (expo.io)
Firebase SDK Setup
Storing Data and Receiving Updates
User Authentication
Using Expo with Firestore
Recording events with Analytics
firestore 7.9.0
**目前最新版本是8.4.2,寫法跟7.9.0有一點點不同。
先利用expo安裝firebase套件 (7.9.0)
expo install firebase@7.9.0
寫第一個測試程式,參考firestore文件 (Get started with Cloud Firestore ),修改一下我們的ProductList。
先import我們需要的套件
import * as firebase from 'firebase';
import firestore from 'firebase/firestore'
import {config} from './firebase_config';
根據我們所產生的應用程式,利用firebase_config.js設定相關參數:
/src/product/firebase_config.js
export const config = {
apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authDomain: "product-a87b2.firebaseapp.com",
databaseURL: "https://product-a87b2.firebaseio.com",
projectId: "product-a87b2",
storageBucket: "product-a87b2.appspot.com",
messagingSenderId: "576564577770",
appId: "1:576564577770:web:a6e3f73c173f1782dd5e93"
};
接下來啟動firebase app,為避免重複啟動,可以利用firebase.apps.length先檢查一下:
if (!firebase.apps.length) {
firebase.initializeApp(config);
}
啟動firestore
const db = firebase.firestore();
firestore 8.4.2
目前最新版本是8.4.2,寫法跟7.9.0有點不同。9.0.0目前還在beta測試(2021/04/28)。
先利用expo安裝firebase套件 (8.4.2)
expo install firebase@8.4.2
寫第一個測試程式,參考firestore文件 (Get started with Cloud Firestore ),修改一下我們的ProductList。
先import我們需要的套件
import firebase from 'firebase/app';
import 'firebase/firestore';
import {config} from './firebase_config';
根據我們所產生的應用程式,利用firebase_config.js設定相關參數:
/src/product/firebase_config.js
export const config = {
apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authDomain: "product-a87b2.firebaseapp.com",
databaseURL: "https://product-a87b2.firebaseio.com",
projectId: "product-a87b2",
storageBucket: "product-a87b2.appspot.com",
messagingSenderId: "576564577770",
appId: "1:576564577770:web:a6e3f73c173f1782dd5e93"
};
接下來啟動firebase app,為避免重複啟動,可以利用firebase.apps.length先檢查一下:
!firebase.apps.length ? firebase.initializeApp(config) : firebase.app();
等於
if (!firebase.apps.length) {
firebase.initializeApp(config);
}
else {
firebase.app();
}
啟動firestore
const db = firebase.firestore();
讀取資料
Firestore讀取資料的範例(Get started with Cloud Firestore),這個範例是去讀取users的collection,利用get()讀取collection裡所有文件,利用console.log印出id (doc.id)及文件裡的內容(doc.data()):
db.collection("users").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(`${doc.id} => ${doc.data()}`);
});
});
我們先在firestore新增一些資料。先新增一個product的collection,再新增兩個文件,文件裡有兩個欄位:desc跟price,desc是個字串(string),price是個數字(number)。
根據範例,我們改成讀取product集合,將讀取到的每一筆資料,利用console.log印出
db.collection("product").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(`${doc.id} => ${doc.data()}`);
console.log(doc.data().desc);
console.log(doc.data().price);
});
});
以上的寫法是利用.then,建議改用async/await:
async function readData(){
console.log("reading...");
const newProducts=[];
try {
const querySnapshot = await db.collection("product").get();
querySnapshot.forEach((doc) => {
//console.log(`${doc.id} => ${doc.data().desc}`);
console.log(doc.data().desc);
console.log(doc.data().price);
});//foreach
}//try
catch(e){console.log(e);}
}//readData
useEffect(()=>{
readData();}
,[]
);
自己先試試看,再跟老師的範例比對。
/src/product/ProductList.js
執行的時候會產生這個警告,似乎是firebase與react native的相容問題。
Setting a timer for a long period of time, i.e. multiple minutes, is a performance and correctness issue on Android as it keeps the timer module awake,
and timers can only be called when the app is in the foreground. See https://github.com/facebook/react-native/issues/12981 for more info.
(Saw setTimeout with duration 434054ms)
Setting a timer for a long period of time, i.e. multiple minutes #97
React Native Warning: “Setting a timer for a long period of time…”. How to locate cause?
Setting a timer for a long period of time, i.e. multiple minutes
似乎解決方案就是先把警告訊息關掉:
import React, {useState, useEffect} from 'react';
import {FlatList, View, Text, YellowBox} from 'react-native';
import {Icon, Fab} from 'native-base';
import * as firebase from 'firebase';
import firestore from 'firebase/firestore'
import ProductAdd from './ProductAdd';
import styles from '../styles';
import {config} from './firebase_config';
export default function ProductList() {
YellowBox.ignoreWarnings(['Setting a timer']);
現在可以在console看到讀取到的資料了,接下來,要把讀取到的資料放到state,才會讓內容出現在FlatList裡。
不過,React Native 0.63之後,以LogBox取代YellowBox。**還沒處理**
接下來,把資料放到FlatList裡面。
/src/product/ProductList.js
import React, {useState, useEffect} from 'react';
import {FlatList, View, Text, YellowBox} from 'react-native';
import {Icon, Fab} from 'native-base';
import * as firebase from 'firebase';
import firestore from 'firebase/firestore'
import ProductAdd from './ProductAdd';
import styles from '../styles';
import {config} from './firebase_config';
export default function ProductList() {
YellowBox.ignoreWarnings(['Setting a timer']);
const renderItem = ({ item, index }) => (
<View style={styles.item}>
<Text>{index}</Text>
<Text style={styles.title}>{item.desc}</Text>
<Text>{item.price}</Text>
</View>
);
if (!firebase.apps.length) {
firebase.initializeApp(config);
}
const db = firebase.firestore();
//console.log("test");
const [products, setProducts] = useState([]);
async function readData(){
const newProducts = [];
try {
const querySnapshot = await db.collection("product").get();
querySnapshot.forEach((doc) => {
const newProduct = {
desc:doc.data().desc,
price:doc.data().price
}
newProducts.push(newProduct);
});//foreach
}//try
catch(e){console.log(e);}
setProducts([...newProducts]);
}//readData
useEffect(()=>{
readData();}
,[]
);
const [modalVisible, setModalVisible] = useState(false);
function update(newProduct){
setModalVisible(false);
setProducts(oldProducts=>[...oldProducts, newProduct]);
}
return (
<View style={styles.container}>
<FlatList
data={products}
renderItem = {renderItem}
keyExtractor={(item, index) => ""+index}
>
</FlatList>
<Fab onPress={()=>setModalVisible(true)}>
<Icon ios='ios-add' android="md-add"/>
</Fab>
<ProductAdd modalVisible = {modalVisible} update={update}/>
</View>
);
}
簡化設定
除了把設定參數寫在firebase-config裡之外,還有更好的做法,可以使用expo提供的FirebaseCore。安裝FirebaseCore:
expo install expo-firebase-core
安裝好之後,先將firebase相關參數設定好 (詳參:Using the native Firebase SDK ):
修改後的app.json:
{
"expo": {
"name": "test-reactnative",
"slug": "test-reactnative",
"platforms": [
"ios",
"android",
"web"
],
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"web": {
"config": {
"firebase": {
"apiKey": "XXXXXXXXXXXXXXXXXXXXXXX",
"authDomain": "product-a87b2.firebaseapp.com",
"databaseURL": "https://product-a87b2.firebaseio.com",
"projectId": "product-a87b2",
"storageBucket": "product-a87b2.appspot.com",
"messagingSenderId": "576564577770",
"appId": "1:576564577770:web:a6e3f73c173f1782dd5e93"
}
}
}
}
}
因為所有的設定已經放在app.json裡,就可以不用把設定放在我們的程式裡。
/src/product/ProductList.js
import React, {useState, useEffect} from 'react';
import {FlatList, View, Text, YellowBox} from 'react-native';
import {Icon, Fab} from 'native-base';
import * as firebase from 'firebase';
import firestore from 'firebase/firestore';
import * as FirebaseCore from 'expo-firebase-core';
import ProductAdd from './ProductAdd';
import styles from '../styles';
export default function ProductList() {
YellowBox.ignoreWarnings(['Setting a timer']);
const renderItem = ({ item, index }) => (
<View style={styles.item}>
<Text>{index}</Text>
<Text style={styles.title}>{item.desc}</Text>
<Text>{item.price}</Text>
</View>
);
if (!firebase.apps.length) {
//console.log(FirebaseCore.DEFAULT_WEB_APP_OPTIONS);
//firebase.initializeApp(config);
firebase.initializeApp(FirebaseCore.DEFAULT_WEB_APP_OPTIONS);
}
const db = firebase.firestore();
//console.log("test");
const [products, setProducts] = useState([]);
async function readData(){
try {
const querySnapshot = await db.collection("product").get();
const newProducts = [];
querySnapshot.forEach((doc) => {
const newProduct = {
desc:doc.data().desc,
price:doc.data().price
}
newProducts.push(newProduct);
});//foreach
setProducts([...newProducts]);
}//try
catch(e){console.log(e);}
}//readData
useEffect(()=>{
readData();}
,[]
);
const [modalVisible, setModalVisible] = useState(false);
function update(newProduct){
setProducts(oldProducts=>[...oldProducts, newProduct]);
setModalVisible(false);
}
return (
<View style={styles.container}>
<FlatList
data={products}
renderItem = {renderItem}
keyExtractor={(item, index) => ""+index}
>
</FlatList>
<Fab onPress={()=>setModalVisible(true)}>
<Icon ios='ios-add' android="md-add"/>
</Fab>
<ProductAdd modalVisible = {modalVisible} update={update}/>
</View>
);
}
資料排序與篩選
可以試試看如何讓資料根據欄位排序或設定篩選的條件,官網範例如下:
var citiesRef = db.collection("cities");
取得collection後
var query = citiesRef.orderBy("state").orderBy("population", "desc");
或
var query = citiesRef.where("population", ">", 100000).orderBy("population");
利用query取得資料
const querySnapshot = query.get();
可以利用where進行條件查詢 (詳參: 在Cloud Firestore中執行簡單和復合查詢)
// Create a reference to the cities collection
var citiesRef = db.collection("cities");
// Create a query against the collection.
var query = citiesRef.where("state", "==", "CA");
複合查詢
var query = citiesRef.where("state", "==", "CO").where("name", "==", "Denver");
可以指定排序的方式,也可以限制只傳前幾筆 (詳參:使用Cloud Firestore排序和限制資料數量)
var query = citiesRef.orderBy("name").limit(3);
當資料量大的時候,需要建立索引,增加搜尋的速度。(詳參: 在Cloud Firestore中管理索引)
新增資料
Firestore新增資料的範例:
// Add a second document with a generated ID.
db.collection("users").add({
first: "Alan",
middle: "Mathison",
last: "Turing",
born: 1912
})
.then(function(docRef) {
console.log("Document written with ID: ", docRef.id);
})
.catch(function(error) {
console.error("Error adding document: ", error);
});
接下來,根據範例,我們來修改ProductAdd.js,讓輸入的資料新增到firestore裡,也利用async/await來呼叫add()。
/src/product/ProductAdd,js
import React, {useState} from 'react';
import { Button , TextInput, Modal } from 'react-native';
import * as firebase from 'firebase';
import firestore from 'firebase/firestore';
import * as FirebaseCore from 'expo-firebase-core';
import styles from '../styles';
export default function ProductAdd(props) {
const [desc, setDesc] = useState("");
const [price, setPrice] = useState("");
if (!firebase.apps.length) {
firebase.initializeApp(FirebaseCore.DEFAULT_WEB_APP_OPTIONS);
}
const db = firebase.firestore();
async function update(){
try {
const docRef = await db.collection("product").add({
desc: desc,
price: parseInt(price)
});
console.log(docRef.id);
setDesc("");
setPrice("");
props.update();
}
catch(error) {
console.error("Error adding document: ", error);
}
}
function cancel(){
setDesc("");
setPrice("");
props.update();
}
return (
<Modal visible={props.modalVisible}>
<TextInput placeholder="產品說明" value={desc} onChangeText={text=>setDesc(text)}/>
<TextInput placeholder="價格" value={price} onChangeText={text=>setPrice(text)}/>
<Button onPress={update} title="新增"/>
<Button onPress={cancel} title="取消"/>
</Modal>
);
}
/src/product/ProductList.js配合ProductAdd.js,做了一些調整:
import React, {useState, useEffect} from 'react';
import {ActivityIndicator, FlatList, View, Text, YellowBox} from 'react-native';
import {Icon, Fab} from 'native-base';
import * as firebase from 'firebase';
import firestore from 'firebase/firestore';
import * as FirebaseCore from 'expo-firebase-core';
import ProductAdd from './ProductAdd';
import styles from '../styles';
export default function ProductList() {
YellowBox.ignoreWarnings(['Setting a timer']);
const [products, setProducts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const renderItem = ({ item, index }) => (
<View style={styles.item}>
<Text>{index}</Text>
<Text style={styles.title}>{item.desc}</Text>
<Text>{item.price}</Text>
</View>
);
if (!firebase.apps.length) {
firebase.initializeApp(FirebaseCore.DEFAULT_WEB_APP_OPTIONS);
}
const db = firebase.firestore();
async function readData(){
const newProducts=[];
try {
const querySnapshot = await db.collection("product").get();
querySnapshot.forEach((doc) => {
const newProduct = {
desc:doc.data().desc,
price:doc.data().price
}
newProducts.push(newProduct);
});//foreach
setIsLoading(false);
setProducts([...newProducts]);
}//try
catch(e){console.log(e);}
}//readData
useEffect(()=>{
readData();}
,[modalVisible]
);
function update(){
setModalVisible(false);
}
return (
<View style={styles.container}>
{!isLoading ?
<FlatList
data={products}
renderItem = {renderItem}
keyExtractor={(item, index) => ""+index}
>
</FlatList>
:
<View style={styles.loading}>
<ActivityIndicator color="red" size="large" animating={isLoading} />
</View>
}
<Fab onPress={()=>setModalVisible(true)}>
<Icon ios='ios-add' android="md-add"/>
</Fab>
<ProductAdd modalVisible = {modalVisible} update={update}/>
</View>
);
}
update的部分,由於會再去讀資料庫的內容,所以,只需要改變modalVisible的內容,因為modalVisible的內容改變,也透過useEffect呼叫readData()
function update(){
setModalVisible(false);
}
作業
完成修改,在firestore裡,利用set去新增或修改文件內容,這時候需要有document的id,因為我們產生document的時候,是利用自動產生的id,所以,就像airtable一樣要先取得id。(詳參:Add data to Cloud Firestore )
set是更新整個文件的內容
// update document in collection "cities"
db.collection("cities").doc("LA").set({
name: "Los Angeles",
state: "CA",
country: "USA"
})
.then(function() {
console.log("Document successfully written!");
})
.catch(function(error) {
console.error("Error writing document: ", error);
});
update是只更新文件的部分內容
// update document in collection "cities"
db.collection("cities").doc("LA").update({
state: "CA",
})
.then(function() {
console.log("Document successfully written!");
})
.catch(function(error) {
console.error("Error writing document: ", error);
});
完成刪除
db.collection("cities").doc("DC").delete().then(function() {
console.log("Document successfully deleted!");
}).catch(function(error) {
console.error("Error removing document: ", error);
});
FAQ
如何取得一筆資料。document的id存在doc_id變數中:
let doc = await db.collection("cities").doc(doc_id).get();
let event_id = doc.data().event_id;
為什麼我console.log可以取得doc.id但是doc.data()是有問題的,因為doc.id會取得我們傳給.doc()的值,如果這個值在firestore裡面不存在,就無法取得資料,可以利用exists檢查:
if (doc.exists) {
console.log (doc.data().event_id);
}
參考資料
權限設定
其他
React Native Firebase Tutorial: Build CRUD Firestore App (2018內容有點舊...)
expo + firebase.js
Using Firebase (Expo)
How to Build a React Native App and Integrate It with Firebase
Creating a Firebase project
Creating & configuring a new React Native app
Setting up the folder structure, routes, and navigation
Implementing the UI for Login, Registration, and Home screens
Registration with Firebase Auth
Login with Firebase Auth
Persistent Login Credentials
Writing and reading data from Firebase Firestore
How to Add Authentication to React Native in Three Steps Using Firebase (not working with Expo)
Installing react-native-firebase
Authentication React Native Firebase
Adding react-native-firebase to an existing project
Running the app