Simon Lin
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee
  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # Firebase Storage Note ## Firebase Storage 大小限制 免費方案的空間大小限制是 5GB 每日最多下載流量 1GB 一天下載次數最多5萬次 上傳次數最多2萬次 就一般的開發者寫寫 side project 是非常夠用的 ![](https://i.imgur.com/izLxKzS.png) ## 新建專案 首先到 [Firebase](https://firebase.google.com/) 官網新增專案 ![](https://i.imgur.com/R7NRHtj.png) 專案命名 按自己喜好取名 ![](https://i.imgur.com/s8H3yYe.png) 這邊會問要不要啟用 GA 功能,因為不是今天重點,所以不啟用 同學們不用擔心若是之後有需要仍可以到設定內開啟 GA 相關功能 開始建立專案 會花一點時間 ![](https://i.imgur.com/GfxqMkq.png) ![](https://i.imgur.com/vTeqnPR.png) 點選繼續可以進入控制台介面 左側會有許多功能可以設定,一般開發者基本上只會用到 **開發** 功能 剛剛沒開啟的 GA 功能若是之後有需求可以到 **數據分析** 這邊開啟 簡單介紹一下控制台開發功能列表 ![](https://i.imgur.com/DkMiur0.png) * Authentication 身份驗證:顧名思義就是一些權限驗證的設定 * Database 資料庫:字面上翻過來就是資料庫 可以在這裡檢查設定我們寫進來的東西 目前區分成 Cloud Firestore 和 Realtime Database 兩種,差異可見: [GCP專門家: Firebase Cloud Firestore 及 Realtime Database 介紹及比較](https://blog.gcp.expert/compare-firebase-cloud-firestore-with-realtime-database/) > 附帶一提六角學院 Node.js 課程有 Firebase Database 的 Realtime Database 詳細教學,有興趣同學們可以回去複習一下 * Storage 儲存:字面上翻過來就是儲存、存儲 可以上傳一些圖片與影片 檔案都東西 可以想成類似 google 那種雲端的感覺 * Hosting 託管功能:是 Firebase 的網站部署功能 可以讓自己的 Firebase 作品部署託管在上面,就不用再用 github Page 或 Heroku 部署自己的網站 * Functions 後端功能 整合成一個一個自己的 Fumctions 就是 JavaScript Methods 開發者可以透過操作這些 Fumction Methods 開發和管理我們的後端功能 * ML Kit 這是和機器學習有關的服務 不在我們這次的主題內呦 ## 開始使用 Firebase Storage 點選控制台左側列表 Storage ![](https://i.imgur.com/RYgMk1D.png) 點選開始使用 ### Step1 ![](https://i.imgur.com/oiT7gXP.png) 有用過 database 功能的人應該對這個挺熟悉的,同樣是安全權限的部分,這個步驟只提示安全權限,當前步驟無法修改,我們點下一步繼續 ### Step2 ![](https://i.imgur.com/AsaTe1v.png) 接著要設定 Cloud Storage 的(機房)位置,一但選了就不能變更,有分成多區域與單區域,多區域如美國和歐洲就代表實際選擇後,這地區有好幾個機房位置可以使用,(代表歐美有不只一個機房) 基本上選哪裡都還是可以用 Firebase Storage 的服務,這裡當然選離我們比較近的地區,點選 **了解詳情** 可以看近一步說明 ![](https://i.imgur.com/oBVov3g.png) 亞洲有四個選項分別是代表 * 孟買 * 香港 * 東京 * 大阪 這裡我選擇香港 asia-east2 ![](https://i.imgur.com/KBJu0wV.png) 稍等幾秒就建立好了 建立好進來 storage 頁面,我們可以看三個分頁選項 ![](https://i.imgur.com/Xx6lauG.png) * 檔案 可以在這裡觀看上傳上來的檔案與資料夾結構,可以想成條列式的 google 雲端那種感覺 * 規則 就是剛剛說的安全權限設定規則 * 用量 可以觀看目前使用到的空間大小與流量 ## 新增應用程式 從 Project Overview 處點選 `</>` 新增網頁應用程式 ![](https://i.imgur.com/PvPCmQ9.png) 或者是點選 Project Overview 旁邊的小齒輪 選專案設定 ![](https://i.imgur.com/z3dnn2s.png) ![](https://i.imgur.com/tXJW1Jc.png) 點選 `</>` 新增網頁應用程式 進入網頁應用程式畫面 ### Step1 ![](https://i.imgur.com/E6P5Tug.png) 輸入我們的應用程式名稱,這裡也是按自己喜好取 應用程式名稱和專案名稱是不一樣的東西,一個專案底下可以有數個應用程式 這裡也可以選擇一併為這個應用程式設定代管功能 這裡先不選擇,代管功能之後還可以設定 ### Step2 ![](https://i.imgur.com/WDDp32J.png) 這邊有兩個重點 1. 檔案引入的方式: 要使用 Firebase 功能,他的新版 SDK,預設引入的 firebase-app.js 是 Firebase 的主要核心,其他的功能都被拆分成各個子項目,我們要在 script 引入 Firebase 對應的 Storage Library 檔案才可以使用 2. firebaseConfig firebaseConfig 包含了我們專案應用程式的金鑰,透過這個金鑰,你的網頁服務才能找到對應的 Firebase 對應的 database 或 storage 應用,所以每個人金鑰參數都會不一樣呦 ## 引入檔案 接著開始開發吧,使用 codepen 或在自己的電腦內新增一個 `index.html`,並 CDN 引入核心檔案與 Storage 檔案 ### CDN 引入 ``` <!-- Firebase 核心 --> <script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-app.js"></script> <!-- Firebase storage 模組檔案 --> <script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-storage.js"></script> ``` 若是在自己電腦上 html 開發,將上面兩個檔案放在`</body>`前面 ### Vue Cli 或 Create-React-App 如果是使用 Vue cli 或 Create-React-App,需要透過 npm 或 yarn 來安裝 使用終端機 cd 到專案資料夾後下指令 ``` npm install --save firebase ``` 到我們的專案 `app.js` 或 `app.vue` 開頭加入 ``` import * as firebase from "firebase/app"; import "firebase/storage"; ``` ## 初始化設定 ### 貼入金鑰 > 這裡的金鑰設定檔晚點我會砍掉,你們替換成自己的 firebaseConfig 版本就好了 新開一個 `.js` 檔案引入你的 HTML,並在裡面貼入我們的金鑰 ``` var firebaseConfig = { apiKey: "AIzaSyBDLr5ffr1l85D6rF3hsBRjALgumfZm8ZQ", authDomain: "test-storage-1150d.firebaseapp.com", databaseURL: "https://test-storage-1150d.firebaseio.com", projectId: "test-storage-1150d", storageBucket: "test-storage-1150d.appspot.com", messagingSenderId: "661329561283", appId: "1:661329561283:web:2a5a297384c6542a" }; ``` 如果是使用 Vue cli 或 Create-React-App 就貼進你的 `app.js` 或 `app.vue` 想在專門檔案管理也可以將金鑰抽出來成一隻檔案,再 `import` 進 `app.js` 或 `app.vue` 也可以 ### 初始化 Firebase 在你的 `.js` 檔案或 `app.js` 或 `app.vue` 加入這行 ``` firebase.initializeApp(firebaseConfig); ``` 這樣就完成初始化 也可以指派給一個變數並 console.log 印出來檢查看看 ``` const db = firebase.initializeApp(firebaseConfig); console.log(db) ``` ## 檔案上傳 接著終於可以開始時做檔案上傳功能了,我們先從**單檔上傳**開始,為了方便解釋我會用 codepen 來說明: ### 單檔上傳 HTML ``` <progress value="0" max="100" id="uploader">0%</progress></br> <input type="file" value="upload" id="uploadBtn"></br> <div id="msg"></div> ``` JS 先透過 getElementById 取得對應的元素 ``` const uploader = document.getElementById("uploader"); const uploadBtn = document.getElementById("uploadBtn"); const msg = document.getElementById("msg"); ``` 我們要做的是:當我們選擇檔案後,就將其上傳至雲端 ``` uploadBtn.addEventListener("change", event => { // 取得檔案資訊 const file = event.target.files[0]; const path = file.name; // 取得 storage 對應的位置 const storageReference = firebase.storage().ref(path); // .put() 方法把東西丟到該位置裡 const task = storageReference.put(file); }); ``` 實際操作會發現...沒反應 打開 console 檢查 ![](https://i.imgur.com/X2PTLmc.png) 403沒有權限,原因是因為我們規則設定,權限的部分現在是只有認證(例如登入後)的權限才可以上傳,我們到規則這邊 ![](https://i.imgur.com/udvQV0Q.png) 把不等於 ``` allow read, write: if request.auth != null; ``` 改成 ``` allow read, write: if true; ``` 然後發布 ![](https://i.imgur.com/DKN1sL8.png) 這裡方便說明我就先改成 true,但這麼做的缺點就是門戶大開,只要有你的金鑰設定檔,大家都可以上傳東西上來,實際開發時一定會會和登入功能去做權限控管 回到 codpen 重新上傳,會發現這次就成功了 在我們上傳檔案的程式碼內,我們利用 ``` firebase.storage() ``` 來取得 Firebase storage 對應的 API 和功能,利用 `.ref()` 來指向 storage 雲端裡對應的位置 若是有寫過 Firebase database 的人,應該會知道`.ref()` 不傳入路徑參數,會指向 root 根目錄,但是,我們現在上傳檔案 **這個對應位置字串需要包含檔案名稱** ``` firebase.storage().ref(相對路徑包含檔名); ``` 什麼意思呢? 來看看如果我們這樣做,會發生什麼事情: ``` .ref("folder") ``` 你以為結果是沒上傳成功或者新建一個資料夾名叫 folder 嗎? 實際上是上傳到 root 根目錄的檔案名稱變成 folder 了 因為我們真正推東西上去的 API 是 `.put()` 而`.ref()`若沒有 `"/"` 來區分資料夾層級,那麼字串參數就會變成我們檔案上傳進去的檔案名稱 但是如果我們改成這樣 ``` .ref("folder/newName") ``` 來看看結果 這邊自動新增了資料夾 folder,我們剛剛上傳的檔案名稱變成 newName 實際上 Firebase storage 沒有新增資料夾的 API,所謂的資料夾實際上是我們的路徑,storage 會替我們在上傳時自動產生: * 如果上傳時路徑的資料夾層級已經存在 storage,`.ref()`就按照這個路徑指過去 * 如果上傳時路徑資料夾層級不存在,`.ref()`就按照這個路徑新建資料夾並指過去 而另一個重點 API 當然是 ``` .put(檔案) ``` 我們透過 `.put()` 來上傳檔案 要注意的是必須傳入參數,這個參數就是我們要上傳的檔案,也就是從`event.target.file[0]` 取到的東西 [codepen 範例1](https://codepen.io/SkyrimTomato/pen/QWLgxNr?editors=1011) 了解上述資料後,可以小小優化一下,讓讀取條可以跟著跑動 ``` uploadBtn.addEventListener("change", event => { msg.textContent = ""; // 取得檔案資訊 const file = event.target.files[0]; const path = file.name; // 取得 storage 對應的位置 const storageReference = firebase.storage().ref(path) // .put() 方法把東西丟到該位置裡 const task = storageReference.put(file); // .on()監聽並連動 progress 讀取條 task.on( "state_changed", function progress(snapshot) { let uploadValue = snapshot.bytesTransferred / snapshot.totalBytes * 100; uploader.value = uploadValue; }, function error(err) { msg.textContent = "上傳失敗"; }, function complete() { msg.textContent = "上傳成功"; } ); }); ``` 當然大家都知道,實際上檔案在傳輸並不是同步的,而是**非同步**傳輸,在傳輸的過程一定需要時間,所以我們需要"監聽"上傳的過程,如何監聽呢?這裡可以使用 `.on()` ``` .on() ``` 我們把`.put()`上傳這段程式碼指派給一個變數 task ``` const task = storageReference.put(file); ``` 用點`.on()`接續在 task 變數後面監聽,`.on()`可以帶入以下參數: ``` .on("state_changed", callback1, callback2, callback3) ``` * `"state_changed"` 字串格式 表示監聽`.put()` 上傳狀態的變動 * callback1 上傳**中**觸發的 callback function 函式,上傳過程會不停觸發 * callback2 上傳**失敗**觸發的 callback function 函式,只在失敗後觸發一次 * callback3 上傳**結束**觸發的 callback function 函式,只在完成後觸發一次 callback1 這邊會帶入一個當前資料快照 snapshot 作為參數,我們從 snapshot 取出 bytesTransferred 和 totalBytes 兩個數值: * bytesTransferred 表示當前已上傳的檔案大小 * totalBytes 表示預期要上傳的檔案大小 然後將 ``` bytesTransferred/totalBytes * 100 ``` 計算出來的值指派給 `<progress>` 的 `value` 屬性 這邊是簡單的數學計算: 在看`<progress>` 這個 HTML5 讀取條時,可以用%數來想,100% 就是完成,假如我們今天有個 `10MB` 檔案在上傳,並且網路速度固定 `0.1秒` 上傳 `1MB` * 0.1 秒時 ``` 1/10 * 100 ``` 已上傳`1MB` progress 讀取條這時就是 10(%) * 0.2 秒時 ``` 2/10 * 100 ``` 已上傳`2MB` progress 讀取條這時就是 20(%) * 到 0.5 秒時 ``` 5/10 * 100 ``` 已上傳`5MB` progress 讀取條這時就是 50(%) 直到最後.. * 1秒時 ``` 10/10 * 100 ``` 已上傳`10MB` progress 讀取條這時就是 100(%) [codepen 範例2](https://codepen.io/SkyrimTomato/pen/QWLgxNr?editors=1011) ### 拖曳上傳 接著來說拖曳上傳,拖曳上傳會需要用到 HTML5 新增的 Event 事件 `drop` 和 `dragover` 這裡一樣以 codepen 為範例 HTML ``` <div id="dropContainer" class="drop-container"> <span>拖曳檔案至此上傳</span> </div> <div id="msg"></div> ``` CSS ``` .drop-container { width: 600px; height: 400px; border: 4px dashed #000; line-height: 400px; text-align: center; } ``` JS 一樣要引入 Firebase Library 和金鑰設定 ``` const dropContainer = document.getElementById("dropContainer"); const msg = document.getElementById("msg"); // 將這個範例存進 Storage drag-and-drop/ 資料夾 const folder = "drag-and-drop/" ``` 接著對我們的 dropContainer 拖曳區域下 drop 和 dragover 事件監聽 ``` // dragover 拖曳檔案至範圍 dropContainer.addEventListener("dragover", event => { event.preventDefault(); }); // drop 拖曳時放開檔案 dropContainer.addEventListener("drop", event => { event.preventDefault(); }); ``` * dragover 監聽事件是滑鼠拖曳東西進入被監聽範圍內就會觸發 * drop 監聽事件是滑鼠在該監聽範圍內左鍵放開時就會觸發 那..為什麼 dragover 和 drop 都要加 `event.preventDefault();` 阻止預設行為呢? 因為 dragover 會瘋狂被觸發嗎?這倒不是最主要考量 > 正常使用瀏覽器網站,隨便將一個圖片拖曳進瀏覽器會發生什麼事情? > 瀏覽器會離開當前網站並開啟這張圖片 為了讓瀏覽器不去做預設開啟這張圖片的行為,我們在拖曳和放開的這兩個過程,必須加上`.preventDefault();` 去阻止它的預設行為 理解這點後,我們繼續修改程式碼 在 drop 的 callback 函式內加上 ``` const file = event.dataTransfer.files[0]; ``` 注意現在是**拖曳事件**,要從 callback 參數取得檔案資訊,要用 `.dataTransfer` 而不是 `.target` 接下來如同單檔上傳那邊的範例一樣,透過 `.ref()` 指向位置,透過 `.put()` 將 file 檔案上傳 ``` const name = file.name const fullPath = `${folder}${name}`; const storageReference = firebase.storage().ref(fullPath); const task = storageReference.put(file); ``` [codepen 單檔拖曳上傳範例](https://codepen.io/SkyrimTomato/pen/abowjVw?editors=0011) ### 多檔拖曳上傳 多檔拖曳其實很簡單 應該有人注意到我用事件 event 參數取 file 檔案時是用 `files[0]` 去取 實際上 `event.dataTransfer.files` 是一個 `Array-like` 物件,他是個類陣列的物件,我們可以用 `[0]` 取到這個 files 物件的第一筆檔案資料 > 當然,若你對 JavaScript 夠熟悉,應該知道 Array 就是 Object 我們可以下 console.log() 來檢查 files ![](https://i.imgur.com/bqzQUES.png) 可以發現 files 是個物件,其 key 像陣列一樣是由 0 開始數,並且有個隱藏屬性 `length` 列出有多少數量的 *File* 檔案 我們沒辦法用 Array 原生方法例如 `forEach` 來迭代 *File* 物件 用 `for-in` 還會把 `length` 給印出來多此一舉,雖然可以用 `Object.key` 或 `Array.from` 來轉換成 Array,但既然 `length` 這個隱藏屬性表示實際上 *File* 數量.. 那我們就可以用一般的 for 迴圈來迭代就可以了 ``` const files = event.dataTransfer.files; for(let i = 0; i < files.length; i++) { const path = folder + files[i].name; const storageReference = firebase.storage().ref(path); const task = storageReference.put(files[i]); } ``` 這個一來就可以在 for 迴圈迭代的過程中,個別將上傳的檔案透過 `.ref()` 和 `.put()` 上傳至雲端 [codepen 多檔拖曳上傳範例](https://codepen.io/SkyrimTomato/pen/mdbwzdr?editors=0011) ### 資料夾上傳 最後一個上傳來講資料夾上傳,資料夾上傳就比較要複雜一點 上傳一個資料夾,裡頭可能有複數資料夾,有的層級有檔案,有的層級還是資料夾,面對這種巢狀結構,我們需要用到 **遞迴** 來迭代資料夾樹狀的路徑 先來看看 drop 事件這邊 ``` dropContainer.addEventListener("drop", event => { event.preventDefault(); // 用 dataTransfer.items 取得拖曳進來的資料物件 注意這裡不是使用 dataTransfer.file const filesData = event.dataTransfer.items; }) ``` 我們現在上傳的資料分類不只是 file 檔案,同時還會有資料夾,所以我們要用 `.items` 來取得拖曳進來的資料,然後再篩選分類出哪些是資料夾或檔案 ``` dropContainer.addEventListener("drop", event => { event.preventDefault(); // 用 dataTransfer.items 取得拖曳進來的資料物件 注意這裡不是使用 dataTransfer.file const filesData = event.dataTransfer.items; // 考量到可能會一次拖曳好幾個資料夾,這裡要先 for 迭代一次 for (var i=0; i < filesData.length; i++) { // 使用 webkitGetAsEntry 來取得傳入進來的檔案分層列表 const files = filesData[i].webkitGetAsEntry(); if (files) { sortFileTree(files); } } }) ``` 而 sortFileTree 函式長這樣 ``` // 遞迴檢查是檔案還是資料夾 const sortFileTree = (item, path = "") => { if (item.isFile) { item.file( file => { const fullPath = `${path}${file.name}` console.log(fullPath) const storageReference = firebase.storage().ref(fullPath); // .put() 方法把東西丟到雲端裡 var task = storageReference.put(file); }); } else if (item.isDirectory) { // 透過 File API createReader 去創建這層資料夾物件已供我們後續讀取 const dirReader = item.createReader(); // 透過 readEntries 去讀取這個資料夾目錄裡頭的東西 dirReader.readEntries(entries => { console.log("entries.length",entries.length) // 使用 entries.length 知道該層級往下共有多少檔案數量 為了 loop 讀取這些檔案 這裡要跑 for 迭代 for (let i = 0; i < entries.length; i++) { // loop 讀到該檔案時,再呼叫一次 sortFileTree 並傳入該檔案與路徑 sortFileTree(entries[i], path + item.name + "/"); } }); } } ``` 使用遞迴和使用 for 迴圈一樣:要小心無窮遞迴 囧 我們要在裡頭做個 `if` 來判斷什麼情境下呼叫自己、什麼時候結束 我們傳入遞迴函式的檔案是透過 webkitGetAsEntry 取得的目錄物件,我們可以透過 `isFile` 和 `isDirectory` 來幫助我們判斷 * isFile 判斷是否是檔案:如果是檔案就將其上傳到 storage,遞迴結束 * isDirectory 判斷是否為資料夾:如果是資料夾就就條列取得裡頭所有項目,然後再呼叫 sortFileTree 往下遞迴去判斷 [firebase storage 拖曳資料夾上傳](https://codepen.io/SkyrimTomato/pen/oNvwRdz?editors=0011) ## 下載 拖曳資料夾上傳這邊可能很多人聽不懂,沒關係接下來拉回 storage 功能,來講講下載 下載的範例會沿用單檔上傳的部分 HTML 新增下載按鈕 ``` <progress value="0" max="100" id="uploader">0%</progress></br> <input type="file" value="upload" id="uploadBtn"></br></br> <button id="downloadBtn">download</button></br> <div id="msg"></div> ``` ![](https://i.imgur.com/tqaNqml.png) JS 新增下面這段事件 ``` downloadBtn.addEventListener("click",event => { // 取得 storage 中對應的檔案位置 const fileRef = firebase.storage().ref(fullPath) // .ref() 指向已存在 storage 中的檔案位置後 可以透過 getDownloadURL 取得連結 fileRef.getDownloadURL().then(function (url) { console.log(url) }) }) ``` ### 取得檔案連結 我們在下載點擊事件中,用到了 `getDownloadURL` * `getDownloadURL()` 回傳字串連結,可取得檔案在 storage 上的連結位置 getDownloadURL 是個 promise ,我們可以後面接個 `.then` 去接它返傳的 url 這裡搭配 `console.log` 來檢查 ![](https://i.imgur.com/byvF4Qp.png) 可以看到 url 就是檔案在 Firebase storage 上的位置連結 接著我們就可以拿 `getDownloadURL` return 的 url 去做些事情,例如是圖片連結就指派其給 `<img>` 的 `src` 屬性 ### 跨域問題 若遇到跨域問題 ![](https://i.imgur.com/Du9EogA.png) 見下方 Firebase Storage Cors 跨域設定 ### 下載 取得檔案連結後,我們想要將實體化下載成本地檔案,這時遇到幾個知識點: * 若今天檔案下載網站和檔案來源是同源同網域,可以直接透過 `<a>` 標籤配合 `download` 屬性去抓 * 若檔案下載網站是不同源 (例如 codepen 或 localhost ),解決了跨域問題後,我們還要把它轉成 *blob* 格式才可以下載 這裡 AJAX 取檔案,用 `fetch` 和 `blob()` 來示範 當我們轉成 blob 格式後,要用 `createObjectURL()` 將 *blob* 轉成實際檔案與取得在本地瀏覽器中的位置連結 ``` fetch(url) .then(res => res.blob()) .then(blob => { let a = document.createElement("a"); let url = window.URL.createObjectURL(blob); a.href = url; a.download = name; // Firefox 需要將 JS 建立出的 element appendChild 到 DOM 上才可以 work a.style.display = "none"; document.body.appendChild(a); a.click(); // 刪除多餘的 DOM 與 釋放記憶體 document.body.removeChild(a); window.URL.revokeObjectURL(url); }); ``` [codepen 範例: Firebase storage 檔案下載](https://codepen.io/SkyrimTomato/pen/ExYXqGG?editors=0010) ## 刪除 刪除的部分最簡單 只會用到 `delete()` * `.delete()` 一樣用 `.ref` 取得的檔案位置,後面再接上`.delete()`就可以刪除 HTML 新增刪除按鈕 ``` <progress value="0" max="100" id="uploader">0%</progress></br> <input type="file" value="upload" id="uploadBtn"></br></br> <button id="downloadBtn">download</button></br></br> <button id="deleteBtn">delete</button></br> <div id="msg"></div> ``` JS ``` deleteBtn.addEventListener("click",() => { // 取得 storage 中對應的檔案位置 const fileRef = firebase.storage().ref(fullPath); fileRef.delete().then( () => { msg.textContent = "刪除成功"; uploadBtn.value = ""; uploader.value = 0; }).catch(function (error) { msg.textContent = "刪除失敗"; }) }) ``` [codepen 範例: Firebase storage 下載&刪除](https://codepen.io/SkyrimTomato/pen/NWKvWGo?editors=0010) ## 條列式讀取 Firebase Storage 上的檔案 最後可能會有人想,那怎麼沒講到讀取? 製作雲端系統時,頁面顯示時要怎麼**條列式列出這層有哪些檔案或資料夾?** 這部分,Firebase Storage 有提供便捷的 API 來幫助我們列出 * `.listAll()` * `.prefixes` * `.items` `listAll()` 後方傳入的參數 res 透過`.prefixes` 取得這層所有資料夾 透過 `.items` 取得這層所有檔案與資訊 [codepen 範例: 條列出資料夾與檔案名稱](https://codepen.io/SkyrimTomato/pen/mdbMdGQ?editors=1011) > 備註:請將 firebaseConfig 替換成你自己的設定呦 ## Firebase Storage Cors 跨域設定 當我們要下載或上傳 Storage 東西時,若瀏覽器 console 出現 CORS 報錯,代表我們沒設定跨域設定,見 [Firebase 文件](https://firebase.google.com/docs/storage/web/download-files#cors_configuration) 與 [Cloud Storage CORS](https://cloud.google.com/storage/docs/configuring-cors?hl=zh-tw) 說明 要做到跨域的設定與白名單,我們需要安裝 gsutil 這個套件 [gsutil 安裝方法](https://cloud.google.com/storage/docs/gsutil_install) 接著在自己電腦新增一支名為 cors.json 的檔案,其內容為 ``` [ { "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600 } ] ``` * origin 值為陣列,裡頭可以存放多個要設為白名單的網域(字串格式),這裡 `"*"` 字號代表不限制網域都可以的意思 * method 值為陣列,裡頭存放我們想設定允許的方法(字串格式),例如 GET、POST,這裡先設定`"GET"` * maxAgeSeconds 值為數值,maxAgeSeconds 指的是指定時間秒數,瀏覽器對特定資源的預取(OPTIONS)請求返回結果的快取緩存時間,單位為秒,藉由快取回應的方式,瀏覽器便不需要在原始要求重複時,將再次向 Firebase 傳送要求。 > cors.json 檔案存放位置不拘,但要記住你放在哪個資料夾位置 因為接著要開啟終端機,cd 指到你存有 cors.json 的資料夾位置後,輸入 ``` gsutil cors set cors.json 你的Storage資料夾網址 ``` Storage 資料夾網址可以到 Firebase 後台 > Storage > 檔案,就是附圖中的 `gs://xxxx` ![](https://i.imgur.com/CjHrZEA.png) 若是終端機出現 401 訊息 代表沒登入,在終端機輸入 ``` gcloud auth login ``` 有安裝 gsutil 這邊 gcloud 指令就會正常執行 選擇你的帳戶,注意是對應你當前 Firebase 應用程式的 google 帳號登入 接著再輸入 ``` gsutil cors set cors.json 你的Storage資料夾網址 ``` 看到終端機出現 `Setting CORS on gs:/xxxx` 就成功了! 若是出現 403 訊息,代表當前可能已經登入但對應的 Storage 帳號不一樣,就要先登出 ``` gcloud auth revoke --all ``` 然後再登入一次 ``` gcloud auth login ``` 接著登入後一樣回終端機輸入 ``` gsutil cors set cors.json 你的Storage資料夾網址 ``` 看到終端機出現 `Setting CORS on gs:/xxxx` 就成功了! ## 進階 TODO 瞭解 Firebase Storage 基本上傳、下載、刪除、條列顯示功能,各位可以思考進階雲端系統開發,自己的雲端系統還要添加什麼功能和設計 例如 * 檔案存入 Storage 時,是否也要存入檔案資訊到 Firebase Database? * listAll 條列出檔案時,是否將檔案/資料夾路徑資訊透過 `data-` 綁在 HTML tag 上供點擊事件取用? * 雲端資料夾與路由該如何分層? * 麵包選單設定? * 分享功能的網址哪裡來的? * 刪除與 undo 功能? ### Enjoy Coding!

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully