接着上一篇文章继续聊 HTML head 标签:
Roscoe:What's in your head —— 深入 head 标签(一)zhuanlan.zhihu.com在(一)中,我们已经介绍过了<title>, <link>, <base>,<style>,下面我们继续介绍另外两个重要的元素<script>和<meta>。
<script>
<script>一般用于插入 JavaScript 脚本,如:
脚本也可以不从外部加载,而是直接在元素中嵌入内容。
<script>console.log('Hello!')</script>
说到<script>不得不提的两个属性是 defer 和 async。
现在应用的脚本动辄好几 M(比如 Facebook Feed 页的 JavaScript 就超过 6M),在页面头部加载和执行这些脚本将会严重阻碍页面展现给用户的速度。如下面所示:
<html>
<head>
<script src="./asset/vendor.js"></script>
<script src="./asset/index.js"></script>
</head>
<body>...</body>
</html>
为了解决 HTML 解析等待时间过长的问题,可以把一些和渲染本身无关的脚本放在 HTML body 的末尾:
<html>
<head>
...
</head>
<body>
...
<script src="./asset/vendor.js"></script>
<script src="./asset/index.js"></script>
</body>
</html>
从上图可以看到,脚本的加载和执行已经不会阻塞 HTML parsing 了,到了 ready 的那一时刻页面已经可以呈现出内容。但现在还有一个问题,就是在 ready 到脚本处理完成之前这段时间,用户仍然不能交互。
对于页面性能来说有一项重要指标就是 TTI(Time to Interactive),也就是浏览器 domInteractive 事件触发的时间,它代表此刻 HTML 解析以及 DOM 的构建已经完毕,用户可以进行操作。从上图处理流程来看,把脚本放在 body 的末尾并不能解决 TTI 时间长的问题。
defer
defer 可以让浏览器对当前脚本进行异步加载,并且仅在 HTML parsing 完毕之后执行。如:
<html>
<head>
<script src="./asset/app.js" defer></script>
</head>
<body>...</body>
</html>
和把脚本放在 body 末尾相比,fetch script 这部分时间被节省了下来。
async
async 和 defer 一样都会使浏览器异步地加载脚本,不同的是加载完毕后 async 的脚本会立即执行,即便页面还在解析 HTML 也要暂停下来,等脚本处理完毕后再继续。如:
<html>
<head>
<script src="./asset/app.js" async></script>
</head>
<body>...</body>
</html>
由于我们很难控制 async 脚本在什么时候加载完成,页面中 async 脚本的执行顺序可能会非常随机,如果需要保证多个脚本的加载顺序的话仍然需要选择 defer。
另外借助 async 和 defer异步加载的特性,可以让浏览器同时获取多个脚本:
crossorigin
在我们收集页面脚本错误的过程中,由于浏览器的安全机制,对于跨域 JavaScript 文件抛出的错误我们是看不到详细信息的,能看到的只有 Script Error。
然而很多时候我们的静态资源都是放在 CDN 上,不得不跨域,这时就可以在<script>上添加 crossorigin 属性,这样就可以像同域脚本一样看到错误信息了(注意:使用该特性时需要服务器响应头包含属性 Access-Control-Allow-Origin: *)。
type
一般会将 type 设置为 type="text/javascript":
<script type="text/javascript" src="..."></script>
除此以外,type 还可以设置为 module,表明这是一个 JavaScript 模块,它将遵循 ES6 中对于模块的定义。目前各个现代浏览器(除 IE)均已支持该特性。module 和一般的脚本相比有以下几点不同:
- 默认处于严格模式下;
- 顶层作用域不再是全局作用域。也就是说对于 var a = 1,a 只会存在与当前模块的作用域中,而不会像以前一样是全局声明;
- 有了 import 和 export 语法,可以通过 import 加载外部模块;
- 多次加载/引用同一模块,该模块只会执行一次,这遵循了 ES6 对于模块的定义。
在浏览器上主要表现在以下几点不同:
- 默认表现出 defer 的行为,把模块放在任何位置都只在 HTML 解析后执行;
- 默认以 CORS 形式获取,因此需要服务端设置响应头 Access-Control-Allow-Origin: *。
接下来简单对使用原生 ES Modules 和用 Webpack/Rollup/Parcel 打包进行一下对比。
优势:
- 无需任何配置的按需加载,不必将所有模块打包在一起,每个页面只加载自己需要的模块;
- 细粒度的缓存,单个模块的改动不会影响其它模块。
劣势:
- 模块数量过多影响页面性能(尽管使用 HTTP2/Server Push/Preload 等技术可以进行一定的弥补);
- 无法 Tree Shaking;
nomodule
带有 nomodule 属性的脚本会被所有支持 type="module" 的浏览器所忽略,因此它基本上就是 type="module" 的 fallback:
<!-- for browsers support type="module" -->
<script type="module" src="index.mjs"></script>
<!-- for browsers don't support type="module" -->
<script nomodule src="app-fallback.js"></script>
<meta>
在(一)中我们说过,<head> 包含的是页面的元数据(描述数据的数据),所有<head> 中的元素都是不会直接渲染的,比如标题(<title>)、样式(<style>)等等。<meta>顾名思义也是用于表达元数据的,只不过它可以包含的信息种类更加丰富。
鉴于 meta 使用方法非常多,下面我们来根据用途分分类。
用途1:设置 charset
设置页面字符编码,如:
<meta charset="utf-8">
实际请求页面的时候响应头如果有 content-type 字段,它里面的 charset 会覆盖当前 meta 标签中的值。如:content-type: text/html; charset=utf-8。
用途2:指导 SEO
通过 name + content 属性可以设置各种 SEO 相关的元数据。如:
<meta name="keywords" content="Free, Movies, TV shows, ...">
<meta name="description" content="Watch free movies and TV shows online in HD on any device. Tubi offers streaming movies in genres like Action, Horror, Sci-Fi, Crime and Comedy. Watch now.">
keywords 可以帮助搜索引擎确定当前页面的主题,比如上面例子中的 tubi.tv,就添加了如 Movies、TV shows 等关键字 。
description 则很可能与<title>一起出现在搜索结果中,它可以帮助用户了解你的页面所提供的内容是否是他们需要的。准确的描述可以降低用户的跳出率。
另外还有一个与 SEO 非常相关的元数据——robots。它可以对页面爬虫(Google、Bing 等)进行指导,比如是否对当前页面生成 index(像 404 页就不需要)、在搜索结果中是否使用页面中的 description 等。
在(一)中也包含了关于 SEO 优化的内容,如 canonical 元素等,可以结合起来回顾下。
用途3:社交媒体分享
当我们把一个页面链接通过社交媒体分享给他人时,可以附带一些信息,如:
这项技术叫 Open Graph Protocol,它相当于面向不同社交媒体的一个通用协议。当我们分享一个链接,理所当然地会希望接收者能了解它所指向的大概内容,因为链接本身包含的信息极为有限,甚至链接太长完全不可读,导致分享的效果不好。
按照 Open Graph Protocol 的规则开发者可以定义一系列元数据,用于展示除链接本身外更丰富的信息以供用户预览。其形式为:
<meta property="og:<property_name>" content="<property_content>" />
社交媒体平台诸如 Twitter、Facebook 等平台会提取页面中的这类信息:
经过重新整合就成为了上面截图中看到的样子。
由于各平台展现方式不同,有些社交媒体定义了属于的协议,因此除了 og 之外,可能还会看到如 twitter,fb 等。
用途4:设置 viewport
viewport(视口)是一个虚拟的窗口,用户通过它来查看页面内容。viewport 的宽度可以被认为设置,并且允许缩放。下图是同一个网页在没有进行任何 viewport 设置的情况:
可以看到在较窄的移动端屏幕上,为了展示出全部内容,网页被缩小了。相信很多人都有过这种体验,在手机上看一个电脑版的网页,如果需要看清里面的内容基本就需要进行放大,拖拽等操作。
假如 PC 和页面的宽度均是 1440px,手机的宽度是 360px,那么此时 viewport 的尺寸是多少?答案是 1440px,因为它展示了 1440px 的内容。
由于 viewport 宽度是手机宽度的 1440 / 360 = 4 倍,并且我们没有设置缩放比,因此所有字体/图片在移动端的实际大小都被自动缩小了 4 倍。
下面我们将 viewport 的宽度设置为设备宽度,初始缩放比为 1:
<meta name="viewport" content="width=device-width, initial-scale=1">
此时 viewport 在移动端上的宽度为 360px,可以看到已经没有自动缩放的效果了。
然而页面的布局还是不理想,需要拖拽来查看完整内容,因此我们还需要使用 media query 等技术对移动端的屏幕进行重新布局:
上面经过重新排布后,移动端的效果看起来比之前好多了。它的 viewport 宽度仍然是 360px,只不过我们将页面的宽度通过布局也处理为了 360px,这样就无需用户的多余操作了。
在此基础上,我们还可以通过 "minimum-scale", "maximum-scale", or "user-scalable" 控制用户缩放。
举个例子,下面是知乎的 viewport:
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
初始缩放比和最大缩放比均为 1,相当于阻止了用户缩放(当 viewport 宽度小于设备宽度时会自动放大,所以这里虽然没有设置 minimum-scale,但相当于已经无法缩小)。
用途5:开启 IE edge 模式
如果你的页面需要支持较老版本的 IE 浏览器(如 IE8),请加上:
<meta http-equiv=“X-UA-Compatible” content=“IE=edge”>
它会让 IE 浏览器使用尽量高版本的模式对页面进行解析,在一定程度上减少兼容性问题以及提高性能。
其它
从上面我们已经可以看出,对于不同的平台、设备以及搜索引擎来说,都可以借助 meta data 来更好的展示页面内容,甚至我们自己也可以针对应用的需求自定义 meta data,因此对于 meta 的应用可能会随着时间越来越丰富。
小结
两篇文章大致梳理了一下 <head>中主要的元素以及它们的作用,其实还有一些点没有提到。比如涉及到<script>安全性的 integrity、nonce 等属性,在国外的大站上基本都有,但国内还没有见到过,希望能慢慢弥补上来吧。