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 storageauthenticationcloud message

firebase.ppt
  • 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)

似乎解決方案就是先把警告訊息關掉:

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

參考資料

權限設定

其他