跳到主要内容

HTML 中的 JavaScript

将 JavaScript 引入网页,首先要解决它与网页的主导语言 HTML 的关系问题。在 JavaScript 早期, 网景公司的工作人员希望在将 JavaScript 引入 HTML 页面的同时,不会导致页面在其他浏览器中渲染出 问题。通过反复试错和讨论,他们最终做出了一些决定,并达成了向网页中引入通用脚本能力的共识。当初他们的很多工作得到了保留,并且最终形成了 HTML 规范。

<script> 元素

属性

<script> 元素在 HTML 中有多种属性,用于控制 JavaScript 脚本的加载和执行方式。以下是 <script> 元素的所有主要属性及其作用:

  • src: 指定外部 JavaScript 文件的URL。
  • type: 表示代码块中脚本语言的内容类型(也称 MIME 类型), 该属性的值可能为以下类型:
  • 属性未设置(默认),一个空字符串,或一个 JavaScript MIME 类型。
  • 如果这个值是 module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字。
  • importmap: 此值代表元素体内包含导入映射(importmap)表。导入映射表是一个 JSON 对象,开发者可以用它来控制浏览器在导入 JavaScript 模块时如何解析模块标识符。
  • async: 表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其 他脚本加载。只对外部脚本文件有效。

  • defer: 表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。

  • integrity: 提供加密签名,确保外部脚本在传输过程中没有被篡改。

  • crossorigin: 配置跨域请求的方式。常用值有 anonymoususe-credentials

  • nomodule: 这个布尔属性被设置来标明这个脚本不应该在支持 ES 模块的浏览器中执行。实际上,这可用于在不支持模块化 JavaScript 的旧浏览器中提供回退脚本。

  • nonce

  • referrerpolicy

在不使用 deferasync 属性的情况下,包含在 <script> 元素中的代码必须严格按次序解释。

另外,使用了 src 属性的 <script> 元素不应该再在 <script></script> 标签中再包含其他 JavaScript 代码。如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。

<script> 元素的一个最为强大、同时也备受争议的特性是,它可以包含来自外部域的 JavaScript 文件。跟 <img> 元素很像,<script> 元素的 src 属性可以是一个完整的 URL,而且这个 URL 指向的 4 资源可以跟包含它的 HTML 页面不在同一个域中,比如这个例子:

<script src="http://www.somewhere.com/afile.js"></script>
注意

按照惯例,外部 JavaScript 文件的扩展名是.js。这不是必需的,因为浏览器不会检 查所包含 JavaScript 文件的扩展名。这就为使用服务器端脚本语言动态生成 JavaScript 代 码,或者在浏览器中将 JavaScript 扩展语言(如 TypeScript,或 React 的 JSX)转译为 JavaScript 提供了可能性。不过要注意,服务器经常会根据文件扩展来确定响应的正确 MIME 类型。 如果不打算使用.js 扩展名,一定要确保服务器能返回正确的 MIME 类型。

标签位置

过去,所有 <script> 元素都被放在页面的 <head> 标签内,如下面的例子所示:

<!doctype html>
<html lang="en">
<head>
<title>Example HTML Page</title>
<script src="example1.js"></script>
<script src="example2.js"></script>
</head>

<body>
<!-- 这里是页面内容 -->
</body>
</html>

这种做法的主要目的是把外部的 CSS 和 JavaScript 文件都集中放到一起。不过,把所有 JavaScript 文件都放在<head>里,也就意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲 染页面(页面在浏览器解析到<body>的起始标签时开始渲染)。对于需要很多 JavaScript 的页面,这会 导致页面渲染的明显延迟,在此期间浏览器窗口完全空白。为解决这个问题,现代 Web 应用程序通常 将所有 JavaScript 引用放在<body>元素中的页面内容后面,如下面的例子所示:

<!doctype html>
<html lang="en">
<head>
<title>Example HTML Page</title>
</head>

<body>
<!-- 这里是页面内容 -->
<script src="example1.js"></script>
<script src="example2.js"></script>
</body>
</html>

推迟执行脚本

HTML 4.01 为 <script> 元素定义了一个叫 defer 的属性。这个属性表示脚本在执行的时候不会改变页面的结构。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在 <script> 元素上 设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。

<!doctype html>
<html lang="en">
<head>
<title>Example HTML Page</title>
<script defer src="example1.js"></script>
<script defer src="example2.js"></script>
</head>

<body>
<!-- 这里是页面内容 -->
</body>
</html>

虽然这个例子中的 <script> 元素包含在页面的 <head> 中,但它们会在浏览器解析到结束的 </html> 标签后才会执行。HTML5 规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚 本会在第二个推迟的脚本之前执行,而且两者都会在 DOMContentLoaded 事件之前执行。不过在实际当中,推迟执行的脚本不一定总会按顺序执行或者在 DOMContentLoaded 事件之前执行,因此最好只包含一个这样的脚本。

异步执行脚本

HTML5 为 <script>元素定义了 async 属性。从改变脚本处理方式上看,async 属性与 defer 类似。当然,它们两者也都只适用于外部脚本,都会告诉浏览器立即开始下载。不过,与 defer 不同的 是,标记为 async 的脚本并不保证能按照它们出现的次序执行,比如:

<!doctype html>
<html lang="en">
<head>
<title>Example HTML Page</title>
<script async src="example1.js"></script>
<script async src="example2.js"></script>
</head>

<body>
<!-- 这里是页面内容 -->
</body>
</html>

在这个例子中,第二个脚本可能先于第一个脚本执行。因此,重点在于它们之间没有依赖关系。给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到 该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改 DOM。

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

总结

asyncdefer 是现代网页开发中常用的优化手段,可以帮助开发者更好地管理脚本加载和执行顺序。在实际项目中,根据脚本的依赖关系和执行需求,合理使用这两个属性,可以显著提升网页性能和用户体验。理解和应用这些优化技巧,是前端开发的重要技能之一。

动态加载脚本

除了 <script> 标签,还有其他方式可以加载脚本。因为 JavaScript 可以使用 DOM API,所以通过 向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可。

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

当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下, 以这种方式创建的 <script> 元素是以异步方式加载的,相当于添加了 async 属性。不过这样做可能会 有问题,因为所有浏览器都支持 createElement()方法,但不是所有浏览器都支持 async 属性。因此, 11 如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:

script.async = false;

以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先 级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些 动态请求文件的存在,可以在文档头部显式声明它们:

<link rel="preload" href="gibberish.js" />

<noscript> 元素

针对早期浏览器不支持 JavaScript 的问题,需要一个页面优雅降级的处理方案。最终,<noscript> 元素出现,被用于给不支持 JavaScript 的浏览器提供替代内容。虽然如今的浏览器已经 100%支持 JavaScript,但对于禁用 JavaScript 的浏览器来说,这个元素仍然有它的用处。

<noscript> 元素可以包含任何可以出现在 <body> 中的 HTML 元素,<script> 除外。在下列两种 情况下,浏览器将显示包含在 <noscript> 中的内容:

  • 浏览器不支持脚本;
  • 浏览器对脚本的支持被关闭

任何一个条件被满足,包含在 <noscript> 中的内容就会被渲染。否则,浏览器不会渲染 <noscript> 中的内容。

扩展

importmap

新语法

自 2023 年 3 月起,此功能适用于最新设备和浏览器版本。此功能可能无法在较旧的设备或浏览器中使用。

语法

<script> 元素的 type 属性 importmap 值表示元素的主体包含一个导入映射。

导入映射(import map)是一个 JSON 对象,它允许在导入的JavaScript 模块时,控制浏览器如何解析模块安全。它在import语句或import()表达式中用作模块安全的文本,会在解析模块安全时与要替换的文本之间建立映射。JSON 对象必须符合导入映射 JSON 表示格式。

导入映射用于解析静态和动态导入模块的资源,因此必须在使用映射表中声明的资源模块的任何 <script> 元素之前声明和处理。注意,导入映射仅适用于import语句或import()真理中的模块资源;它不适用于 <script> 元素的src属性中指定的路径,也不适用于到 worker 或 worklet 中的模块。

语法格式
<script type="importmap">// 定义导入的 JSON 对象</script>

不得指定 srcasyncnomoduledefercrossoriginintegrityreferrerpolicy 属性。

仅处理文档中具有内联定义的第一个导入映射;忽略任何额外的导入映射和外部导入映射。

描述

当导入 JavaScript 模块时,import 语句和 import() 运算符都有一个“模块标识符”,其指示要导入的模块。浏览器必须能够将此标识符解析为绝对路径,才能导入模块。

例如,以下语句从模块标识符 "./modules/shapes/square.js" 导入元素,其是一个相对于文档的基础 URL 路径,而模块标识符 "https://example.com/shapes/circle.js" 是一个绝对路径。

不使用 importmap 示例
import { name as squareName, draw } from './modules/shapes/square.js';
import { name as circleName } from 'https://example.com/shapes/circle.js';
使用 importmap 示例
<script type="importmap">
{
"imports": {
"square": "./module/shapes/square.js",
"circle": "https://example.com/shapes/circle.js"
}
}
</script>

使用导入映射,我们可以导入以上相同的模块,但在我们的模块标识符要使用“裸模块(bare module)”。

import { name as squareName, draw } from 'square';
import { name as circleName } from 'circle';

映射路径前缀

模块标识符映射键也可用于重新映射模块标识符中的路径前缀。请注意,在这种情况下,属性和映射路径都必须有一个尾随的正斜杠(/)。

<script type="importmap">
{
"imports": {
"shapes/": "./module/shapes/",
"othershapes/": "https://example.com/modules/shapes/"
}
}
</script>

然后我们可以这样导入 circle 模块。

import { name as squareName, draw } from 'shapes/circle.js';