JavaScript引入
参考:JavaScript高级程序设计(第4版)
使用 script 的方式有两种:通过它直接在网页中嵌入 JavaScript 代码,以及通过它在网页中包含外部 JavaScript 文件。
这里要讨论的是第二种方式: 外部引入
# html 中的 script 元素
我们一般这样引入 JavaScript
<script src="example.js"></script>
注意 按照惯例,外部 JavaScript 文件的扩展名是.js。这不是必需的,因为浏览器不会检查所包含 JavaScript 文件的扩展名。这就为使用服务器端脚本语言动态生成 JavaScript 代码,或者在浏览器中将 JavaScript 扩展语言(如 TypeScript,或 React 的 JSX)转译为 JavaScript 提供了可能性。不过要注意,服务器经常会根据文件扩展来确定响应的正确 MIME 类型。如果不打算使用.js 扩展名,一定要确保服务器能返回正确的 MIME 类型。
另外,使用了 src 属性的 script 元素不应该再在 script 和标签中再包含其他 JavaScript 代码。
如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。
script 可以引入外部域的 JavaScript 文件
script 元素的一个最为强大、同时也备受争议的特性是,它可以包含来自外部域的 JavaScript 文件。
跟 img 元素很像,script 元素的 src 属性可以是一个完整的 URL,而且这个 URL 指向的资源可以跟包含它的 HTML 页面不在同一个域中,比如这个例子:
<script src="http://www.somewhere.com/afile.js"></script>
浏览器在解析这个资源时,会向 src 属性指定的路径发送一个 GET 请求,以取得相应资源,假定是一个 JavaScript 文件。
这个初始的请求不受浏览器同源策略限制,但返回并被执行的 JavaScript 则受限制。当然,这个请求仍然受父页面 HTTP/HTTPS 协议的限制。
来自外部域的代码会被当成加载它的页面的一部分来加载和解释。这个能力可以让我们通过不同的域分发 JavaScript。
注意:引用了放在别人服务器上的 JavaScript 文件时要格外小心,因为恶意的程序员随时可能替换这个文件。在包含外部域的 JavaScript 文件时,要确保该域是自己所有的,或者该域是一个可信的来源。
script 标签的 integrity 属性是防范这种问题的一个武器,但这个属性也不是所有浏览器都支持。
script 执行顺序
不管包含的是什么代码,浏览器都会按照 script 在页面中出现的顺序依次解释它们,前提是它们没有使用 defer 和 async 属性。第二个 script 元素的代码必须在第一个 script 元素的代码解
释完毕才能开始解释,第三个则必须等第二个解释完,以此类推。
# script 标签位置
过去,所有 script 元素都被放在页面的 head 标签内,如下面的例子所示:
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script src="example1.js"></script>
<script src="example2.js"></script>
</head>
<body>
<!-- 这里是页面内容 -->
</body>
</html>
2
3
4
5
6
7
8
9
10
11
这种做法的主要目的是把外部的 CSS 和 JavaScript 文件都集中放到一起。不过,把所有 JavaScript 文件都放在 head 里,也就意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面(页面在浏览器解析到 body 的起始标签时开始渲染)。
对于需要很多 JavaScript 的页面,这会导致页面渲染的明显延迟,在此期间浏览器窗口完全空白。
为解决这个问题,现代 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>
2
3
4
5
6
7
8
9
10
11
这样一来,页面会在处理 JavaScript 代码之前完全渲染页面。用户会感觉页面加载更快了,因为浏览器显示空白页面的时间短了。
# defer 与 async
defer 与 async 都可以用来优化 script 的加载过程
# defer 立即下载 延迟执行脚本
HTML 4.01 为 script 元素定义了一个叫 defer 的属性。这个属性表示脚本在执行的时候不会改变页面的结构。
也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在 script 元素上设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script defer src="example1.js"></script>
<script defer src="example2.js"></script>
</head>
<body>
<!-- 这里是页面内容 -->
</body>
</html>
2
3
4
5
6
7
8
9
10
11
虽然这个例子中的 script 元素包含在页面的 head 中,但它们会在浏览器解析到结束的
html 标签后才会执行。HTML5 规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚本会在第二个推迟的脚本之前执行,而且两者都会在 DOMContentLoaded 事件之前执行。不过在实际当中,推迟执行的脚本不一定总会按顺序执行或者在 DOMContentLoaded 事件之前执行,因此最好只包含一个这样的脚本。
# async 异步执行脚本
HTML5 为script元素定义了 async 属性。从改变脚本处理方式上看,async 属性与 defer 类似。
当然,它们两者也都只适用于外部脚本,都会告诉浏览器立即开始下载。
不过,与 defer 不同的是,标记为 async 的脚本并不保证能按照它们出现的次序执行,比如:
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script async src="example1.js"></script>
<script async src="example2.js"></script>
</head>
<body>
<!-- 这里是页面内容 -->
</body>
</html>
2
3
4
5
6
7
8
9
10
11
在这个例子中,第二个脚本可能先于第一个脚本执行。
因此,重点在于它们之间没有依赖关系。给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改 DOM。