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://javascript.js.cn/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 方法,但策略完全相同。

诸如“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…)