将JavaScript引入web页面立即与web的主流语言HTML相冲突。作为JavaScript最初工作的一部分,Netscape试图弄清楚如何使JavaScript在HTML页面中共存而不破坏这些页面在其他浏览器中的呈现。通过尝试、错误和争议,最终做出了几个决定,并就将通用脚本支持引入web达成了一致。在web早期所做的许多工作都保留了下来,并在HTML规范中得到了形式化的描述。

# <script>元素

将JavaScript插入HTML页面的主要方法是通过<script>元素。此元素由Netscape创建,在Netscape Navigator 2中首次实现。它后来被添加到正式的HTML规范中。<script>元素有6个属性:

  • async— 可选。指示脚本应立即开始下载,但不应阻止页面上的其他操作,如下载资源或等待加载其他脚本。仅对外部脚本文件有效。
  • charset— 可选。使用src属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不支持它的值。
  • crossorigin - 可选。为相关的请求配置CORS设置;默认情况下,根本不使用CORS。crossorigin="anonymous"将配置文件请求不设置凭证标志。crossorigin="use-credentials"将设置凭证标志,这意味着发出的请求将包括凭证。
  • defer — 可选。指示脚本的执行可以安全地推迟到完全解析和显示文档内容之后。仅对外部脚本有效。Internet Explorer 7和更早版本也支持内联脚本。
  • integrity - 可选 允许通过根据提供的加密签名检查检索到的资源来验证子资源完整性(SRI)。如果检索到的资源的签名与此属性指定的签名不匹配,则页面将出错,脚本将不会执行。这有助于确保内容交付网络(CDN)不为恶意负载提供服务
  • language - 弃用 最初表示代码块使用的脚本语言(如“JavaScript”、“JavaScript1.2”或“VBScript”)。大多数浏览器都会忽略这个属性;它不应该被使用
  • scr - 可选 表示包含要执行代码的外部文件。
  • type - 可选 替换语言;指示代码块使用的脚本语言的内容类型(也称为MIME类型)。传统上,这个值一直是“text/javascript”,尽管“text/javascript”和“text/ecmascript”都被弃用。JavaScript文件通常使用“application/x-JavaScript”MIME类型,即使在type属性中设置此值可能会导致忽略脚本。在非Internet Explorer浏览器中工作的其他值是“application/javascript”和“application/ecmascript”。如果值为module,则代码被视为ES6模块,只有这样才有资格使用导入和导出关键字。

使用<script>元素有两种方法:直接将JavaScript代码嵌入页面或包含外部文件中的JavaScript。

要包含内联JavaScript代码,请将JavaScript代码直接放在<script>元素中,如下所示:

<script>
  function sayHi() {
    console.log("Hi!");
  }
</script>

<script>元素中包含的JavaScript代码从上到下进行解释。在本例中,函数定义被解释并存储在解释器环境中。在对<script>元素中的所有代码求值之后,才会加载和/或显示页面内容的其余部分。

在使用内联JavaScript代码时,请记住不能在代码中的任何位置使用字符串“”。例如,以下代码在加载到浏览器中时会导致错误:

<script>
  function sayScript() {
    console.log("</script>");
} 
</script>

由于内联脚本的解析方式,浏览器会将字符串“</script>”视为结束标记。通过转义“/”字符可以很容易地避免此问题,如以下示例所示:

<script>
  function sayScript() {
    console.log("<\/script>");
  }
</script>

对这段代码的修改使其可被浏览器接受,并且不会导致任何错误。

要包含来自外部文件的JavaScript,需要src属性。src的值是一个链接到包含JavaScript代码的文件的URL,如下所示:

<script src="example.js"></script>

在本例中,一个名为example.js的外部文件被加载到页面中。文件本身只需要包含在开始<script>和结束标记之间发生的JavaScript代码。与内联JavaScript代码一样,在解释外部文件时,将停止对页面的处理。(下载文件也需要一些时间。)在XHTML文档中,可以省略结束标记,如下例所示:

<script src="example.js"/>

这种语法不应该在HTML文档中使用,因为它是无效的HTML,某些浏览器(尤其是Internet Explorer)不能正确地处理它。

按照惯例,外部JavaScript文件的扩展名为.js。这不是必需的,因为浏览器不检查包含的JavaScript文件的文件扩展名。这样就可以使用服务器端脚本语言动态生成JavaScript代码,或者从TypeScript或React的JSX这样的JavaScript扩展语言在浏览器中转换成JavaScript。不过,请记住,服务器通常使用文件扩展名来确定应用于响应的正确MIME类型。如果不使用.js扩展,请仔细检查服务器是否返回了正确的MIME类型。

需要注意的是,使用src属性的<script>元素不应该在<script></script>标记之间包含额外的JavaScript代码。如果两者都提供了,脚本文件将被下载并执行,而内联代码将被忽略。

<script>元素最强大和最有争议的部分之一是它能够包含来自外部域的JavaScript文件。与<img>元素非常相似,<script>元素的src属性可以设置为HTML页面所在域之外的完整URL,如下例所示:

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

当浏览器解析此资源时,它将向src属性中指定的路径发送一个GET请求,以检索资源—假设是一个JavaScript文件。此初始请求不受浏览器的跨域限制,但返回和执行的任何JavaScript都受此限制。当然,这个请求仍然受制于父页面的HTTP/HTTPS协议。

外部域的代码将被加载并解释为加载它的页面的一部分。如果需要,此功能允许您从各个域提供JavaScript。但是,如果要引用位于无法控制的服务器上的JavaScript文件,则需要小心。恶意的程序员可以在任何时候替换文件。当包含来自不同域的JavaScript文件时,请确保您是域所有者,或者域由受信任的源拥有。<script>标签的完整性属性为您提供了一个工具来抵御这种攻击;然而,它只有有限的浏览器支持。

不管代码是如何包含的,只要没有出现defer和async属性,就按照它们在页面中出现的顺序解释<script>元素。第一个<script>元素的代码必须在第二个<script>元素开始解释之前完成,第二个必须在第三个元素之前完成,依此类推。

# 标记位置(Tag Placement)

传统上,所有<script>元素都放在页面的<head>元素中,如下例所示:

<!DOCTYPE html>
<html>
<head>
  <title>Example HTML Page</title>
  <script src="example1.js"></script>
  <script src="example2.js"></script>
</head>
<body>
<!-- content here -->
</body>
</html>

这种格式的主要目的是将外部文件引用(CSS文件和JavaScript文件)保存在同一区域中。但是,在文档的<head>中包含所有JavaScript文件意味着必须在页面开始呈现之前下载、解析和解释所有JavaScript代码(当浏览器收到打开的<body>标记时,开始渲染)。对于需要大量JavaScript代码的页面,这可能会导致页面呈现出现明显的延迟,在此期间浏览器将完全为空。因此,现代web应用程序通常在页面内容之后的<body>元素中包含所有JavaScript引用,如本例所示:

<!DOCTYPE html>
<html>
<head>
  <title>Example HTML Page</title></head>
<body>
<!-- content here -->
<script src="example1.js"></script>
<script src="example2.js"></script>
</body>
</html>

使用这种方法,在处理JavaScript代码之前,页面完全在浏览器中呈现。用户体验会更快,因为花在空白浏览器窗口上的时间减少了。

# 延迟脚本(Deferred Scripts)

HTML 4.01为<script>元素定义了一个名为defer的属性。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>
<!-- content here -->
</body>
</html>

即使本例中的<script>元素包含在文档<head>中,它们也要在浏览器收到关闭的</html>标记之后才会被执行。HTML5规范指出,脚本将按照它们出现的顺序执行,因此第一个延迟的脚本将在第二个延迟的脚本之前执行,两个脚本都将在DOMContentLoaded事件之前执行(有关更多信息,请参阅“事件”一章)。但实际上,延迟脚本并不总是按顺序或在DOMContentLoaded事件之前执行,所以最好尽可能只包含一个。

如前所述,defer属性仅支持外部脚本文件。这是HTML5中的一个澄清,因此当在内联脚本上设置时,支持HTML5实现的浏览器将忽略defer。InternetExplorer4-7都展示了旧的行为,而InternetExplorer8及更高版本支持HTML5行为。

从Internet Explorer 4、Firefox 3.5、Safari 5和Chrome 7开始添加了对defer属性的支持。所有其他浏览器都会忽略此属性,并将脚本视为正常脚本。因此,最好还是将延迟脚本放在页面底部。

对于XHTML文档,将defer属性指定为defer=“defer”。

# 异步脚本(Asynchronous Scripts)

HTML5为<script>元素引入了async属性。async属性类似于defer,因为它改变了脚本的处理方式。同样类似于defer,async只应用于外部脚本,并通知浏览器立即开始下载文件。与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>
<!-- content here -->
</body>
</html>

在此代码中,第二个脚本文件可能会在第一个脚本文件之前执行,因此重要的是,两者之间应没有依赖关系。 指定异步脚本的目的是指示该页面在继续加载之前无需等待脚本下载并执行,并且在执行相同操作之前也无需等待其他脚本加载并执行。 因此,建议异步脚本在加载时不要修改DOM。

异步脚本保证在页面加载事件之前执行,并且可以在DOMContentLoaded之前或之后执行(有关详细信息,请参阅“事件”一章)。Firefox3.6、Safari5和Chrome7支持异步脚本。使用异步脚本也会给页面带来一个隐含的假设,即您不打算使用document.write,但是良好的web开发实践要求您无论如何都不应该使用它。

# 动态脚本加载(Dynamic Script Loading)

您不受限于使用静态<script>标记来检索资源。因为JavaScript能够使用DOM API,所以非常欢迎您添加脚本元素,这些元素将依次加载它们指定的资源。这可以通过创建脚本元素并将它们附加到DOM来实现:

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

当然,在HTMLElement附加到DOM之前不会生成此请求,因此在脚本本身运行之前也不会生成此请求。默认情况下,以这种方式创建的脚本是异步的。但是,这可能有问题,因为所有浏览器都支持createElement,但并非所有浏览器都支持异步脚本请求。因此,要统一动态脚本加载行为,可以显式地将标记标记标记为同步:

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

以这种方式获取的资源将对浏览器预加载程序隐藏。这将严重损害它们在资源获取队列中的优先级。根据应用程序的工作方式和使用方式,这可能会严重损害性能。要通知预加载程序这些动态请求文件的存在,可以在文档头中显式声明它们:

<link rel="subresource" href="gibberish.js">

# XHTML的变化(Changes in XHTML)

可扩展超文本标记语言(Extensible HyperText Markup Language,简称XHTML)是HTML作为XML应用程序的重新定义。在HTML中,使用JavaScript时不需要type属性,而在XHTML中,<script>元素要求将type属性指定为text/JavaScript。

用XHTML编写代码的规则比HTML更严格,这在使用嵌入的JavaScript代码时影响了<script>元素。下面的代码块在HTML中是有效的,但在XHTML中是无效的:

<script type="text/javascript">
  function compare(a, b) {
    if (a < b) {
      console.log("A is less than B");
    } else if (a > b) {
      console.log("A is greater than B");
    } else {
      console.log("A is equal to B");
    }
  }
</script>

在HTML中,<script>元素有特殊的规则来管理它的内容如何被解析;在XHTML中,这些特殊规则不适用。这意味着语句a < b中的小于号(<)被解释为标记的开头,这将导致语法错误,因为小于号后面不能跟空格。

有两种方法可以修复XHTML语法错误。第一种方法是将所有出现的小于号(<)替换为它的HTML实体(<)。结果的代码是这样的:



 









<script type="text/javascript">
  function compare(a, b) {
    if (a &lt; b) {
      console.log("A is less than B");
    } else if (a > b) {
      console.log("A is greater than B");
    } else {
      console.log("A is equal to B");
    }
  }
</script>

这段代码现在将在XHTML页面中运行;但是,代码的可读性稍差。幸运的是,还有另一种方法。

将此代码转换为有效的XHTML版本的第二个选项是将JavaScript代码封装在CDATA部分中。在XHTML(和XML)中,CDATA部分用于指示文档中包含不打算解析的自由格式文本的区域。这使您可以使用任何字符,包括小于号,而不会导致语法错误。格式如下:

 











<script type="text/javascript"><![CDATA[
  function compare(a, b) {
    if (a < b) {
      console.log("A is less than B");
    } else if (a > b) {
      console.log("A is greater than B");
    } else {
      console.log("A is equal to B");
    }
  }
</script>

在兼容xhtml的web浏览器中,这解决了这个问题。但是,许多浏览器仍然不兼容xhtml,并且不支持CDATA部分。为了解决这个问题,CDATA标记必须被JavaScript注释所抵消:


 









 


<script type="text/javascript">
//<![CDATA[
  function compare(a, b) {
    if (a < b) {
      console.log("A is less than B");
    } else if (a > b) {
      console.log("A is greater than B");
    } else {
      console.log("A is equal to B");
    }
  }
//]]>
</script>

这种格式适用于所有现代浏览器。虽然有一点小技巧,但它验证为XHTML,并在XHTML之前的浏览器中优雅地降级。

当页面将其MIME类型指定为“application/ XHTML +xml”时,将触发XHTML模式。并不是所有的浏览器都支持以这种方式提供的XHTML。

# 弃用的语法(Deprecated Syntax)

自从Netscape 2在1995年发布以来,所有的浏览器都使用JavaScript作为默认的编程语言。type属性使用MIME类型字符串来标识<script>的内容,但是MIME类型在不同的浏览器中是不标准化的。尽管浏览器默认使用JavaScript,但在某些情况下,type属性的MIME类型值无效或无法识别,这将导致某些浏览器跳过相关代码的执行。因此,除非使用XHTML或<script>标记请求或包装非javascript,否则最佳实践是根本不指定类型属性。 当<script>元素最初被引入时,它标志着与传统的HTML解析不同。需要在这个元素中应用特殊的规则,这给不支持JavaScript的浏览器带来了问题(最值得注意的是Mosaic)。不支持的浏览器会将<script>元素的内容输出到页面上,这实际上破坏了页面的外观。 Netscape与Mosaic合作,提出了一个解决方案,可以向不支持它的浏览器隐藏嵌入的JavaScript代码。最后的解决方案是将脚本代码封装在HTML注释中,如下所示:

<script><!--
function sayHi(){ console.log("Hi!");
}
//--></script>

使用这种格式,像Mosaic这样的浏览器可以安全地忽略<script>标记中的内容,支持JavaScript的浏览器必须寻找这种模式来识别确实有JavaScript内容需要解析。 尽管这种格式仍然被所有web浏览器正确地识别和解释,但它不再是必需的,也不应该被使用。在XHTML模式下,这还会导致脚本被忽略,因为它位于有效的XML注释中。

# 内联代码与外部文件

虽然可以直接将JavaScript嵌入到HTML文件中,但通常认为最好的做法是尽可能多地使用外部文件来包含JavaScript。请记住,对于这种做法没有硬性规定,使用外部文件的理由如下:

  • 可维护性: 散布在各种HTML页面中的JavaScript代码将代码维护变成了一个问题。为所有JavaScript文件创建一个目录要容易得多,这样开发人员就可以编辑独立于所用标记的JavaScript代码。
  • 缓存: 浏览器根据特定的设置缓存所有外部链接的JavaScript文件,这意味着如果两个页面使用同一个文件,该文件只下载一次。这最终意味着更快的页面加载时间。
  • 面向未来: 通过使用外部文件包含JavaScript,不需要使用前面提到的XHTML或注释技巧。对于HTML和XHTML,包含外部文件的语法是相同的。

在配置如何请求外部文件时,值得注意的一个方面是它们对请求带宽的影响。 使用SPDY / HTTP2,每个请求的开销将大大减少,因为将脚本作为轻量级独立JavaScript组件传递给客户端可能是有利的。

例如,你的首页可能有以下内容:

<script src="mainA.js"></script>
<script src="component1.js"></script>
<script src="component2.js"></script>
<script src="component3.js"></script>

加载的后续页可能具有以下内容:

<script src="mainB.js"></script>
<script src="component3.js"></script>
<script src="component4.js"></script>
<script src="component5.js"></script>

在最初的请求中,如果浏览器支持SPDY/HTTP2,它将能够有效地从同一个端点检索多个文件,并将它们按每个文件输入浏览器缓存。从浏览器的角度来看,通过SPDY/HTTP2检索这些单独的资源的延迟应该与传递单一JavaScript负载的延迟大致相同。

在第二个页面请求中,因为您将应用程序分割为轻量级可缓存文件,所以第二个页面所依赖的一些组件已经在浏览器缓存中。

当然,这假设浏览器支持SPDY/HTTP2,这对于现代浏览器来说只是一个有效的假设。如果您的目标是包含对旧浏览器的支持,那么单块有效负载可能更合适。

# 文档模式

Internet Explorer 5.5通过使用doctype切换引入了文档模式的概念。前两种文档模式分别是怪癖模式和标准模式,前者使Internet Explorer的行为类似于版本5(具有一些非标准功能),后者使internetexplorer的行为更符合标准。尽管这两种模式之间的主要区别与CSS的内容呈现有关,但也有一些与JavaScript相关的副作用。这些副作用在整本书中都有讨论。

自从Internet Explorer首次引入文档模式的概念以来,其他浏览器也纷纷效仿。随着这种采用的发生,出现了一种称为准标准模式的第三种模式。这种模式有很多标准模式的特点,但没有那么严格。主要区别在于处理图像周围的间距(在表格中使用图像时最明显)。

怪癖模式在所有浏览器中都是通过在文档开头省略doctype来实现的。这被认为是糟糕的做法,因为怪癖模式在所有浏览器中都是非常不同的,如果不使用一些技巧,就不可能实现真正的浏览器一致性。

当使用下列文档类型之一时,将打开标准模式:

<!-- HTML 4.01 Strict -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<!-- XHTML 1.0 Strict -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- HTML5 -->
<!DOCTYPE html>

准标准模式由transitional和frameset doctype触发,具体如下:

<!-- HTML 4.01 Transitional -->
<!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<!-- HTML 4.01 Frameset -->
<!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Frameset//EN"
"http://www.w3.org/TR/html4/frameset.dtd">
<!-- XHTML 1.0 Transitional -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- XHTML 1.0 Frameset -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Frameset//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">

由于准标准模式是如此接近标准模式,因此几乎没有区别。谈论“标准模式”的人可能是在谈论这两种模式中的任何一种,而对文档模式的检测(将在本书的后面讨论)也没有做出区分。在本书中,标准模式这个术语应该被理解为除了怪癖模式之外的任何一种模式。

# <noscript> 元素

早期浏览器特别关注的是浏览器不支持JavaScript时页面的优雅降级。为此,创建了<noscript>元素来为没有JavaScript的浏览器提供替代内容。尽管现在100%的浏览器都支持JavaScript,但这个元素对于显式禁用JavaScript的浏览器仍然有用。

<noscript>元素可以包含除<script>之外的任何HTML元素,这些元素可以包含在文档<body>中。 <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>

在本例中,当脚本不可用时,将向用户显示一条消息。对于具有脚本功能的浏览器,即使此消息仍然是页面的一部分,也永远看不到它。

# 总结

  • JavaScript通过使用<script>元素插入HTML页面。此元素可用于将JavaScript嵌入HTML页面,使其与标记的其余部分保持内联,或包含外部文件中存在的JavaScript。以下是要点:
  • 要包含外部JavaScript文件,必须将src属性设置为要包含的文件的URL,该URL可以是与包含页面位于同一服务器上的文件,也可以是位于完全不同的域中的一个文件。
  • 所有<script>元素均按照它们在页面上出现的顺序进行解释。只要不使用defer和async属性,在开始下一个<script>元素中的代码之前,必须完全解释<script>元素中包含的代码。
  • 对于非延迟脚本,浏览器必须先完成<script>元素内的代码解释,然后才能继续呈现页面的其余部分。因此,通常在页面末尾,主要内容之后以及结束</body>标记之前包含<script>元素。
  • 您可以使用defer属性将脚本的执行推迟到文档渲染完成之后。延迟脚本始终按照指定的顺序执行。
  • 您可以使用async属性指示一个脚本不需要等待其他脚本,也不必阻止文档呈现。异步脚本不能保证以它们在页面中出现的顺序执行。

通过使用<noscript>元素,您可以指定仅当浏览器不提供脚本支持时才显示内容。 如果在浏览器中启用了脚本,则不会呈现<noscript>元素中包含的任何内容。

Last Updated: 4/13/2020, 10:26:45 AM