一文搞懂 script 标签

前端开发应该都知道 HTML 中 <script> 标签的作用——引入 JS 代码,不过由于脚手架和打包工具的普及,我想很少有人再亲手写 <script> 了。本期就借机写一下这个快被遗忘了的 <script> 教程,看看大家是否真的掌握了这个元素。

基本功能

废话不多说了,直入正题。<script> 引入 JS 的方式主要有三种:内联、外置,以及动态引入。

内联

直接将 JS 代码写到 script 标签内:

<html>
  <head>
    <script>
      console.log("Hello");
    </script>
  </head>
</html>

外置脚本

即通过 src 属性引入外部 URL 或 JS 文件:

<html>
  <body>
    <script src="http://www.example.com/example.js"></script>
    <!-- 只有加载完并执行完 example.js 后,才开始加载 0.js -->
    <script src="./js/0.js"></script>
  </body>
</html>

script 标签可以放置在 html 任意位置,head、body,甚至是 div 里。它们——无论是内联还是外置——的执行顺序基本上(async 和 defer 除外)秉承由上至下串行执行的原则。浏览器首次加载 script 期间,还会阻塞 HTML 页面解析;尤其是外置引入 JS,需要经历网络传输、解析和执行,有时候会导致浏览器白屏。所以谈到首屏渲染的时候,我们往往会建议将 script 标签放到 <body> 元素的最下方——先呈现页面再执行 JS。

<html>
  <head>
    <script>
      var x = "onion";
      console.log(document.head); // <head>...</head>
      console.log(document.body); // null
    </script>
  </head>
  <body>
    <script>
      console.log(x); // onion
      console.log(document.head); // <head>...</head>
      console.log(document.body); // <body>...</body>
    </script>
  </body>
</html>

此外,在 script 执行期间,它可以获取到所有出现在它上方的 JS 全局变量和 DOM 元素;这导致在一些垃圾代码里,全局元素经常无缘无故被其他代码块污染了。

动态引入

我们也可以在 JS 代码里动态添加 script 标签。方法很简单,就是追加一个 script 元素:

var myScript = document.createElement("script");
myScript.textContent = 'alert("✋")';
document.head.appendChild(myScript);

还有,通过 innerHTML 方式其实也能添加 script 标签,只是该标签下的 JS 不会运行——很有趣的冷知识。

document.head.innerHTML += '<script>alert("✋")';

加载(async & defer)

上文提到为了加快首屏渲染,我们通过把 script 标签放到 <body> 底部加快首屏渲染速度。现代浏览器还可以使用其他的手段,比如 defer(延迟加载)和 async(异步加载)

defer

defer 是 script 里的一个布尔属性,设计目的是将该脚本的执行放到文档完成解析后、DOMContentLoaded(约等于 jQuery.ready)事件前。举个例子,下方的 example.js 文件虽然放在了 head 里,但是它有 defer 属性,不会阻塞下方的 <body> 解析。

<!DOCTYPE html>
<html lang="en">
  <head>
    <scrip defer src="http://www.example.com/example.js"></scrip>
  </head>

  <body>
    <!-- content -->
  </body>
</html>

此外,当存在多个 defer 脚本时,html5 标准要求按出现顺序执行脚本;但在现实中,浏览器厂商并不那么遵循标准:defer 脚本不一定顺序执行,甚至不一定会排在 DOMContentLoaded 事件前。因此通常的建议是:最好只含一个延迟脚本。

async

async 也是 script 标签里的一个属性,该属性也能够消除部分 JS 阻塞。当加上 async 属性后,script 脚本的网络请求便可以并行于 HTML 页面解析发生;并尽快解析和执行该 JS 脚本。也许你会有疑问,async 和 defer 似乎差不多呀,那它们的区别到底是什么?一图胜千言:

async vs. defer

模块化

type

先说一个叫 type 的属性,该属性原本是用来指定 script 脚本的 MIME 类型,默认值是 text/javascript,其他值还有诸如:text/ecmascriptapplication/ecmascriptapplication/javascript 等等。不过,现代浏览器很多都不再鸟这些值了;而是把 type 用来支持 es6 的模块功能:

<!-- index.html -->
<scrip type="module">
  import { sayHi } from "./hello.js"; document.body.innerHTML = sayHi("Onion");
</scrip>

用法很简单,在 script 标签里指定 type="module",当脚本使用 import 指令时,浏览器会自动请求并加载相关的 JS 文件。

// hello.js
export function sayHi(user) {
  return `Hello, ${user}!`;
}

这里再提一下,module 的默认加载机制就是 defer,只不过下载过程中会顺道把 import 导入的文件也给下载了;如果和 async 属性一起使用,其加载方式就是 async 形式了,大同小异,就不再赘述了。

nomodule

除此之外,我们常常会看到 module script 下方还会跟一个 nomodule 的 script:

<scrip type="module" src="app.js"></scrip>
<scrip nomodule src="classic-bundle.js"></scrip>

这个功能主要是用来兼容一些老版本的浏览器:

  • 支持 module 的浏览器,设定上就不会执行 nomodule 属性的 script 脚本,所以它只会跑上方的 app.js 脚本

  • 而老破旧的浏览器不支持 type="module",会跳过这个 script 标签;同时又由于它不认识 nomodule 属性,反倒会执行 nomodule script 里的 classic-bundle.js 文件了

一个小技巧就解决了浏览器兼容方面的问题。

安全机制

integrity

该属性允许 script 标签提供一个 hash 值,用于检验加载的 JS 文件是否完整。比如,如下便签的 integrity 值就是告诉浏览器:使用 sha256 算法计算 JS 文件的摘要签名,然后对比 integrity 值,如果不一致就不执行该资源。它的主要功能就是防止托管在 CDN 上的资源被篡改。

<scrip
  src="//code.jquery.com/jquery.js"
  integrity="sha256-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
></scrip>

nonce

nonce 在我之前的文章——《CSP 101》——提到过:它是一个加密数字,需要配合 Content-Security-Policy 的 script-src 使用。举个例子,http 头的 CSP 属性如下:

Content-Security-Policy: script-src 'nonce-EfNBf03nceIOAn39fn389h3sdfa';

只有在 script 标签内带有相同 nonce 值的脚本才能执行:

<script nonce="nonce-EfNBf03nceIOAn39fn389h3sdfa" src="./hello.js"></script>

referrerPolicy

该属性主要和 HTTP 头里的 Referer 配合使用。有些服务器审查比较严格,需要知道请求的“引荐人”(Referrer);客户请求 API 时,需要同时发送引荐人信息。最简单的使用方式就是给相关的 script 标签添加 referrerPolicy 属性:

<scrip referrerpolicy="origin" src="./js/hello.js"></scrip>

如上代码中,hello.js 里的所有 api 请求都会在头信息里加上相应 URL 的域(origin)。referrerPolicy 的值很多,也很琐碎,有兴趣的朋友可以去MDM 相关页面查看。

冷知识:HTTP 头的 Referer 有拼写错误,正确的写法是 Referrer;但是标准提案里写错了,结果大家就将错就错了

crossorigin

在 HTML5 中,<script> 与其他一些元素(<audio><img><link>、和 <video>)提供了对 CORS 的支持; 他们均有一个跨域属性——crossorigin——来配置元素获取数据的 CORS 请求。一旦启用 crossorigin,http 头里须包含 Access-Control-Allow-Origin 属性,若该属性不存在或是源不必配,则不能加载资源。

Crossorigin 的默认值是 anonymous(空值或是无效值都等于 anonymous),表示对跨域请求不设置凭据标志;相反,想要提供该凭证,就需要设置 crossorigin="use-credentials"。(这里的凭据,指的就是 cookies、http 里的 auth,以及客户端的 SSL 证书

onload & onerror

onload 和 onerror 算是两个隐藏属性吧,因为只能在动态引入时使用。顾名思义,onload 会指向成功加载时的事件,onerror 就是失败时触发的事件。用法也很简单,就是给这两个属性赋值某个事件函数。现实操作中常配合 crossorigin 使用,打印出三方源的一些错误信息。

let script = document.createElement("script");

script.src = "http://www.example.com/example.js";
document.head.append(script);

script.onload = function () {
  alert("Success Loading");
};

script.onerror = function () {
  alert("Error Loading");
};

扩展小知识:基本上所有包含 src 属性的 HTML 元素都有 onload 和 onerror 这两个隐藏属性,如: <img><iframe>

其他

  • language: 早年间用来指定脚本语言的属性,如 Javascript、JavaScript1.2、VBScript,不过现在已弃用

  • charset:指定代码的字符集,如charset="UTF-8",可惜也已经过时了

小结

<script> 一直是我的知识盲点,网上除了 MDM 这种艰涩难懂的标准文档外,竟然很难再找到相关的教程了。本文整理了我见到过的所有 script 属性,并加了一点小小的知识延伸,希望能给大家查漏补缺予以一定帮助。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • <script>标签 async="async" 异步执行脚本 defer="defer" 脚本在页面完成解析时执...
    BGING阅读 380评论 0 1
  • 属性 async: 表示应该立即下载脚本, 但不影响页面中其他的操作.该属性对于内联脚本无作用(即没有src属性的...
    IOneStar阅读 523评论 0 0
  • script 拥有的属性 async:可选,表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等...
    miner敏儿阅读 1,657评论 0 0
  • defer和asnyc(只对外部文件有效)defer 在页面完成解析时执行代码,这个属性表明脚本在执行时不会影响页...
    Obeing阅读 387评论 0 3
  • 这篇文章来源于JS高级程序设计第三版中关于script标签的介绍,结合查阅的资料写下的学习笔记。 向html页面中...
    加油小杜阅读 600评论 0 0