JavaScript 基础(一):HTML 中的 JavaScript(页面生命周期、脚本加载、跨源资源共享)

406 阅读7分钟

1、页面生命周期

DOMContentLoaded

  • DOMContentLoaded —— 浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成

    • 发生在 document 对象上,必须使用 addEventListener 来捕获:

      document.addEventListener("DOMContentLoaded", ready);
      
    • 可以将 JavaScript 应用于元素,例如查找 DOM 节点。

    • 诸如 <script>...</script> 之类的脚本会阻塞 DOMContentLoaded,浏览器将等待它们执行结束

    <script>
      document.addEventListener("DOMContentLoaded", () => {
        alert("DOM ready!");
      });
    </script>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
    
    <script>
      alert("Library loaded, inline script executed");
    </script>
    
    // 我们会先看到 “Library loaded…”,然后才会看到 “DOM ready!”(所有脚本都已经执行结束)
    

    不会阻塞 DOMContentLoaded 的脚本

    此规则有两个例外:

    1. 具有 async 特性(attribute)的脚本不会阻塞 DOMContentLoaded
    2. 使用 document.createElement('script') 动态生成并添加到网页的脚本也不会阻塞 DOMContentLoaded
    • 外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。但如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成。当 DOMContentLoaded 等待脚本时,它现在也在等待脚本前面的样式
    <link type="text/css" rel="stylesheet" href="style.css">
    <script>
      // 在样式表加载完成之前,脚本都不会执行
      alert(getComputedStyle(document.body).marginTop);
    </script>
    
    • Firefox,Chrome 和 Opera 都会在 DOMContentLoaded 中自动填充表单

load

  • load —— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。

    <script>
      window.onload = function() { // 与此相同 window.addEventListener('load', (event) => {
        alert('Page loaded');
    
        // 此时图片已经加载完成
        alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
      };
    </script>
    
    <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
    
  • beforeunload 事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开

    你可以通过运行下面这段代码,然后重新加载页面来进行尝试:

    window.onbeforeunload = function() {
      return false;
    };
    

    image.png

    由于历史原因,返回非空字符串也被视为取消事件。在以前,浏览器曾经将其显示为消息,但是根据 现代规范 所述,它们不应该这样。

    window.onbeforeunload = function() {
      return "There are unsaved changes. Leave now?";
    };
    

    它的行为已经改变了,因为有些站长通过显示误导性和恶意信息滥用了此事件处理程序。所以,目前一些旧的浏览器可能仍将其显示为消息,但除此之外 —— 无法自定义显示给用户的消息。

unload

  • unload 事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作(只能执行不涉及延迟或询问用户的简单操作),例如发送统计数据

    • 我们可以使用 navigator.sendBeacon 来发送网络请求。详见规范 w3c.github.io/beacon/。它会在后台发送数据,转换到另外一个页面不会有延迟:浏览器离开页面,但仍然在执行 sendBeacon
    let analyticsData = { /* 带有收集的数据的对象 */ };
    
    window.addEventListener("unload", function() {
      navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
    });
    
    • 请求以 POST 方式发送。
    • 我们不仅能发送字符串,还能发送表单以及其他格式的数据,但通常它是一个字符串化的对象
    • 数据大小限制在 64kb

    当 sendBeacon 请求完成时,浏览器可能已经离开了文档,所以就无法获取服务器响应(对于分析数据来说通常为空)。

document.readyState

  • document.readyState 属性可以为我们提供当前文档的加载状态

    • loading —— 文档正在被加载。

    • interactive —— 文档被全部读取

      • 与 DOMContentLoaded 几乎同时发生,但是在 DOMContentLoaded 之前发生。
    • complete —— 文档被全部读取,并且所有资源(例如图片等)都已加载完成

      • 与 window.onload 几乎同时发生,但是在 window.onload 之前发生。
    • 可以在 readystatechange 事件跟踪状态更改

    // 状态改变时打印它
    document.addEventListener('readystatechange', () => console.log(document.readyState));
    

让我们看看完整的事件流。

<script>
  log('initial readyState:' + document.readyState);

  document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
  document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));

  window.onload = () => log('window onload');
</script>

<iframe src="iframe.html" onload="log('iframe onload')"></iframe>

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/451f6107a29f49f29dac999cea336d1b~tplv-k3u1fbpfcp-zoom-1.image" id="img">
<script>
  img.onload = () => log('img onload');
</script>

典型输出:

  1. [1] initial readyState:loading
  2. [2] readyState:interactive
  3. [2] DOMContentLoaded
  4. [3] iframe onload
  5. [4] img onload
  6. [4] readyState:complete
  7. [4] window onload

方括号中的数字表示发生这种情况的大致时间。标有相同数字的事件几乎是同时发生的(± 几毫秒)。

2、<script>元素

<script>元素的属性

<script>元素有下列 8 个属性:

  • src:可选。表示包含要执行的代码的外部文件。

    • 如果设置了 src 特性,script 标签的内部代码将会被忽略
  • language:废弃。最初用于表示代码块中的脚本语言(如 "JavaScript" 、"JavaScript 1.2" 或 "VBScript")。大多数浏览器都会忽略这个属性,不应该再使用它。

  • charset:可选。使用 src 属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不在乎它的值。

  • type:可选。代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。按照惯例,这个值始终都是"text/javascript"

    • 如果这个值是 type = "module",则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 importexport 关键字。

async

  • async:可选。表示脚本会在“后台”下载,并在加载就绪时执行(加载优先顺序)

    • 只对外部脚本文件有效

    • async 脚本不会等待其他脚本(不保证能按照它们出现的次序执行)。

    • 异步脚本保证会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 前或之后。

    <head> 
        <!-- 第二个脚本可能先于第一个脚本执行 --> 
        <script async src="example1.js"></script> 
        <script async src="example2.js"></script> 
    </head> 
    

defer

  • defer:可选。表示脚本会在“后台”下载,然后等 DOM 构建完成后,脚本才会执行。。
    • 只对外部脚本文件有效

    • 一个页面有多个 defer,按出现的顺序执行

    • 在 DOMContentLoaded 事件之前执行

      • 在实际当中,推迟执行的脚本不一定总会按顺序执行或者在 DOMContentLoaded 事件之前执行(例如脚本包含定时器),因此最好只包含一个这样的脚本。
    <head> 
        <!-- 第一个脚本执行之后再执行第二个脚本 --> 
        <script defer src="example1.js"></script> 
        <script defer src="example2.js"></script> 
    </head> 
    

crossorigin 跨源

  • crossorigin:可选。配置相关请求的CORS(跨源资源共享) 设置。默认不使用CORS。
    • 一个源(域/端口/协议三者)无法获取另一个源(origin)的内容

    • 要允许跨源访问,<script> 标签需要具有 crossorigin 特性(attribute),并且远程服务器必须提供特殊的 header。这里有三个级别的跨源访问:

      • 无 crossorigin 特性禁止访问

      • crossorigin="anonymous" 配置文件请求不必设置凭据标志(不会发送 cookie,需要一个服务器端的 header)。

        • 如果服务器的响应带有包含 * 或我们的源(origin)的 header Access-Control-Allow-Origin,则允许访问。

        • 浏览器不会将授权信息和 cookie 发送到远程服务器

      • crossorigin="use-credentials" 设置凭据标志,意味着出站请求会包含凭据(会发送 cookie,需要两个服务器端的 header)。

        • 如果服务器发送回带有我们的源的 header Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials: true,则允许访问。

        • 浏览器会将授权信息和 cookie 发送到远程服务器

    <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` header,则可以正常访问。 -->
    

integrity

  • integrity:可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource Integrity)。
    • 如果接收到的资源的签名与这个属性指定的签名 不匹配,则页面会报错,脚本不会执行
    • 这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。

默认执行顺序

现代的网站中,脚本往往比 HTML 更“重”:它们的大小通常更大,处理时间也更长。

当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script> 也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。

这会导致两个重要的问题:

  1. 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
  2. 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在脚本下载并执行结束前,用户都不能看到页面内,导致页面渲染的明显延迟,在此期间浏览器窗口完全空白

为解决这个问题,现代 Web 应用程序通常将所有 JavaScript 引用放在<body>元素中的最后,如下面的例子所示:

<!DOCTYPE html> 
<html> 
    <head> 
        <title>Example HTML Page</title> 
    </head> 
    <body> 
        <!-- 这里是页面内容 --> 
        <script src="example1.js"></script> 
        <script src="example2.js"></script> 
    </body> 
</html> 

这样一来,页面会在处理 JavaScript 代码之前完全渲染页面。用户会感觉页面加载更快了,因为浏览器显示空白页面的时间短了。

但是这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档之后才会注意到该脚本(并且可以开始下载它)。对于长的 HTML 文档来说,这样可能会造成明显的延迟

这对于使用高速连接的人来说,这不值一提,他们不会感受到这种延迟。但是这个世界上仍然有很多地区的人们所使用的网络速度很慢,并且使用的是远非完美的移动互联网连接。

幸运的是,defer 和 async 可以为我们解决这个问题。

动态加载脚本

通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可。

let script = document.createElement('script'); 
script.src = 'gibberish.js'; 
document.head.appendChild(script); 

当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的<script>元素是以异步方式加载的,相当于添加了 async 属性

可以设置 script.async = false,脚本将按照脚本在文档中的顺序执行,就像 defer 那样

let script = document.createElement('script'); 
script.src = 'gibberish.js'; 
script.async = false; 
document.head.appendChild(script);

以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先级

要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:<link rel="preload" href="gibberish.js">

行内代码与外部文件

虽然可以直接在 HTML 文件中嵌入 JavaScript 代码,但通常认为最佳实践是尽可能将 JavaScript 代码放在外部文件中。不过这个最佳实践并不是明确的强制性规则。推荐使用外部文件的理由如下。

  • 可维护性。JavaScript 代码如果分散到很多 HTML 页面,会导致维护困难。而用一个目录保存所有 JavaScript 文件,则更容易维护。

  • 缓存浏览器会根据特定的设置(下载)缓存所有外部链接的 JavaScript 文件,之后,其他页面想要相同的脚本就会从缓存中获取。这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快

  • 适应未来。通过把 JavaScript 放到外部文件中,就不必考虑行内<script> 代码不能用于 XHTML ,必须通过一些注释或<script type="text/javascript"><![CDATA[ ... ]]></script>代码块来解决问题。

3、<noscript>元素

<noscript>元素,给不支持 JavaScript 的浏览器提供替代内容。

<noscript>元素可以包含任何可以出现在<body>中的 HTML 元素,<script>除外。

在下列两种情况下,浏览器将显示包含在<noscript>中的内容:

  • 浏览器不支持脚本
  • 浏览器对脚本的支持被关闭任何一个条件被满足,包含在<noscript>中的内容就会被渲染。否则,浏览器不会渲染<noscript>中的内容。
<!DOCTYPE html> 
<html> 
    <head> 
        <title>Example HTML Page</title> 
        <script defer="defer" src="example1.js"></script> 
        <script defer="defer" src="example2.js"></script> 
    </head> 
    
    <body> 
        <noscript> 
            <p>This page requires a JavaScript-enabled browser.</p> 
        </noscript> 
    </body> 
</html> 

这个例子是在脚本不可用时让浏览器显示一段话。如果浏览器支持脚本,则用户永远不会看到它。

4、资源加载

浏览器允许我们跟踪外部资源的加载 —— 脚本,iframe,图片等。

这里有两个事件:

  • onload —— 在脚本加载并执行完成时触发。

    • 在 onload 中我们可以使用脚本中的变量,运行函数等
    let script = document.createElement('script');
    
    // 可以从任意域(domain),加载任意脚本
    script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
    document.head.append(script);
    
    script.onload = function() {
      // 该脚本创建了一个变量 "_"
      alert( _.VERSION ); // 显示库的版本
    }; 
    
  • onerror —— 出现 error。

    • 无法获取更多 HTTP error 的详细信息。
    let script = document.createElement('script');
    script.src = "https://example.com/404.js"; // 没有这个脚本
    document.head.append(script);
    
    script.onerror = function() {
      alert("Error loading " + this.src); // Error loading https://example.com/404.js
    }; 
    

onload/onerror 事件仅跟踪加载本身

在脚本处理和执行期间可能发生的 error 超出了这些事件跟踪的范围。也就是说:如果脚本成功加载,则即使脚本中有编程 error,也会触发 onload 事件

如果要跟踪脚本 error,可以使用 window.onerror 全局处理程序。

load 和 error 事件也适用于其他资源,基本上(basically)适用于具有外部 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 事件,无论是成功加载还是出现 error

这是出于历史原因。