Fetch API

Emscripten Fetch API 允許原生程式碼透過 XHR (HTTP GET、PUT、POST) 從遠端伺服器傳輸檔案,並將下載的檔案在本機瀏覽器的 IndexedDB 儲存空間中保存,以便在後續頁面瀏覽時可以在本機重新存取。 Fetch API 可從多個執行緒呼叫,並且網路請求可以根據需要同步或非同步執行。

注意

為了使用 Fetch API,您需要使用 -sFETCH 編譯程式碼。

簡介

Fetch API 的使用可以透過一個範例快速說明。以下應用程式會從網頁伺服器非同步下載檔案到應用程式堆積內的記憶體中。

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
  printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
  // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
  printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSucceeded;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

如果像上面的範例一樣,在呼叫 emscripten_fetch 時指定了相對路徑名稱,則 XHR 是相對於目前頁面的 href (URL) 執行。傳遞完全符合規範的絕對 URL 允許跨網域下載檔案,但這些檔案會受到 HTTP 存取控制 (CORS) 規則 的約束。

預設情況下,Fetch API 會非同步執行,這表示 emscripten_fetch() 函式呼叫會立即返回,並且操作將在背景繼續進行。當操作完成時,會調用成功或失敗回呼。

保存資料

Fetch API 發出的 XHR 請求會受到一般瀏覽器快取行為的約束。這些快取是暫時性的,因此無法保證資料會在快取中保留一段特定時間。此外,如果檔案有點大 (數 MB),瀏覽器通常根本不會快取下載。

為了更明確地控制保存下載的檔案,Fetch API 會與瀏覽器的 IndexedDB API 互動,IndexedDB API 可以載入和儲存大型資料檔案,這些檔案可以在後續瀏覽頁面時使用。若要啟用 IndexedDB 儲存,請在擷取屬性中傳遞 EMSCRIPTEN_FETCH_PERSIST_FILE 旗標

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  ...
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_PERSIST_FILE;
  ...
  emscripten_fetch(&attr, "myfile.dat");
}

如需完整範例,請參閱檔案 test/fetch/test_fetch_persist.c

從記憶體保存資料位元組

有時,將應用程式記憶體中的一系列位元組保存到 IndexedDB (而無需執行任何 XHR) 會很有用。透過將特殊的 HTTP 動作動詞「EM_IDB_STORE」傳遞給 Emscripten Fetch 操作,可以使用 Emscripten Fetch API 完成此操作。

void success(emscripten_fetch_t *fetch) {
  printf("IDB store succeeded.\n");
  emscripten_fetch_close(fetch);
}

void failure(emscripten_fetch_t *fetch) {
  printf("IDB store failed.\n");
  emscripten_fetch_close(fetch);
}

void persistFileToIndexedDB(const char *outputFilename, uint8_t *data, size_t numBytes) {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "EM_IDB_STORE");
  attr.attributes = EMSCRIPTEN_FETCH_REPLACE | EMSCRIPTEN_FETCH_PERSIST_FILE;
  attr.requestData = (char *)data;
  attr.requestDataSize = numBytes;
  attr.onsuccess = success;
  attr.onerror = failure;
  emscripten_fetch(&attr, outputFilename);
}

int main() {
  // Create data
  uint8_t *data = (uint8_t*)malloc(10240);
  srand(time(NULL));
  for(int i = 0; i < 10240; ++i) data[i] = (uint8_t)rand();

  persistFileToIndexedDB("outputfile.dat", data, 10240);
}

從 IndexedDB 刪除檔案

可以使用 HTTP 動作動詞「EM_IDB_DELETE」從 IndexedDB 中清除檔案

void success(emscripten_fetch_t *fetch) {
  printf("Deleting file from IDB succeeded.\n");
  emscripten_fetch_close(fetch);
}

void failure(emscripten_fetch_t *fetch) {
  printf("Deleting file from IDB failed.\n");
  emscripten_fetch_close(fetch);
}

int main() {
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "EM_IDB_DELETE");
  emscripten_fetch(&attr, "filename_to_delete.dat");
}

同步擷取

在某些情況下,能夠在呼叫執行緒中同步執行 XHR 請求或 IndexedDB 檔案操作會很好。這可以使移植應用程式更容易,並透過避免傳遞回呼的需求來簡化程式碼流程。

所有類型的 Emscripten Fetch API 操作 (XHR、IndexedDB 存取) 都可以透過傳遞 EMSCRIPTEN_FETCH_SYNCHRONOUS 旗標同步執行。當傳遞此旗標時,呼叫執行緒將會封鎖以進入睡眠狀態,直到擷取操作完成為止。請參閱以下範例。

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS;
  emscripten_fetch_t *fetch = emscripten_fetch(&attr, "file.dat"); // Blocks here until the operation is complete.
  if (fetch->status == 200) {
    printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
    // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  } else {
    printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  }
  emscripten_fetch_close(fetch);
}

在上面的程式碼範例中,未使用成功和失敗回呼函式。但是,如果指定了這些函式,則會在 emscripten_fetch() 返回之前同步呼叫它們。

注意

同步 Emscripten Fetch 操作會受到一些限制,具體取決於使用的 Emscripten 建置模式 (連結器旗標)

  • 無旗標:僅提供非同步 Fetch 操作。

  • --proxy-to-worker:同步 Fetch 操作允許僅執行 XHR 但不與 IndexedDB 互動的擷取。

  • -pthread:同步 Fetch 操作在 pthreads 上可用,但在主執行緒上不可用。

  • --proxy-to-worker + -pthread:同步 Fetch 操作在主執行緒和 pthreads 上都可用。

追蹤進度

為了可靠地管理擷取,可以使用多個欄位來追蹤 XHR 的狀態。

每當收到新資料時,就會呼叫 onprogress 回呼。這允許使用者測量下載速度並計算完成的 ETA。此外,emscripten_fetch_t 結構會傳遞 XHR 物件欄位 readyState、status 和 statusText,這些欄位提供關於請求的 HTTP 載入狀態的資訊。

emscripten_fetch_attr_t 物件有一個 timeoutMSecs 欄位,允許使用者指定傳輸的逾時持續時間。此外,可以隨時呼叫 emscripten_fetch_close() 以中止非同步和可等待擷取的下載。以下範例說明這些欄位和 onprogress 處理常式。

void downloadProgress(emscripten_fetch_t *fetch) {
  if (fetch->totalBytes) {
    printf("Downloading %s.. %.2f%% complete.\n", fetch->url, fetch->dataOffset * 100.0 / fetch->totalBytes);
  } else {
    printf("Downloading %s.. %lld bytes complete.\n", fetch->url, fetch->dataOffset + fetch->numBytes);
  }
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSucceeded;
  attr.onprogress = downloadProgress;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

管理大型檔案

應特別注意擷取的記憶體使用策略。先前的範例都傳遞了 EMSCRIPTEN_FETCH_LOAD_TO_MEMORY 旗標,這會導致 emscripten_fetch() 在 onsuccess() 回呼中完整填入記憶體中下載的檔案。當稍後要立即存取整個檔案時,這很方便,但是對於大型檔案,這在記憶體使用方面可能是一種浪費的策略。如果檔案非常大,它甚至可能不適合應用程式的堆積區域內。

以下子章節提供了以記憶體有效方式管理大型擷取的方法。

直接下載到 IndexedDB

如果應用程式想要下載檔案以供本機存取,但不需要立即使用檔案,例如在預先載入資料以供稍後存取時,最好完全避免使用 EMSCRIPTEN_FETCH_LOAD_TO_MEMORY 旗標,而只傳遞 EMSCRIPTEN_FETCH_PERSIST_FILE 旗標。這會使擷取將檔案直接下載到 IndexedDB,這樣可以避免在下載完成後暫時將檔案填入記憶體中。在這種情況下,onsuccess() 處理常式只會報告下載的檔案總大小,但不包含檔案的資料位元組。

串流下載

注意:這目前僅在 Firefox 中有效,因為它使用「moz-chunked-arraybuffer」。

如果應用程式不需要對檔案進行隨機搜尋存取,但能夠以串流方式處理檔案,則可以使用 EMSCRIPTEN_FETCH_STREAM_DATA 旗標,以便在下載檔案時串流檔案中的位元組。如果傳遞此旗標,則會以相干的檔案循序順序將下載的資料區塊傳遞到 onprogress() 回呼中。請參閱以下程式碼片段以取得範例。

void downloadProgress(emscripten_fetch_t *fetch) {
  printf("Downloading %s.. %.2f%%s complete. HTTP readyState: %d. HTTP status: %d.\n"
    "HTTP statusText: %s. Received chunk [%llu, %llu[\n",
    fetch->url, fetch->totalBytes > 0 ? (fetch->dataOffset + fetch->numBytes) * 100.0 / fetch->totalBytes : (fetch->dataOffset + fetch->numBytes),
    fetch->totalBytes > 0 ? "%" : " bytes",
    fetch->readyState, fetch->status, fetch->statusText,
    fetch->dataOffset, fetch->dataOffset + fetch->numBytes);

  // Process the partial data stream fetch->data[0] thru fetch->data[fetch->numBytes-1]
  // This buffer represents the file at offset fetch->dataOffset.
  for(size_t i = 0; i < fetch->numBytes; ++i)
    ; // Process fetch->data[i];
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_STREAM_DATA;
  attr.onsuccess = downloadSucceeded;
  attr.onprogress = downloadProgress;
  attr.onerror = downloadFailed;
  attr.timeoutMSecs = 2*60;
  emscripten_fetch(&attr, "myfile.dat");
}

在這種情況下,onsuccess() 處理常式根本不會收到最終檔案緩衝區,因此記憶體使用量將保持在最低限度。

位元組範圍下載

大型檔案也可以透過對它們執行位元組範圍下載來以較小的區塊進行管理。這會啟動 XHR 或 IndexedDB 傳輸,該傳輸僅擷取整個檔案的所需子範圍。這在例如大型套件檔案在特定搜尋偏移位置包含多個較小檔案時很有用,這些檔案可以單獨處理。

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
  printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
  // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
  printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  // Make a Range request to only fetch bytes 10 to 20
  const char* headers[] = {"Range", "bytes=10-20", NULL};
  attr.requestHeaders = headers;
  attr.onsuccess = downloadSucceeded;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

待辦文件

Emscripten_fetch() 也支援以下操作,這些操作需要記載

  • Emscripten_fetch 可用於透過 HTTP PUT 將檔案上傳到遠端伺服器

  • Emscripten_fetch_attr_t 允許設定自訂 HTTP 請求標頭 (例如,用於快取控制)

  • 在 Emscripten_fetch_attr_t 中記錄 HTTP 簡單驗證欄位。

  • 在 Emscripten_fetch_attr_t 中記錄 overriddenMimeType 屬性。

  • Emscripten_fetch_attr_t、Emscripten_fetch_t 和 #define 中個別欄位的參考文件。

  • 關於僅從 IndexedDB 載入而不執行 XHR 的範例。

  • 關於使用新的 XHR 覆寫 IndexedDB 中現有檔案的範例。

  • 關於如何將整個檔案系統預先載入到 IndexedDB 以便輕鬆取代 –preload-file 的範例。

  • 關於如何將內容以 Gzipped 格式保存到 IndexedDB 並在載入時解壓縮的範例。

  • 關於如何中止並恢復到 IndexedDB 的部分傳輸的範例。