浏览器运行机制
1.浏览器是多进程的区分进程和线程
- 进程是一个工厂,工厂有它的独立资源
- 工厂之间相互独立
- 线程是工厂中的工人,多个工人协作完成任务
- 工厂内有一个或多个工人
- 工人之间共享空间
- 工厂的资源 -> 系统分配的内存(独立的一块内存)
- 工厂之间的相互独立 -> 进程之间相互独立
- 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
- 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
最后,再用较为官方的术语描述一遍:
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
不同进程之间也可以通信,不过代价较大
现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)
2.浏览器是多进程的
浏览器是多进程的
浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程
3.浏览器都包含哪些进程?
1.Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有
。负责浏览器界面显示,与用户交互。如前进,后退等
。负责各个页面的管理,创建和销毁其他进程
。将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
。网络资源的管理,下载等
2.第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
3.GPU进程:最多一个,用于3D绘制等
4.浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
。页面渲染,脚本执行,事件处理等
- 强化记忆:在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)
4.浏览器多进程的优势
相比于单进程浏览器,多进程有如下优点:
。避免单个page crash影响整个浏览器
。避免第三方插件crash影响整个浏览器
。多进程充分利用多核优势
。方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果是单进程,插件崩溃了也会影响整个浏览器;而且多进程还有其它的诸多优势。。。
当然,内存等资源消耗也会更大,有点空间换时间的意思。
5.重点是浏览器内核(渲染进程)
重点来了,我们可以看到,上面提到了这么多的进程,那么,对于普通的前端操作来说,最终要的是什么呢?答案是渲染进程
可以这样理解,页面的渲染,JS的执行,事件的循环,都在这个进程内进行。接下来重点分析这个进程
浏览器的渲染进程是多线程的
终于到了线程这个概念了😭,好亲切。那么接下来看看它都包含了哪些线程(列举一些主要常驻线程):
1.GUI渲染线程
。负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
。当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
。注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
2.JS引擎线程
。也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
。JS引擎线程负责解析Javascript脚本,运行代码
。JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
。同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞
3.事件触发线程
。归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
。当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
。当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
。注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4.定时触发器线程
。传说中的setInterval与setTimeout所在线程
。浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
。因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
。注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5.异步http请求线程
。在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
。将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
6.Browser进程和浏览器内核(Renderer进程)的通信过程
如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程), 然后在这前提下,看下整个的过程:(简化了很多)
Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染
。渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
。当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
。最后Render进程将结果传递给Browser进程
Browser进程接收到结果并将结果绘制出来
7.梳理浏览器内核中线程之间的关系
-
GUI渲染线程与JS引擎线程互斥
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起, GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
-
JS阻塞页面加载
要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
-
WebWorker,JS的多线程?
前文中有提到JS引擎是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力么? 后来HTML5中支持了 Web Worker。
MDN的官方解释是 Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window 因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误这样理解下:
。创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM) 。JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可,perfect!
而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
8.WebWorker与SharedWorker
-
WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享
。所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
-
SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用
。所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。
本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程
9.浏览器渲染流程
- 浏览器输入url,浏览器主进程接管,开一个下载线程,
然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,
随后将内容通过RendererHost接口转交给Renderer进程
- 浏览器渲染流程开始
浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤: 1.解析html建立dom树
2.解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
3.布局render树(Layout/reflow),负责各元素尺寸、位置的计算
4.绘制render树(paint),绘制页面像素信息
5.浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。
上面提到,渲染完毕后会触发load事件,那么你能分清楚load事件与DOMContentLoaded事件的先后么?
- 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。 (譬如如果有async加载的脚本就不一定完成)
- 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。 (渲染完毕了)
- 顺序是:DOMContentLoaded -> load
css加载是否会阻塞dom树渲染?
这里说的是头部引入css的情况
首先,我们都知道:css是由单独的下载线程异步下载的。
- css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
- 但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)
这可能也是浏览器的一种优化机制。
因为你加载css的时候,可能会修改下面DOM节点的样式,
如果css加载不阻塞render树渲染的话,那么当css加载完之后,
render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。
所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,
在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。
普通图层和复合图层
渲染步骤中就提到了composite概念。
可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层
首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)
其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层。
然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)
可以简单理解下:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒
可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息
如何变成复合图层(硬件加速)
将该元素变成一个复合图层,就是传说中的硬件加速技术
-
最常用的方式:translate3d、translateZ
-
opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
-
will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层), 作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)
-
video iframe canvas webgl 等元素
-
譬如以前的flash插件
。 absolute和硬件加速的区别 可以看到,absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。 所以,就算absolute中信息改变时不会改变普通文档流中render树, 但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。 (浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的) 。 复合图层的作用? 一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能 但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡 。硬件加速时请使用index 使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染 具体的原理时这样的: webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低, 那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的), 会默认变为复合层渲染,如果处理不当会极大的影响性能 简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意
浏览器兼容性问题
普及:浏览器的兼容性问题,往往是个别浏览器(没错,就是那个与众不同的浏览器)对于一些标准的定义不一致导致的。俗话说:没有IE就没有伤害。
Normalize.css
不同浏览器的默认样式存在差异,可以使用 Normalize.css 抹平这些差异。当然,你也可以定制属于自己业务的 reset.css
<link href="https://cdn.bootcss.com/normalize/7.0.0/normalize.min.css" rel="stylesheet">
简单粗暴法
* { margin: 0; padding: 0; }
html5shiv.js
解决 ie9 以下浏览器对 html5 新增标签不识别的问题。
<!--[if lt IE 9]>
<script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
respond.js
解决 ie9 以下浏览器不支持 CSS3 Media Query 的问题。
<script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
picturefill.js
解决 IE 9 10 11 等浏览器不支持 <picture> 标签的问题
<script src="https://cdn.bootcss.com/picturefill/3.0.3/picturefill.min.js"></script>
IE 条件注释
IE 的条件注释仅仅针对IE浏览器,对其他浏览器无效
IE 属性过滤器(较为常用的hack方法)
针对不同的 IE 浏览器,可以使用不同的字符来对特定的版本的 IE 浏览器进行样式控制
浏览器 CSS 兼容前缀
-o-transform:rotate(7deg); // Opera
-ms-transform:rotate(7deg); // IE
-moz-transform:rotate(7deg); // Firefox
-webkit-transform:rotate(7deg); // Chrome
transform:rotate(7deg); // 统一标识语句
a 标签的几种 CSS 状态的顺序
很多新人在写 a 标签的样式,会疑惑为什么写的样式没有效果,或者点击超链接后,hover、active 样式没有效果,其实只是写的样式被覆盖了
正确的a标签顺序应该是:==love hate==
. link:平常的状态
. visited:被访问过之后
. hover:鼠标放到链接上的时候
. active:链接被按下的时候
完美解决 Placeholder
<input type="text" value="Name *" onFocus="this.value = '';" onBlur="if (this.value == '') {this.value = 'Name *';}">
清除浮动 最佳实践
.fl { float: left; }
.fr { float: right; }
.clearfix:after { display: block; clear: both; content: ""; visibility: hidden; height: 0; }
.clearfix { zoom: 1; }
BFC 解决边距重叠问题
当相邻元素都设置了 margin 边距时,margin 将取最大值,舍弃小值。为了不让边距重叠,可以给子元素加一个父元素,并设置该父元素为 BFC:overflow: hidden;
<div class="box" id="box">
<p>Lorem ipsum dolor sit.</p>
<div style="overflow: hidden;">
<p>Lorem ipsum dolor sit.</p>
</div>
<p>Lorem ipsum dolor sit.</p>
</div>
IE6 双倍边距的问题
设置 ie6 中设置浮动,同时又设置 margin,会出现双倍边距的问题
display: inline;
解决 IE9 以下浏览器不能使用 opacity
opacity: 0.5;
filter: alpha(opacity = 50);
filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50);
解决 IE6 不支持 fixed 绝对定位以及IE6下被绝对定位的元素在滚动的时候会闪动的问题
*html, *html body {
background-image: url(about:blank);
background-attachment: fixed;
}
*html #menu {
position: absolute;
top: expression(((e=document.documentElement.scrollTop) ? e : document.body.scrollTop) + 100 + 'px');
}
IE6 背景闪烁的问题
问题:链接、按钮用 CSSsprites 作为背景,在 ie6 下会有背景图闪烁的现象。原因是 IE6 没有将背景图缓存,每次触发 hover 的时候都会重新加载
解决:可以用 JavaScript 设置 ie6 缓存这些图片:
document.execCommand("BackgroundImageCache", false, true);
解决在 IE6 下,列表与日期错位的问题
日期<span> 标签放在标题 <a> 标签之前即可
解决 IE6 不支持 min-height 属性的问题
min-height: 350px;
_height: 350px;
让 IE7 IE8 支持 CSS3 background-size属性
由于 background-size 是 CSS3 新增的属性,所以 IE 低版本自然就不支持了,但是老外写了一个 htc 文件,名叫
background-size polyfill,使用该文件能够让 IE7、IE8 支持 background-size 属性。其原理是创建一个 img
元素插入到容器中,并重新计算宽度、高度、left、top 等值,模拟 background-size 的效果。
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
background-image: url('img/37.png');
background-repeat: no-repeat;
background-size: cover;
-ms-behavior: url('css/backgroundsize.min.htc');
behavior: url('css/backgroundsize.min.htc');
}
IE6-7 line-height 失效的问题
问题:在ie 中 img 与文字放一起时,line-height 不起作用
解决:都设置成 float
width:100%
width:100% 这个东西在 ie 里用很方便,会向上逐层搜索 width 值,忽视浮动层的影响.
Firefox 下搜索至浮动层结束,如此,只能给中间的所有浮动层加 width:100%才行,累啊。
opera 这点倒学乖了,跟了 ie
cursor:hand
显示手型 cursor: hand,ie6/7/8、opera 都支持,但是safari 、 ff 不支持
cursor: pointer;
td 自动换行的问题
问题:table 宽度固定,td 自动换行
解决:设置 Table 为 table-layout: fixed,td 为 word-wrap: break-word
键盘事件 keyCode 兼容性写法
var inp = document.getElementById('inp')
var result = document.getElementById('result')
function getKeyCode(e) {
e = e ? e : (window.event ? window.event : "")
return e.keyCode ? e.keyCode : e.which
}
inp.onkeypress = function(e) {
result.innerHTML = getKeyCode(e)
}
求窗口大小的兼容写法
// 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)
// 1600 * 525
var client_w = document.documentElement.clientWidth || document.body.clientWidth;
var client_h = document.documentElement.clientHeight || document.body.clientHeight;
// 网页内容实际宽高(包括工具栏和滚动条等边线)
// 1600 * 8
var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;
// 网页内容实际宽高 (不包括工具栏和滚动条等边线)
// 1600 * 8
var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;
// 滚动的高度
var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;
DOM 事件处理程序的兼容写法(能力检测)
var eventshiv = {
// event兼容
getEvent: function(event) {
return event ? event : window.event;
},
// type兼容
getType: function(event) {
return event.type;
},
// target兼容
getTarget: function(event) {
return event.target ? event.target : event.srcelem;
},
// 添加事件句柄
addHandler: function(elem, type, listener) {
if (elem.addEventListener) {
elem.addEventListener(type, listener, false);
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, listener);
} else {
// 在这里由于.与'on'字符串不能链接,只能用 []
elem['on' + type] = listener;
}
},
// 移除事件句柄
removeHandler: function(elem, type, listener) {
if (elem.removeEventListener) {
elem.removeEventListener(type, listener, false);
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, listener);
} else {
elem['on' + type] = null;
}
},
// 添加事件代理
addAgent: function (elem, type, agent, listener) {
elem.addEventListener(type, function (e) {
if (e.target.matches(agent)) {
listener.call(e.target, e); // this 指向 e.target
}
});
},
// 取消默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
// 阻止事件冒泡
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
网站前端性能优化
1.浏览器渲染页面的过程
CSS为什么要放到<head>里面、js放到</body>前面,以及js的异步加载(async、defer)等优化。
2.减少HTTP请求
. CSS/JS 合并打包
. 小图标等用iconfont代替
. 使用base64格式的图片:有些小图片,可能色彩比较复杂,这个时候再用iconfont就有点不合适了,此时可以将
其转化为base64格式(不能缓存),直接嵌在src中,比如webpack的url-loader设置limit参数即可
. 减少静态资源的体积
3.减少静态资源的体积
压缩静态资源:合并打包的js、css文件体积一般会比较大,一些图片也会比较大,这个时候必须要压缩处理。前后端
分离项目,不论是gulp还是webpack,都有相应的工具包。针对个别图片,有时候也可以单独拿出来处理,我个人经常
使用这个网站 tinypng.com/ 在线压缩
编写高效率的CSS:因为现在项目里面基本上都在使用CSS预处理器,Less、SaaS、Stylus等等,这导致了某些初级前端的
滥用:嵌套5、6层,甚者能达到7、8层,吓死个人!嵌套这么深,影响浏览器查找选择器的速度不说,这也一定程度上
产出了很多冗余的字节,这个要改、要提醒,一般建议嵌套3层即可。
服务端开启gzip压缩:大招,最近刚知晓,真是太牛逼了,一般的css、js文件能压缩60、70%,当然,这个比率可以设定
的。前后端分离,如果前端部署用node、express作服务器的话,使用中间件compression即可开启gzip压缩:
// server.js
var express = require('express');
var compress = require('compression');
var app = express();
app.use(compress());
4.使用缓存
设置Http Header里面缓存相关的字段,做进一步的优化。
5.脚本加载的优化
<1>动态加载
所谓动态加载脚本就是利用javascript代码来加载脚本,通常是手工创建script元素,然后等到HTML文档解析
完毕后插入到文档中去。这样就可以很好地控制脚本加载的时机,从而避免阻塞问题。
function loadJS(src) {
const script = document.createElement('script');
script.src = src;
document.getElementsByTagName('head')[0].appendChild(script);
}
loadJS('http://example.com/scq000.js');
<2>异步加载
我们都知道,在计算机程序中同步的模式会产生阻塞问题。所以为了解决同步解析脚本会阻塞浏览器渲染的问题,
采用异步加载脚本就成为了一种好的选择。利用脚本的async和defer属性就可以实现这种需求:
<script type="text/javascript" src="./a.js" async></script>
<script type="text/javascript" src="./b.js" defer></script>
虽然利用了这两个属性的script标签都可以实现异步加载,同时不阻塞脚本解析。但是使用async属性的脚本执行
顺序是不能得到保证的。而使用defer属性的脚本执行顺序可以得到保证。另一方面,defer属性是在html文档解
析完成后,DOMContentLoaded事件之前就会执行js。async一旦加载完js后就会马上执行,最迟不超过window.onload
事件。所以,如果脚本没有操作DOM等元素,或者与DOM时候加载完成无关,直接使用async脚本就好。如果需要DOM,
就只能使用defer了。
<3>解决异步加载脚本的问题
上面介绍的异步加载脚本并不是十分完美的。如何处理加载过程中这些脚本的互相依赖关系,就成了实现异步加载
过程中所需要考虑的问题。一方面,对于页面中那些独立的脚本,如用户统计等插件就可以放心大胆地使用异步加载
。而另一方面,对于那些确实需要处理依赖关系的脚本,业界已经有很成熟的解决方案了。如采用AMD规范的RequireJS
,甚至有采用了hack技术(通过欺骗浏览器下载但不执行脚本)的labjs(已过时)。如果你熟悉promise的话,就知道
这是在JS中处理异步的一种强有力的工具。下面以promise技术来实现处理异步脚本加载过程中de的依赖问题:
// 执行脚本
function exec(src) {
const script = document.createElement('script');
script.src = src;
// 返回一个独立的promise
return new Promise((resolve, reject) => {
var done = false;
script.onload = script.onreadystatechange = () => {
if (!done && (!script.readyState || script.readyState === "loaded" || script.readyState === "complete")) {
done = true;
// 避免内存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
}
script.onerror = reject;
document.getElementsByTagName('head')[0].appendChild(script);
});
}
function asyncLoadJS(dependencies) {
return Promise.all(dependencies.map(exec));
}
asyncLoadJS(['https://code.jquery.com/jquery-2.2.1.js', 'https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js']).then(() => console.log('all done'));
可以看到,我们针对每个脚本依赖都会创建一个promise对象来管理其状态。采用动态插入脚本的方式来管理脚本,然后
利用脚本onload和onreadystatechange(兼容性处理)事件来监听脚本是否加载完成。一旦加载完毕,就会触发promise的
resovle方法。最后,针对依赖的处理,是promise的all方法,这个方法只有在所有promise对象都resolved的时候才会触
发resolve方法,这样一来,我们就可以确保在执行回调之前,所有依赖的脚本都已经加载并执行完毕。
<4>懒加载(lazyload)
懒加载是一种按需加载的方式,也通常被称为延迟加载。主要思想是通过延迟相关资源的加载,从而提高页面的加载
和响应速度。在这里主要介绍两种实现懒加载的技术:虚拟代理技术以及惰性初始化技术。
a.虚拟代理加载
所谓虚拟代理加载,即为真正加载的对象事先提供一个代理或者说占位符。最常见的场景是在图片的懒加载中,
先用一种loading的图片占位,然后再用异步的方式加载图片。等真正图片加载完成后就填充进图片节点中去。
// 页面中的图片url事先先存在其data-src属性上
const lazyLoadImg = function() {
const images = document.getElementsByTagName('img');
for(let i = 0; i < images.length; i++) {
if(images[i].getAttribute('data-src')) {
images[i].setAttribute('src', images[i].getAttribute('data-src'));
images[i].onload = () => images[i].removeAttribute('data-src');
}
}
}
b.惰性初始化
惰性初始模式是在程序设计过程中常用的一种设计模式。顾名思义,这个模式就是一种将代码初始化的时机推迟
(特别是那些初始化消耗较大的资源),从而来提升性能的技术。
jQuery中大名鼎鼎的ready方法就用到了这项技术,其目的是为了在页面DOM元素加载完成后就可以做相应的操作,
而不需要等待所有资源加载完毕后。与浏览器中原生的onload事件相比,可以更加提前地介入对DOM的干涉。当
页面中包含大量图片等资源时,这个方法就显出它的好处了。在jQuery内部的实现原理上,它会设置一个标志位
来判断页面是否加载完毕,如果没有加载完成,会将要执行的函数缓存起来。当页面加载完毕后,再一一执行。
这样一来,就将原本应该马上执行的代码,延迟到页面加载完毕后再执行。
6.利用webpack实现脚本加载优化
针对懒加载,webpack也提供了十分友好的支持。这里主要介绍两种方式。
<1 .import()方法
我们知道,在原生es6的语法中,提供了import和export的方式来管理模块。而其import关键字是被设置成静态
的,因此不支持动态绑定。不过在es6的stage3规范中,引入了一个新的方法import()使得动态加载模块成为
可能。所以,你可以在项目中使用这样的代码:
$('#button').click(function() {
import('./dialog.js')
.then(dialog => {
//do something
})
.catch(err => {
console.log('模块加载错误');
});
});
//或者更优雅的写法
$('#button').click(async function() {
const dialog = await import('./dialog.js');
//do something with dialog
});
由于该语法是基于promise的,所以如果需要兼容旧浏览器,请确保在项目中使用es6-promise或者
promise-polyfill。同时,如果使用的是babel,需要添加syntax-dynamic-import插件。
<2 .require.ensure
require.ensure与import()类似,同样也是基于promise的异步加载模块的一种方法。这是在
webpack 1.x时代官方提供的懒加载方案。现在,已经被import()语法取代了。为了文章的完整性,
这里也做一些介绍。
在webpack编译过程中,会静态地解析require.ensure中的模块,并将其添加到一个单独的chunk中,
从而实现代码的按需加载。
语法如下:
require.ensure(dependencies: String[], callback: function(require), errorCallback:
function(error), chunkName: String)
一个十分常见的例子是在写单页面应用的时候,使用该技术实现基于不同路由的按需加载:
const routes = [
{path: '/comment', component: r => require.ensure([], r(require('./Comment')), 'comment')}
];
6.预加载
用户在具体的页面使用过程中的体验也很重要。如果能够通过预判用户的行为,提前加载所需要的资源,则可以快速地
响应用户的操作,从而打造更加良好的用户体验。另一方面,通过提前发起网络请求,也可以减少由于网络过慢导致的
用户等待时间。因此,“预加载”的技术就闪亮登场了。
<1>preload规范
preload 是w3c新出的一个标准。利用link的rel属性来声明相关“proload",从而实现预加载的目的。就像这样:
<link rel="preload" href="example.js" as="script">
其中rel属性是用来告知浏览器启用preload功能,而as属性是用来明确需要预加载资源的类型,这个资源类型
不仅仅包括js脚本(script),还可以是图片(image),css(style),视频(media)等等。浏览器检测到这个属性
后,就会预先加载资源。
这个规范目前兼容性方面还不是很好,所以可以先稍微了解一下。webpack现在也已经有相关的插件,如果感兴趣
的话,请移步preload-webpack-plugin。
<2>DNS Prefetch 预解析
还有一个可以优化网页速度的方式是利用dns的预解析技术。同preload类似,DNSPrefetch在网络层面上优化了
资源加载的速度。我们知道,针对DNS的前端优化,主要分为减少DNS的请求次数,还有就是进行DNS预先获取。
DNS prefetch就是为了实现这后者。其用法也很简单,只要在link标签上加上对应的属性就行了。
<meta http-equiv="x-dns-prefetch-control" content="on" /> /* 这是用来告知浏览器当前页面要做DNS预解析 */
<link rel="dns-prefetch" href="//example.com">
前端SEO
为什么要做优化:提高网站的权重,增强搜索引擎友好度,以达到提高排名,增加流量,改善(潜在)用户体验,促进销售的作用。
怎么实现:
1、网站结构布局优化:尽量简单、开门见山,提倡扁平化结构
一般而言,建立的网站结构层次越少,越容易被“蜘蛛”抓取,也就容易被收录。一般中小型网站目录结构超过三级
,“蜘蛛”便不愿意往下爬了。并且根据相关数据调查:如果访客经过跳转3次还没找到需要的信息,很可能离开。
因此,三层目录结构也是体验的需要。为此我们需要做到:
(1)控制首页链接数量
网站首页是权重最高的地方,如果首页链接太少,没有“桥”,“蜘蛛”不能继续往下爬到内页,直接影响网站收录
数量。但是首页链接也不能太多,一旦太多,没有实质性的链接,很容易影响用户体验,也会降低网站首页的权
重,收录效果也不好。
(2)扁平化的目录层次
尽量让“蜘蛛”只要跳转3次,就能到达网站内的任何一个内页
(3)导航优化
导航应该尽量采用文字方式,也可以搭配图片导航,但是图片代码一定要进行优化,<img>标签必须添加“alt”
和“title”属性,告诉搜索引擎导航的定位,做到即使图片未能正常显示时,用户也能看到提示文字。其次,在
每一个网页上应该加上面包屑导航,好处:从用户体验方面来说,可以让用户了解当前所处的位置以及当前页面
在整个网站中的位置,帮助用户很快了解网站组织形式,从而形成更好的位置感,同时提供了返回各个页面的接
口,方便用户操作;对“蜘蛛”而言,能够清楚的了解网站结构,同时还增加了大量的内部链接,方便抓取,降低
跳出率。
(4)网站的结构布局---不可忽略的细节
页面头部:logo及主导航,以及用户的信息。
页面主体:左边正文,包括面包屑导航及正文;右边放热门文章及相关文章,好处:留住访客,让访客多停留,
对“蜘蛛”而言,这些文章属于相关链接,增强了页面相关性,也能增强页面的权重。
页面底部:版权信息和友情链接。
(5)利用布局,把重要内容HTML代码放在最前
搜索引擎抓取HTML内容是从上到下,利用这一特点,可以让主要代码优先读取,广告等不重要代码放在下边。
(6)控制页面的大小,减少http请求,提高网站的加载速度。
一个页面最好不要超过100k,太大,页面加载速度慢。当速度很慢时,用户体验不好,留不住访客,并且一旦
超时,“蜘蛛”也会离开。
2、网页代码优化
(1)突出重要内容---合理的设计title、description和keywords
<title>标题:只强调重点即可,尽量把重要的关键词放在前面,关键词不要重复出现,尽量做到每个页面的
<title>标题中不要设置相同的内容。
<meta keywords>标签:关键词,列举出几个页面的重要关键字即可,切记过分堆砌。
<meta description>标签:网页描述,需要高度概括网页内容,切记不能太长,过分堆砌关键词,每个页面也要有所不同。
(2)语义化书写HTML代码,符合W3C标准
尽量让代码语义化,在适当的位置使用适当的标签,用正确的标签做正确的事。让阅读源码者和“蜘蛛”都一目了然。
比如:h1-h6 是用于标题类的,<nav>标签是用来设置页面主导航,列表形式的代码使用ul或ol,重要的文字使用
strong等。
(3)<a>标签:页内链接,要加 “t知道。而外部链接,链接到其他网站的,则需要加上 el="nofollow" 属性,
告诉 “蜘蛛” 不要爬,因为一旦“蜘蛛”爬了外部链接之后,就不会再回来了。
<a href="https://www.360.cn" title="360安全中心" class="logo"></a>
(4)正文标题要用<h1>标签:h1标签自带权重“蜘蛛” 认为它最重要,一个页面有且最多只能有一个H1标签,放在该页面最重要的标题上面,如首页的logo上可以加H1标签。
副标题用<h2>标签, 而其它地方不应该随便乱用 h 标题标签。
(5)<img>应使用 "alt" 属性加以说明
<img src="cat.jpg" width="300" height="200" alt="猫" />
(6)表格应该使用<caption>表格标题标签
caption 元素定义表格标题。caption 标签必须紧随 table 标签之后,您只能对每个表格定义一<table border='1'>
<caption>表格标题</caption>
<tbody>
<tr>
<td>apple</td>
<td>100</td>
</tr>
<tr>
<td>banana</td>
<td>200</td>
</tr>
</tbody>
</table>
(7)<br>标签:只用于文本内容的换行,比如:
<p>
第一行文字内容<br/>
第二行文字内容<br/>
第三行文字内容
</p>
(8)重要内容不要用JS输出,因为“蜘蛛”不会读取JS里的内容,所以重要内容必须放在HTML里。
(9)尽量少使用iframe框架,因为“蜘蛛”一般不会读取其中的内容。
(10)谨慎使用 display:none :对于不想显示的文字内容,应当设置z-index或缩进设置成足够大的负数偏离出浏览器之外。因为搜索引擎会过滤掉display:none其中的内容。
3、前端网站性能优化
(1)减少http请求数量
a.CSS Sprites
国内俗称CSS精灵,这是将多张图片合并成一张图片达到减少HTTP请求的一种解决方案,可以通过CSS的
background属性来访问图片内容。这种方案同时还可以减少图片总字节数。
b.合并CSS和JS文件
现在前端有很多工程化打包工具,如:grunt、gulp、webpack等。为了减少 HTTP
请求数量,可以通过这些工具再发布前将多个CSS或者多个JS合并成一个文件。
c.采用lazyload
称懒加载,可以控制网页上的内容在一开始无需加载,不需要发请求,等到用户操作真正需要的时候立即加载
出内容。这样就控制了网页资源一次性请求数量。
(2)控制资源文件加载优先级
浏览器在加载HTML内容时,是将HTML内容从上至下依次解析,解析到link或者script标签就会加载href或者src对应
链接内容,为了第一时间展示页面给用户,就需要将CSS提前加载,不要受 JS 加载影响。
(3)尽量外链CSS和JS(结构、表现和行为的分离),保证网页代码的整洁,也有利于日后维护
<link rel="stylesheet" href="asstes/css/style.css" />
<script src="assets/js/main.js"></script>
(4)利用浏览器缓存
浏览器缓存是将网络资源存储在本地,等待下次请求该资源时,如果资源已经存在就不需要到服务器重新请求该资源,
直接在本地读取该资源。
(5)减少重排(Reflow)
基本原理:重排是DOM的变化影响到了元素的几何属性(宽和高),浏览器会重新计算元素的几何属性,会使渲染树
中受到影响的部分失效,浏览器会验证DOM树上的所有其它结点的visibility属性,这也是Reflow低效的原因。如果
Reflow的过于频繁,CPU使用率就会急剧上升。
减少Reflow,如果需要在DOM操作时添加样式,尽量使用 增加class属性,而不是通过style操作样式。
(6)减少 DOM 操作
(7)图标使用IconFont替换
(8)不使用CSS表达式,会影响效率
(9)使用CDN网络缓存,加快用户访问速度,减轻服务器压力
(10)启用GZIP压缩,浏览速度变快,搜索引擎的蜘蛛抓取信息量也会增大
(11)伪静态设置
如果是动态网页,可以开启伪静态功能,让蜘蛛“误以为”这是静态网页,因为静态网页比较合蜘蛛的胃口,如果url中带有关键词效果更好。
动态地址:http://www.360.cn/index.php
伪静态地址:http://www.360.cn/index.html
正确认识SEO,不过分SEO,网站还是以内容为主。
http协议
1. HTTP 报文结构是怎样的?
对于 TCP 而言,在传输的时候分为两个部分:TCP头和数据部分。
而 HTTP 类似,也是header + body的结构,具体而言:
起始行 + 头部 + 空行 + 实体
由于 http 请求报文和响应报文是有一定区别,因此我们分开介绍。
起始行
对于请求报文来说,起始行类似下面这样:
GET /home HTTP/1.1
也就是方法 + 路径 + http版本。
对于响应报文来说,起始行一般张这个样:
HTTP/1.1 200 OK
响应报文的起始行也叫做状态行。由http版本、状态码和原因三部分组成。
值得注意的是,在起始行中,每两个部分之间用空格隔开,最后一个部分后面应该接一个换行,严格遵循ABNF语法规范。
头部
展示一下请求头和响应头在报文中的位置:
不管是请求头还是响应头,其中的字段是相当多的,而且牵扯到http非常多的特性,这里就不一一列举的,重点看看这些头部字段的格式:
- 字段名不区分大小写
- 字段名不允许出现空格,不可以出现下划线_
- 字段名后面必须紧接着:
空行
很重要,用来区分开头部和实体。
问: 如果说在头部中间故意加一个空行会怎么样?
那么空行后的内容全部被视为实体。
实体
就是具体的数据了,也就是body部分。请求报文对应请求体, 响应报文对应响应体
2. 如何理解 HTTP 的请求方法?
有哪些请求方法?
- http/1.1规定了以下请求方法(注意,都是大写):
- GET: 通常用来获取资源
- HEAD: 获取资源的元信息
- POST: 提交数据,即上传数据
- PUT: 修改数据
- DELETE: 删除资源(几乎用不到)
- CONNECT: 建立连接隧道,用于代理服务器
- OPTIONS: 列出可对资源实行的请求方法,用来跨域请求
- TRACE: 追踪请求-响应的传输路径
GET 和 POST 有什么区别?
- 从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
- 从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
- 从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
- 从幂等性的角度,GET是幂等的,而POST不是。(幂等表示执行相同的操作,结果也是相同的)
- 从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服 务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
3: 如何理解 URI?
URI, 全称为(Uniform Resource Identifier), 也就是统一资源标识符,它的作用很简单,就是区分互联网上不同的资源。
URI 的结构
URI 真正最完整的结构是这样的。
- scheme 表示协议名,比如http, https, file等等。后面必须和://连在一起。
- user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。
- host:port表示主机名和端口。
- path表示请求路径,标记资源所在位置。
- query表示查询参数,为key=val这种形式,多个键值对之间用&隔开。
- fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置。
举个例子:
https://www.baidu.com/s?wd=HTTP&rsv_spt=1
这个 URI 中,https即scheme部分,www.baidu.com为host:port部分(注意,http 和 https
的默认端口分别为80、443),/s为path部分,而wd=HTTP&rsv_spt=1就是query部分。
URI 编码
URI 只能使用ASCII, ASCII 之外的字符是不支持显示的,而且还有一部分符号是界定符,如果不加以处理就会导致解析出错。
因此,URI 引入了编码机制,将所有非 ASCII 码字符和界定符转为十六进制字节值,然后在前面加个%。
如,空格被转义成了%20,三元被转义成了%E4%B8%89%E5%85%83。
4: 如何理解 HTTP 状态码?
1xx: 表示目前是协议处理的中间状态,还需要后续操作。
2xx: 表示成功状态。
3xx: 重定向状态,资源位置发生变动,需要重新请求。
4xx: 请求报文有误。
5xx: 服务器端发生错误。
1xx
101 Switching Protocols。在HTTP升级为WebSocket的时候,如果服务器同意变更,就会发送状态码 101。
2xx
200 OK是见得最多的成功状态码。通常在响应体中放有数据。
204 No Content含义与 200 相同,但响应头后没有 body 数据。
206 Partial Content顾名思义,表示部分内容,它的使用场景为 HTTP 分块下载和断点续传,当然也会带上相应的响应头字段Content-Range。
3xx
301 Moved Permanently即永久重定向,对应着302 Found,即临时重定向。
比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。
而如果只是暂时不可用,那么直接返回302即可,和301不同的是,浏览器并不会做缓存优化。
304 Not Modified: 当协商缓存命中时会返回这个状态码。详见浏览器缓存
4xx
400 Bad Request: 开发者经常看到一头雾水,只是笼统地提示了一下错误,并不知道哪里出错了。
403 Forbidden: 这实际上并不是请求报文出错,而是服务器禁止访问,原因有很多,比如法律禁止、信息敏感。
404 Not Found: 资源未找到,表示没在服务器上找到相应的资源。
405 Method Not Allowed: 请求方法不被服务器端允许。
406 Not Acceptable: 资源无法满足客户端的条件。
408 Request Timeout: 服务器等待了太长时间。
409 Conflict: 多个请求发生了冲突。
413 Request Entity Too Large: 请求体的数据过大。
414 Request-URI Too Long: 请求行里的 URI 太大。
429 Too Many Request: 客户端发送的请求过多。
431 Request Header Fields Too Large请求头的字段内容太大。
5xx
500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。
501 Not Implemented: 表示客户端请求的功能还不支持。
502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。
503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。
5.简要概括一下 HTTP 的特点?HTTP 有哪些缺点?
HTTP 特点
HTTP 的特点概括如下:
(1).灵活可扩展,主要体现在两个方面。一个是语义上的自由,只规定了基本格式,比如空格分隔单词,换行分隔字段,
其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任
意数据,非常方便。
(2).可靠传输。HTTP 基于 TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。
(3).请求-应答。也就是一发一收、有来有回,当然这个请求方和应答方不单单指客户端和服务器之间,如果
某台服务器作为代理来连接后端的服务端,那么这台服务器也会扮演请求方的角色。
(4).无状态。这里的状态是指通信过程的上下文信息,而每次 http 请求都是独立、无关的,默认不需要保留状
态信息。
HTTP 缺点
无状态
在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是
http 的缺点了。
但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。
明文传输
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI
陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。
队头阻塞问题
当 http 开启长连接时,共用一个TCP连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,
其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。接下来会有一小节讨论这个问题。
6: 对 Accept 系列字段了解多少?
对于Accept系列字段的介绍分为四个部分: 数据格式、压缩方式、支持语言和字符集。
数据格式
上一节谈到 HTTP 灵活的特性,它支持非常多的数据格式,那么这么多格式的数据一起到达客户端,客户端怎么知道
它的格式呢?
首先需要介绍一个标准——MIME(Multipurpose Internet Mail Extensions, 多用途互联网邮件扩展)。它首先用在电子邮件系统中,让邮件可以发任意类型的数据,这对于
HTTP 来说也是通用的。
HTTP 从MIME type取了一部分来标记报文 body 部分的数据类型,这些类型体现在Content-Type这个字段,当然这是针对于发送端而言,接收端想要收到特定类型的数据,也可以用Accept字段。
具体而言,这两个字段的取值可以分为下面几类:
text: text/html, text/plain, text/css 等
image: image/gif, image/jpeg, image/png 等
audio/video: audio/mpeg, video/mp4 等
application: application/json, application/javascript, application/pdf, application/octet-stream
压缩方式
当然一般这些数据都是会进行编码压缩的,采取什么样的压缩方式就体现在了发送方的Content-Encoding
字段上, 同样的,接收什么样的压缩方式体现在了接受方的Accept-Encoding字段上。这个字段的取值有下面几种:
gzip: 当今最流行的压缩格式
deflate: 另外一种著名的压缩格式
br: 一种专门为 HTTP 发明的压缩算法
// 发送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gzip
支持语言
对于发送方而言,还有一个Content-Language字段,在需要实现国际化的方案当中,可以用来指定支持的语言,
在接受方对应的字段为Accept-Language。如:
// 发送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en
字符集
最后是一个比较特殊的字段, 在接收端对应为Accept-Charset,指定可以接受的字符集,而在发送端并没有对应的
Content-Charset, 而是直接放在了Content-Type中,以charset属性指定。如:
// 发送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8
7. 对于定长和不定长的数据,HTTP 是怎么传输的?
定长包体
对于定长包体而言,发送端在传输的时候一般会带上 Content-Length, 来指明包体的长度。
我们用一个nodejs服务器来模拟一下:
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10);
res.write("helloworld");
}
})
server.listen(8081, () => {
console.log("成功启动");
})
启动后访问: localhost:8081。
浏览器中显示如下:
helloworld
我们试着把这个长度设置的小一些:
res.setHeader('Content-Length', 8);
重启服务,再次访问,现在浏览器中内容如下:
hellowor
那后面的ld哪里去了呢?实际上在 http 的响应体中直接被截去了。
然后我们试着将这个长度设置得大一些:
res.setHeader('Content-Length', 12);
此时浏览器显示如下:页面无法正常运行
直接无法显示了。可以看到Content-Length对于 http 传输过程起到了十分关键的作用,如果设置不当可以直接导致传输失败。
不定长包体
上述是针对于定长包体,那么对于不定长包体而言是如何传输的呢? 这里就必须介绍另外一个 http 头部字段了:
Transfer-Encoding: chunked
表示分块传输数据,设置这个字段后会自动产生两个效果:
Content-Length 字段会被忽略
基于长连接持续推送动态内容
我们依然以一个实际的例子来模拟分块传输,nodejs 程序如下: const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');
res.write("<p>来啦</p>");
setTimeout(() => {
res.write("第一次传输<br/>");
}, 1000);
setTimeout(() => {
res.write("第二次传输");
res.end()
}, 2000);
}
})
server.listen(8009, () => {
console.log("成功启动");
})
8.HTTP 如何处理大文件的传输?
对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,
严重影响用户体验。因此,HTTP 针对这一场景,采取了范围请求的解决方案,允许客户端仅仅请求一个资源的一部分。
如何支持
当然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:
Accept-Ranges: none
用来告知客户端这边是支持范围请求的。
Range 字段拆解
而对于客户端而言,它需要指定请求哪一部分,通过Range这个请求头字段确定,格式为bytes=x-y。接下来就来讨论一下 这个 Range 的书写格式:
0-499表示从开始到第 499 个字节。
500- 表示从第 500 字节到文件终点。
-100表示文件的最后100个字节。
服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回416错误码,否则读取相应片段,返回206状态码。
同时,服务器需要添加Content-Range字段,这个字段的格式根据请求头中Range字段的不同而有所差异。
具体来说,请求单段数据和请求多段数据,响应头是不一样的。
举个例子:
// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39
接下来我们就分别来讨论着两种情况。
单段数据
接下来我们看看多段请求的情况。得到的响应会是下面这个形式: HTTP/1.1 206 Partial Content Content-Type: multipart/byteranges; boundary=00000010101 Content-Length: 189 Connection: keep-alive Accept-Ranges: bytes
--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96
i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96
eex jspy e
--00000010101--
这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101, 它代表了信息量是这样的:
请求一定是多段数据请求
响应体中的分隔符是 00000010101
因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上--表示结束。
以上就是 http 针对大文件传输所采用的手段。
9.HTTP 中如何处理表单数据的提交?
在 http 中,有两种主要的表单提交的方式,体现在两种不同的Content-Type取值:
application/x-www-form-urlencoded
multipart/form-data
由于表单提交一般是POST请求,很少考虑GET,因此这里我们将默认提交的数据放在请求体中。
application/x-www-form-urlencoded
对于application/x-www-form-urlencoded格式的表单内容,有以下特点:
其中的数据会被编码成以&分隔的键值对
字符以URL编码方式编码。
如:
/ 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最终形式)
"a%3D1%26b%3D2"
multipart/form-data
对于multipart/form-data而言:
.请求头中的Content-Type字段会包含boundary,且boundary的值有浏览器默认指定。例: Content-Type:
multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe。
.数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP
头部描述子包体,如Content-Type,在最后的分隔符会加上--表示结束。
相应的请求体是下面这样:
Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
小结
值得一提的是,multipart/form-data 格式最大的特点在于:每一个表单元素都是独立的资源表述。另外,你可能在写业务的过程中,并没有注意到其中还有boundary的存在,如果你打开抓包工具,确实可以看到不同的表单元素被拆分开了,之所以在平时感觉不到,是以为浏览器和 HTTP 给你封装了这一系列操作。
而且,在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间。
10.HTTP1.1 如何解决 HTTP 的队头阻塞问题?
什么是 HTTP 队头阻塞?
从前面的小节可以知道,HTTP 传输是基于请求-应答的模式进行的,报文必须是一发一收,但值得注意的是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是著名的HTTP队头阻塞问题。
并发连接
对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。在RFC2616规定过客户端最多并发 2 个连接,不过事实上在现在的浏览器标准中,这个上限要多很多,Chrome 中是 6 个
域名分片
一个域名不是可以并发 6 个长连接吗?那我就多分几个域名。 比如 content1.sanyuan.com 、content2.sanyuan.com。 这样一个sanyuan.com域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了,事实上也更好地解决了队头阻塞的问题。
11.对 Cookie 了解多少?
Cookie 简介
前面说到了 HTTP 是一个无状态的协议,每次 http 请求都是独立、无关的,默认不需要保留状态信息。但有时候需要保存一些状态,怎么办呢?
HTTP 为此引入了 Cookie。Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面板的Application这一栏可以看到)。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。而服务端可以通过响应头中的Set-Cookie字段来对客户端写入Cookie。举例如下:
// 请求头
Cookie: a=xxx;b=xxx
// 响应头
Set-Cookie: a=xxx
set-Cookie: b=xxx
Cookie 属性
生存周期
Cookie 的有效期可以通过Expires和Max-Age两个属性来设置。
Expires即过期时间
Max-Age用的是一段时间间隔,单位是秒,从浏览器收到报文开始计算。
若 Cookie 过期,则这个 Cookie 会被删除,并不会发送给服务端。
作用域
关于作用域也有两个属性: Domain和path, 给 Cookie 绑定了域名和路径,在发送请求之前,发现域名或者路径和这两个属性不匹配,那么就不会带上 Cookie。值得注意的是,对于路径来说,/表示域名下的任意路径都允许使用 Cookie
安全相关
如果带上Secure,说明只能通过 HTTPS 传输 cookie。
如果 cookie 字段带上HttpOnly,那么说明只能通过 HTTP 协议传输,不能通过 JS 访问,这也是预防 XSS 攻击的重要手段。
相应的,对于 CSRF 攻击的预防,也有SameSite属性。
SameSite可以设置为三个值,Strict、Lax和None。
a. 在Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当
中请求才能携带 Cookie,在其他网站请求都不能。
b. 在Lax模式,就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带
Cookie,其他情况均不能。
c. 在None模式下,也就是默认模式,请求会自动携带上 Cookie。
Cookie 的缺点
- 容量缺陷。Cookie 的体积上限只有4KB,只能用来存储少量的信息。
- 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的
Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。但可以通过Domain
和Path指定作用域来解决。
- 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,
在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在HttpOnly为 false 的情况下,Cookie 信息能直
接通过 JS 脚本来读取。
12.如何理解 HTTP 代理?
我们知道在 HTTP 是基于请求-响应模型的协议,一般由客户端发请求,服务器来进行响应。
当然,也有特殊情况,就是代理服务器的情况。引入代理之后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份
那代理服务器到底是用来做什么的呢?
功能
1.负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP都是多少,客户端是不知
道的。因此,这个代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台
源服务器的负载尽量平均。当然,这样的算法有很多,包括随机算法、轮询、一致性hash、LRU(最近最少
使用)等等,不过这些算法并不是本文的重点,大家有兴趣自己可以研究一下。
2.保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进
行过滤,对非法 IP 限流,这些都是代理服务器的工作。
3.缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得而不用到源服务器那里
相关头部字段
Via
代理服务器需要标明自己的身份,在 HTTP 传输中留下自己的痕迹,怎么办呢?
通过Via字段来记录。举个例子,现在中间有两台代理服务器,在客户端发送请求后会经历这样一个过程:
客户端 -> 代理1 -> 代理2 -> 源服务器
在源服务器收到请求后,会在请求头拿到这个字段:
Via: proxy_server1, proxy_server2
而源服务器响应时,最终在客户端会拿到这样的响应头:
Via: proxy_server2, proxy_server1
可以看到,Via中代理的顺序即为在 HTTP 传输中报文传达的顺序。
X-Forwarded-For
字面意思就是为谁转发, 它记录的是请求方的IP地址(注意,和Via区分开,X-Forwarded-For记录的是请求方这一个IP)。
X-Real-IP
是一种获取用户真实 IP 的字段,不管中间经过多少代理,这个字段始终记录最初的客户端的IP。
相应的,还有X-Forwarded-Host和X-Forwarded-Proto,分别记录客户端(注意哦,不包括代理)的域名和协议名。
X-Forwarded-For产生的问题
前面可以看到,X-Forwarded-For这个字段记录的是请求方的 IP,这意味着每经过一个不同的代理,这个字段的名字都要变,从客户端到代理1,这个字段是客户端的 IP,从代理1到代理2,这个字段就变为了代理1的 IP。 但是这会产生两个问题:
意味着代理必须解析 HTTP 请求头,然后修改,比直接转发数据性能下降。
在 HTTPS 通信加密的过程中,原始报文是不允许修改的。
由此产生了代理协议,一般使用明文版本,只需要在 HTTP 请求行上面加上这样格式的文本即可: // PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口 PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222 GET / HTTP/1.1 ... 复制代码这样就可以解决X-Forwarded-For带来的问题了。