网页首屏加载过程
2023-03-18
前言
在使用vue的过程中,我对于页面的加载顺序产生了困惑。我总是以vue作为渲染页面的开端,这个误解让我对很多理所应当的现象产生了困惑。实质上,vue自身也不过是一个js文件(对应main.ts/main.js),它嵌入在返回给前端的html代码中,在浏览器解析html的时候被执行到,以控制整个页面的行为。这个文件被称为vue入口文件。
由于这个文件的存在,我们想要在页面中执行其他的js文件就产生了困难。比如我们想要插入个js标签,它引入了一个渲染一些标签样式的脚本文件,我们把它放在vue的入口文件标签的后面,试图在vue渲染出响应的标签dom之后,加载这个文件,但结果却失败了。vue对于dom的渲染和解析有着自己独立的时间点,所以即使main.js被加载了,再去加载我们的脚本,也无法保证这个脚本加载在dom被渲染之后,最后导致我们看不到样式。
有两种方式可以选择:
- 在vue组件中引入对应的js文件。对于使用ts的环境来讲,这需要自己去写声明文件,这种方式并不友好,很麻烦。
- 在vue组件中的声明周期onMounted中动态的插入脚本。这种方式相对比较简单。单仅限于单个脚本引入,不要试图去插入多个相互依赖的脚本,vue虽然是按顺序去插入的,但无法去控制他们的加载顺序——他们是动态脚本。
实际上,我觉得以vue作为主体的思路是没有错的,错在我尝试在vue外部使用js代码,并用它操作dom。它是我们选用的框架,我们没有理由让一个新的js文件或者一段js代码脱离整个框架单独存在,恰恰相反,应该尽可能的让自己的js代码能跑在框架下, 与框架兼容。
这个问题有了解决方案之后,顺便总结了网页生命周期。
大体过程
当我们请求一个页面后,浏览器通过目标url的响应获取到了对应的html页面并开始加载。HTML parser解析DOM树,之后CSS parser解析CSSOM,二者合并连接,生成完整的一颗解析树,最后根据这棵解析树进行各种计算,最终渲染出所有的dom节点,即看到的页面。
HTML 页面的生命周期包含三个重要的事件:
DOMContentLoaded
—— 浏览器已完全加载 HTML,并构建了 DOM 树,但像<img>
和样式表之类的外部资源可能尚未加载完成。load
—— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。beforeunload/unload
—— 当用户正在离开页面时。
每个事件都是有用的:
DOMContentLoaded
事件 —— DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。load
事件 —— 外部资源已加载完成,样式已被应用,图片大小也已知了。beforeunload
事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。unload
事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据。
DOMContentLoaded
DOMContentLoaded
事件发生在 document
对象上。我们必须使用 addEventListener
来捕获它。
document.addEventListener("DOMContentLoaded", ready);
当浏览器处理一个 HTML 文档,并在文档中遇到 <script>
标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write
操作,所以 DOMContentLoaded
必须等待脚本执行结束。
此规则有两个例外:
- 具有
async
特性(attribute)的脚本不会阻塞DOMContentLoaded
。 - 使用
document.createElement('script')
动态生成并添加到网页的脚本也不会阻塞DOMContentLoaded
。
如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成。原因是,脚本可能想要获取元素的坐标和其他与样式相关的属性。因此,它必须等待样式加载完成。
简而言之,ready函数会在html加载完之后被触发。一个案例是,很多浏览器自带的自动填充账号密码功能,就是在这个事件上被触发的。
window.onload
当整个页面,包括样式、图片和其他资源被加载完成时,会触发 window
对象上的 load
事件。可以通过 onload
属性获取此事件。
window.onload = function() {
...
};
window.onunload
当访问者离开页面时,window
对象上的 unload
事件就会被触发。我们可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。
window.onbeforeload
如果访问者触发了离开页面的导航(navigation)或试图关闭窗口,beforeunload
处理程序将要求进行更多确认。
这些事件对于使用前端框架的基本是用不到的。vue的声明周期函数已经够用了。以后用到了再查即可。
script标签
之所以开一个标题用来阐述这玩意,是因为这东西剪不断,理还乱。
拥有的属性
- async:可选,表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。
- charset:可选。表示通过 src 属性指定的代码的字符集。由于大多数浏览器会忽略它的值,因此这个属性很少有人用。
- defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。IE7 及更早版本对嵌入脚本也支持这个属性。
- language: 已废弃。原来用于表示编写代码使用的脚本语言(如 JavaScript 、 JavaScript1.2 或 VBScript )。大多数浏览器会忽略这个属性,因此也没有必要再用了。
- src:可选。表示包含要执行代码的外部文件。
- type:可选。可以看成是 language 的替代属性;表示编写代码使用的脚本语言的内容类型(也称为 MIME 类型)。虽然 text/javascript 和 text/ecmascript 都已经不被推荐使用,但人们一直以来使用的都还是 text/javascript 。实际上,服务器在传送 JavaScript 文件时使用的 MIME 类型通常是 application/x–javascript ,但在 type 中设置这个值却可能导致脚本被忽略。另外,在非IE浏览器中还可以使用以下值: application/javascript 和 application/ecmascript 。考虑到约定俗成和最大限度的浏览器兼容性,目前 type 属性的值依旧还是 text/javascript 。不过,这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为text/javascript 。
在以上属性中 async属性是 HTML5 中的新属性。
放在哪的问题
script标签一般放在head或者body里。这是一种比较传统的做法,目的就是把所有外部文件(包括 CSS
文件和 JavaScript
文件)的引用都放在相同的地方.可是,在文档的 <head>
元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 <body>
标签时才开始呈现内容)。对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。很明显,这种做法有着很明显的缺点,特别是针对于现在的移动端来说,如果超过 1s 还没有内容呈现的话将是一种很差的用户体验。为了避免这个问题,我们可以把标签放到body的最后。对于这种方式,在解析包含的 JavaScript 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了。
defer和async
- defer——下载和加载
在所有dom标签按顺序加载的时候,轮到script标签的加载有可能会很慢。这个属性的用途是表明在加载script标签时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在 <script>
元素中设置defer
属性,相当于告诉浏览器立即下载里面连接的脚本,但不加载它——它将延迟到浏览器遇到 </html>
标签后再执行。
多个defer脚本将按照声明顺序加载。
- async——异步加载
async
脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async
脚本就是一个会在加载完成时执行的完全独立的脚本。就这么简单。它脱离了整个默认流程单独存在。
动态脚本
还有一种向页面添加脚本的重要的方式。
我们可以使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中。这个脚本可以是一个文件,可以是一段脚本代码,取决于type。
//方式1:插入文件
function loadScript(url){
var script = document.createElement("script");
script.src = url;
document.body.appendChild(script);
}
//方式2:插入代码片段
var script = document.createElement("script");
script.type = "text/javascript";
script.appendChild(document.createTextNode("function sayHi(){alert('hi');}"));
document.body.appendChild(script);
当脚本被附加到文档时,脚本就会立即开始加载。**默认情况下,动态脚本的行为是异步的。**这意味着它默认就是async。