2020 年 10 月 5 日

資源載入:onload 和 onerror

瀏覽器允許我們追蹤外部資源的載入,例如腳本、iframe、圖片等等。

有兩個事件可以做到這件事

  • onload – 載入成功
  • onerror – 發生錯誤

載入腳本

假設我們需要載入一個第三方腳本,並呼叫其中的一個函式。

我們可以動態載入,像這樣

let script = document.createElement('script');
script.src = "my.js";

document.head.append(script);

…但是如何執行在該腳本中宣告的函式?我們需要等到腳本載入,然後才能呼叫它。

請注意

對於我們自己的腳本,我們可以在這裡使用 JavaScript 模組,但第三方程式庫並未廣泛採用。

script.onload

主要的幫手是load事件。它會在腳本載入並執行後觸發。

例如

let script = document.createElement('script');

// can load any script, from any domain
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
document.head.append(script);

script.onload = function() {
  // the script creates a variable "_"
  alert( _.VERSION ); // shows library version
};

因此,我們可以在onload中使用腳本變數、執行函式等。

…如果載入失敗呢?例如,沒有該腳本(錯誤 404)或伺服器已關閉(無法使用)。

script.onerror

腳本載入期間發生的錯誤可以在error事件中追蹤。

例如,我們要求一個不存在的腳本

let script = document.createElement('script');
script.src = "https://example.com/404.js"; // no such script
document.head.append(script);

script.onerror = function() {
  alert("Error loading " + this.src); // Error loading https://example.com/404.js
};

請注意,我們無法在此取得 HTTP 錯誤詳細資料。我們不知道是錯誤 404、500 還是其他錯誤。只知道載入失敗。

重要

事件onload/onerror只追蹤載入本身。

腳本處理和執行期間可能發生的錯誤不在這些事件的範圍內。也就是說:如果腳本已成功載入,則會觸發onload,即使其中有程式設計錯誤。若要追蹤腳本錯誤,可以使用window.onerror全域處理常式。

其他資源

loaderror事件也適用於其他資源,基本上適用於任何具有外部src的資源。

例如

let img = document.createElement('img');
img.src = "https://js.cx/clipart/train.gif"; // (*)

img.onload = function() {
  alert(`Image loaded, size ${img.width}x${img.height}`);
};

img.onerror = function() {
  alert("Error occurred while loading image");
};

不過有一些注意事項

  • 大多數資源會在新增至文件時開始載入。但<img>是個例外。它會在取得 src 時開始載入(*)
  • 對於<iframe>iframe.onload事件會在 iframe 載入完成時觸發,無論是載入成功或發生錯誤。

這是基於歷史原因。

跨來源原則

有一個規則:來自一個網站的腳本無法存取另一個網站的內容。因此,例如,位於https://facebook.com的腳本無法讀取使用者在https://gmail.com的信箱。

或者更精確地說,一個來源(網域/埠/通訊協定的三元組)無法存取另一個來源的內容。因此,即使我們有子網域或只是另一個埠,這些也是不同的來源,彼此無法存取。

此規則也影響來自其他網域的資源。

如果我們使用來自其他網域的腳本,且其中有錯誤,我們無法取得錯誤詳細資料。

例如,我們採用由單一(錯誤)函式呼叫組成的腳本error.js

// 📁 error.js
noSuchFunction();

現在從它所在的位置載入它

<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="/article/onload-onerror/crossorigin/error.js"></script>

我們可以看到良好的錯誤報告,如下所示

Uncaught ReferenceError: noSuchFunction is not defined
https://javascriptinfo.dev.org.tw/article/onload-onerror/crossorigin/error.js, 1:1

現在讓我們從另一個網域載入相同的腳本

<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>

報告不同,如下所示

Script error.
, 0:0

詳細資料可能因瀏覽器而異,但概念相同:任何有關腳本內部的資訊,包括錯誤堆疊追蹤,都會被隱藏起來。正是因為它來自另一個網域。

我們為什麼需要錯誤詳細資料?

有許多服務(我們也可以建立自己的服務)會使用 window.onerror 偵聽全域錯誤,儲存錯誤並提供一個介面來存取和分析這些錯誤。這很好,因為我們可以看到由使用者觸發的實際錯誤。但如果指令碼來自其他來源,那麼就幾乎沒有關於其中錯誤的資訊,正如我們剛才看到的。

類似的跨來源政策 (CORS) 也會對其他類型的資源強制執行。

若要允許跨來源存取,<script> 標籤需要具有 crossorigin 屬性,而且遠端伺服器必須提供特殊標頭。

跨來源存取有三個層級

  1. 沒有 crossorigin 屬性 – 禁止存取。
  2. crossorigin="anonymous" – 如果伺服器使用標頭 Access-Control-Allow-Origin 回應 * 或我們的來源,則允許存取。瀏覽器不會將授權資訊和 Cookie 傳送至遠端伺服器。
  3. crossorigin="use-credentials" – 如果伺服器使用標頭 Access-Control-Allow-Origin 回應我們的來源和 Access-Control-Allow-Credentials: true,則允許存取。瀏覽器會將授權資訊和 Cookie 傳送至遠端伺服器。
請注意

您可以在章節 Fetch:跨來源要求 中進一步了解跨來源存取。它描述了用於網路要求的 fetch 方法,但政策完全相同。

「Cookie」這類東西超出了我們目前的範圍,但您可以在章節 Cookie、document.cookie 中進一步了解它們。

在我們的案例中,我們沒有任何 crossorigin 屬性。因此禁止跨來源存取。讓我們新增它。

我們可以在 "anonymous"(不傳送 Cookie,需要一個伺服器端標頭)和 "use-credentials"(也傳送 Cookie,需要兩個伺服器端標頭)之間進行選擇。

如果我們不在乎 Cookie,那麼 "anonymous" 是可行的方法

<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>

現在,假設伺服器提供 Access-Control-Allow-Origin 標頭,一切都很好。我們有完整的錯誤報告。

摘要

影像 <img>、外部樣式、指令碼和其他資源提供 loaderror 事件來追蹤它們的載入

  • load 會在載入成功時觸發,
  • error 會在載入失敗時觸發。

唯一的例外是 <iframe>:基於歷史原因,它總會觸發 load,以進行任何載入完成,即使找不到頁面。

readystatechange 事件也適用於資源,但很少使用,因為 load/error 事件較為簡單。

任務

重要性:4

通常,圖片會在建立時載入。因此,當我們將 <img> 加入頁面時,使用者不會立即看到圖片。瀏覽器需要先載入它。

若要立即顯示圖片,我們可以「預先」建立它,如下所示

let img = document.createElement('img');
img.src = 'my.jpg';

瀏覽器開始載入圖片並將其記在快取中。稍後,當同一個圖片出現在文件中(無論如何),它會立即顯示。

建立一個函式 preloadImages(sources, callback),它會載入陣列 sources 中的所有圖片,並在準備好時執行 callback

例如,這會在圖片載入後顯示一個 alert

function loaded() {
  alert("Images loaded")
}

preloadImages(["1.jpg", "2.jpg", "3.jpg"], loaded);

如果發生錯誤,函式仍應假設圖片已「載入」。

換句話說,當所有圖片都已載入或發生錯誤時,會執行 callback

例如,當我們計畫顯示一個包含許多可捲動圖片的圖庫,並希望確保所有圖片都已載入時,此函式很有用。

您可以在原始文件中找到測試圖片的連結,以及檢查它們是否已載入的程式碼。它應該輸出 300

開啟一個沙盒以執行此任務。

演算法

  1. 為每個來源建立 img
  2. 為每個圖片加入 onload/onerror
  3. onloadonerror 觸發時,增加計數器。
  4. 當計數器值等於來源數量時,我們完成了:callback()

在沙盒中開啟解決方案。

教學地圖

留言

留言前請先閱讀這段文字…
  • 如果您有改進建議,請 提交 GitHub 議題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 要插入少數幾個字元的程式碼,請使用 <code> 標籤,對於多行程式碼,請將它們包在 <pre> 標籤中,對於超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)