妙才

V1

2023/05/25阅读:20主题:凝夜紫

初级前端必备知识:script

在 Web Frontend 开发中,页面的结构和渲染由HTMLCSS处理,而页面的交互逻辑的任务通常会交给script脚本来处理。每一个前端开发者或许都应该深入了解一下隐藏在绚丽页面下的脚本引入基础知识:script元素!

前言

现在由于打包工具的流行,前端已经很少需要去写一些脚本引入的代码了。但是,在某些特殊的业务场景下,还是需要开发者手动引入一些script脚本来实现特定的功能。

因此,或许我们应该花一点点时间来了解关于script标签的知识,打磨自己的基本功。

script 基础

在浏览器环境中,<script>用于定义客户端脚本,其可以分为内联脚本外部脚本,内联脚本将代码写在标签内部:

...
<script>
 ...code...
</script>
...

外部脚本:

<script
  src="https://code.jquery.com/jquery-3.7.0.min.js"
  integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
  crossorigin="anonymous">

</script>

如果你在使用内联脚本的时候,还指定了src外部文件地址引用,那么最新版 chrome 浏览器将会忽略内联脚本代码。

不同版本和厂商针对这个操作的结果可能不同,请不要这样做,这显然是一个反向最佳实践

默认的script代码加载和执行是自上而下的,这种同步的机制将会阻塞DOM的构建(毕竟 JS 能轻松地修改 DOM),从而使得页面渲染的时间往后延长,用户可能会因此看到一段时间的白屏。

先来看一张图:

默认的script标签将会在fetch(拉取远程脚本)和execution(执行)的同时,暂停HTML的解析。

2014 年HTML5发布之前,Script 其所有常用属性如下:

  • charset

  • crossorigin

  • integrity

  • src

HTML5发布之后,增加了:

  • async
  • defer
  • type
  • nomodule
  • referrerpolicy
  • blocking
  • fetchpriority

下面,我们逐一分析每一个属性的作用和使用场景、需要注意的问题和最佳实践。

charset

指定外部脚本的字符集编码(不指定此属性或是内联脚本,脚本的字符集编码格式都使用document的编码格式),通常脚本的字符集编码跟document是一致的,我们不需要处理字符集编码。

但是,总是有一些特殊场景需要我们用指定的字符集编码去应付😌。

crossorigin

这个属性用于控制跨域脚本的访问,对于同源脚本来说没有意义。

这个属性支持两个值:

  • Anonymouse(默认值):匿名请求,不发送凭证
  • Use-credentials:发送凭证

凭证:cookie 或 HTTP 认证信息(明文传输的用户密码、)

对于跨域脚本来说,客户端和服务端的配置都要正确才行。

integrity

此属性是为了确保跨域脚本的完整性,防止脚本中途被修改。

这个属性可以提高站点跨域脚本的安全性,例如引入React这样的跨域脚本(CDN),如果跨域脚本的哈希值跟配置的哈希值不匹配,浏览器就会将之视为不安全的脚本,从而禁止运行。

举个例子:

<script src="hello_world.js"
   integrity="sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd
              sha512-Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw=="

   crossorigin="anonymous">
</script>

上述例子同时指定了两种哈希算法的哈希值,只有全部匹配才会执行脚本内的代码。

为了最大限度地保障安全性,建议使用最新的哈希算法(如 SHA-384SHA-512

src

毫无疑问:资源的相对地址或绝对地址。

async

在没有async这个新属性之前,社区推荐我们将script标签放在<body>的最后面,即优先DOM的构建和内容的渲染,也符合“网页的结构内容优先,脚本内容其次。”这种理念。

caniuse.com的数据上看,async支持11 年前发布的IE10、13年前发布的Chrome8等旧版浏览器,当前国内的支持率为91.99%,在兼容性方面已经不需要担心了。

话说回来,async是一个布尔值属性,当设置为true时表示这个script是一个异步script,浏览器会加载远程脚本(src的地址),在加载完成后立即执行这个脚本。

需要注意的是:

页面上多个async script的执行顺序是不确定的,跟加载顺序无关,因此异步脚本如果存在依赖关系,则可能导致执行异常,如果需要让加载外部脚本的同时不影响 HTML 的解析,还要保证其顺序,那么可以考虑将多个脚本合成一个。并且在这个加载和执行过程中,仅在执行时会暂停HTML的解析。

来看这张WHATWG标准文档的示意图:

就使用场景而言,根据其特性可知,当此脚本内的逻辑与页面 DOM无关,并且执行的耗时较短时,我们可以使用async属性标记。

例如:用户访问分析脚本、广告统计脚本等。

defer

这也是一个布尔属性,表示该脚本加载不影响HTML解析,并且延迟执行时机直到HTML解析完成,并且在DOMContentLoaded事件触发之前执行。

DOMContentLoaded 事件是在HTML文档被完全加载和解析后触发的事件。它表示文档已经准备好,可以被JavaScript代码访问和操作。该事件通常被用来在页面加载完成后执行一些初始化的JavaScript代码,例如注册事件监听器、修改DOM元素、发送网络请求等。

defer适合那些大量引入外部脚本的场景,需要注意的是,使用deferasync一样无法保证最终的执行顺序,但可以提高页面的加载速度和响应速度。

type

script元素里定义type的作用是定义脚本的媒体类型(MIME 类型),浏览器将会根据这个值来判断脚本的编程语言,再解析和执行脚本(默认为text/javascript)。

需要注意的是,如果你使用的脚本类型确实不是text/javascript,那么可以显式地设置别的MIME类型。

这里稍微了解一下支持的MIME类型值:

  • text/javascript
  • text/vbscript
  • application/json
  • module
  • ...

显式大于隐式:显式指定媒体类型能有效兼容古老的浏览器

绝大多数情况下,我们都是引入 JavaScript 脚本,scripttype缺省值是text/javascript,表示这是一个独立的脚本。

在多年前,旧的浏览器也会设置为text/vbscript来执行vbscript程序(IE 荣光),亦或是设置为application/json来传递json数据,减少ajax请求。

在现代浏览器中,将scripttype设置为module,则表示这个脚本是一个ES模块

模块都是异步加载的,再来看这张示意图:

模块的加载不会影响HTML的解析,并且其在未设置asyncdefer属性时都是默认等待页面解析渲染完成后才执行的。

模块天生具有隔离区,内部的变量和函数不会污染全局空间,并且内部模块的引入都是异步的,不会对页面资源的请求加载造成阻塞。

需要注意的是,如果是非同源模块,务必要添加crossorigin属性告知浏览器需要使用CORS。与此同时,远程脚本所在的服务器也需要指定CORS属性来配合浏览器处理跨域资源。二者其一配置不当都会导致浏览器无法正常加载模块。

现代浏览器对module的支持度已经非常高了,但是如果在某些特殊场景下需要兼容老旧的浏览器,则可以使用一个<script nomodule>标签来为不支持模块的浏览器提供备用代码。

nomodule

表示此脚本仅允许不支持module的现代浏览器执行,通常用于兼容老浏览器的场景下作为<script type="module">的备用选项。

referrerpolicy

默认情况下,script标签在请求外部脚本时会带上当前URL的信息到referrer字段,如果需要隐藏referrer来保护用户隐私,那么可以设置scriptreferrerpolicy值为origin

需要注意的是

<script referrerpolicy="origin">
  fetch('/api/data');    // not fetched with <script>'s referrer policy
  import('./utils.mjs'); // is fetched with <script>'s referrer policy ("origin" in this case)
</script>

如上所示,脚本内部import其他脚本时也会遵守引用策略,而内部请求则遵守全局引入策略。

全局引入策略可以通过meta元素设置

blocking

设置blockingtrue则表示让此脚本阻塞页面的渲染,通常我们会担心脚本的耗时操作将会阻塞页面的渲染,带给用户不好的体验,因此这个属性几乎不会被用到。

fetchpriority

我们可以用fetchpriority来控制脚本的优先级,其值为:

  • high
  • low
  • auto (默认值)

需要注意的是,这只是一个提示。浏览器厂商对规范的实现范畴并不一致,因此在需要控制不同脚本的加载顺序时,通常建议:

  • 将脚本放在底部
  • 使用asyncdefer来异步加载和执行
  • 将多个脚本合并成一个
  • 使用typemodule将不同的脚本按模块的引入顺序来控制

参考

  • HTML Standard - script[1]
  • 你知道 script 标签脚本在各种情况下的加载和执行顺序吗? - 知乎[2]
  • 关于script标签中的type的使用[3]

参考资料

[1]

HTML Standard - script: https://html.spec.whatwg.org/multipage/scripting.html

[2]

你知道 script 标签脚本在各种情况下的加载和执行顺序吗? - 知乎: https://zhuanlan.zhihu.com/p/464633848

[3]

关于script标签中的type的使用: https://www.zhihu.com/tardis/zm/art/523727939?source_id=1003

分类:

前端

标签:

前端

作者介绍

妙才
V1