第九章 浏览器模型

半世苍凉 提交于 2019-12-24 07:05:07

浏览器环境概述

1. 代码嵌入网页的方法

网页中嵌入JS代码,主要有三种方法:

(1)JS标签

<javascript>有个type属性,用来指定脚本类型,老式浏览器用text/javascript,新式浏览器用application/javascript

可以用JS标签加载外部脚本,如果脚本文件使用了非英语字符,还应该注明字符的编码charset="utf-8"

为了防止攻击者篡改外部脚本,JS标签允许设置一个integrity属性,写入该外部脚本的Hash签名,用来验证脚本的一致性。

(2)事件属性

网页元素的事件属性(比如onclick和onmouseover),可以写入JS代码。当指定事件发生时就会调用这些代码。

<button id="myBtn" onclick="console.log(this.id)">点击</button>

(3)URL协议

在URL位置写入 代码,使用的时候就会执行JS代码。

<a href="javascript: console.log('Hello')">点击</a>

2. script元素

2.1 工作原理

正常的网页加载流程是这样的

  1. 浏览器一边下载HTML网页,一边开始解析。也就是说不等下载完,就开始解析。
  2. 解析过程中,浏览器发现<script>元素,就暂定解析,把网页渲染的控制权转交给JS引擎。
  3. 如果<script>元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码
  4. JS引擎执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页。

加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后(JS代码可以修改DOM),再继续渲染。

解析和执行CSS,也会产生阻塞。Firefox浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行

2.2 defer属性

解决脚本文件下载阻塞网页渲染的问题,<script>加入defer属性,延迟脚本的执行,等到DOM加载生成后,再执行脚本。其属性的运行流程如下:

  1. 浏览器开始解析HTML网页
  2. 解析过程中,发现带有defer属性的<script>元素
  3. 浏览器继续往下解析HTML网页,同时并行下载<script>元素加载的外部脚本。
  4. 浏览器完成解析HTML网页,此时再回头执行已经下载完成的脚本

2.3 async属性

其作用是使用另一个进程下载脚本,下载时不会阻塞渲染

  1. 浏览器开始解析HTML网页
  2. 解析过程中,发现带有async<script>标签
  3. 浏览器继续往下解析HTML网页,同时并行下载<script>标签中的外部脚本
  4. 脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本
  5. 脚本执行完毕,浏览器恢复解析HTMl网页

一般来说,如果脚本之间没有依赖关系,就是用async属性,如果脚本之间有依赖关系,就使用defer属性。同时使用以async属性为主。

3. JS的动态加载

如果不指定协议,浏览器默认HTTP协议下载。但是有时候会希望根据页面本身的协议来决定加载协议

<script src="//example.js"></script>

4. 浏览器的组成

4.1 渲染引擎

其作用是将网页代码渲染为用户视觉可以感知的平面文档。

  • chrome:Blink引擎
  • safai:WebKit引擎
  • firefox:Gecko引擎

渲染引擎处理网页,通常分为四个阶段

  1. 解析代码:HTML解析为DOM,CSS代码解析为CSS Object Model
  2. 对象合成:将DOM和CSSOM合成一个渲染树
  3. 布局:计算出渲染树的布局
  4. 绘制:将渲染树绘制到屏幕

上述四步并非严格顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。

重流和重绘

渲染树转换为网页布局,称为“布局流”,布局显示到页面的过程是“绘制”。

优化渲染:

  • 读取或写入DOM要一次性
  • 缓存DOM信息
  • 用CSS class 一次性改变样式
  • 使用documentFragment操作DOM
  • 动画使用absolutefixed定位
  • 只在必要时才显示隐藏元素
  • 使用虚拟DOM库
  • 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流

4.2 JS引擎

其主要作用是,读取网页中的JS代码,对其处理后运行。

浏览器内部对JS的处理如下:

  1. 读取代码,进行词法分析,将代码分解成词元
  2. 对词元进行语法分析,将代码整理成”语法树“
  3. 使用翻译器,将代码转为字节码
  4. 使用字节码解释器,将字节码转为机器码

现在浏览器:字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存。

字节码不能直接运行,而是运行在一个虚拟机上,一般也把虚拟机称为JS引擎。并非所有的JS虚拟机运行时都有字节码,有的JS虚拟机基于源码,即只要有可能,就能通过即时编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如JAVA)的语言不尽相同。这样可以尽可能优化代码、提高性能

  • V8(chrome)
  • Nitro/JavaScript Core(Safai)
  • SpiderMonkey(Firefox)

Cookie

Cookie是服务器保存在浏览器的一小段文本信息,一般大小不能超过4kb。浏览器每次向服务器发出请求,就会自动附上这段信息。它主要保存状态信息:

  • 对话管理:保存登录、购物车等需要记录的信息
  • 个性化信息:保存用户的偏好,比如网页的字体大小、背景颜色
  • 追踪用户:记录和分析用户行为

客户端的存储应该使用Web storage API和IndexedDB。只有那些每次请求都需要让服务器知道的信息,才应该放在Cookie里面。

每个Cookie都有:名字、值(真正的数据)、到期时间、所属域名、生效路径。

1. Cookie与HTTP协议

Cookie由HTTP协议产生,也主要由HTTP使用。

服务器希望浏览器保存Cookie,就要在HTTP回应的头信息里面,放置一个Set-Cookie字段。Set-Cookie: name = value

如果服务器向改变一个早先设置的Cookie,必须同时满足四个条件:Cookie的key、domain、path、secure都匹配。

Cookie的发送

浏览器向服务器HTTP请求时,每个请求都会带上相应的Cookie。

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

服务器收到浏览器发来的Cookie,但无法知道Cookie的各种属性,比如何时过期;不知道是哪个域名设置的Cookie,一级或二级

2. Cookie的属性

Expires,Max-Age

Expires是UTC格式,可以指定一个具体的到期时间,到了指定的时间后,浏览器不再保存这个Cookie。可以使用Date.prototype.toUTCString()进行格式转换。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

如果不设置该属性,或者为null,浏览器窗口一旦关闭,当前Session结束,该Cookie会被删除。

Max-Age是指定Cookie存在的秒数,比如365*24*60*60一年。

Domain、Path

Domain属性指定浏览器发出HTTP请求时,那些域名要附带这个Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,子域名将不会带上这个Cookie。

Path属性指定浏览器发出HTTP请求时,哪些路径要附带这个Cookie。

Secure、HttpOnly

Secure属性指定浏览器只有在HTTPS加密协议下,才能将这个Cookie发送到服务器。

HttpOnly属性指定该Cookie无法通过JS脚本拿到,主要就是document.cookieXMLHttpRequest对象和Request API都拿不到该属性,这样就防止了该Cookie被脚本读到,只有浏览器发出HTTP请求时,才会带上Cookie。

SameSite

该属性用来防止CSRF攻击和用户追踪。Cookie往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确Cookie的HTTP请求,这就是CSRF攻击

https://wangdoc.com/javascript/bom/cookie.html

3. document.cookie

读取返回当前网页的所有Cookie。手动还原:

var cookies = document.cookie.split(';')
for(let i = 0; i < cookies.length; i++) { ...}

一次写入一个Cookiedocument.cookie = 'name=value',写入的时候,可以同时写入Cookie的属性document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";

删除一个现存 Cookie 的唯一方法,是设置它的expires属性为一个过去的日期。

document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';

XMLHttpRequest 对象

# 1. XMLHttpRequest 对象

AJAX(Asynchronous JavaScript and XML),指的是通过JS的异步通信,从服务器获取XML文档中提取数据,再更新当前的对应部分,而不用刷新整个页面。后来AJAX成为JS脚本发起HTTP通信的代名词(现在服务器返回的都是JSON格式数据)。AJAX只能向同源网址发出HTTP请求,不可跨域。

https://wangdoc.com/javascript/bom/xmlhttprequest.html

同源限制

1. 限制范围

为防止恶意的网站窃取数据,非同源的网址,有如下行为受限制

  • 无法读取非同源网页的Cookie、LocalStorage和IndexedDB
  • 无法接触非同源网页的DOM
  • 无法向非同源网址发送AJAX请求(可以发送,但浏览器会拒绝接受响应)

2. 规避限制

Cookie

如果两个网页的一级域名相同,只是次级域名不同,浏览器允许通过document.domain属性来检查是否同源(两个网页都要设置)

片段识别符

片段识别符指的是URL的#号后面的部分,如果只是改变片段标识符,页面不会重新刷新。

解决跨域窗口的通信问题:父窗口可以把信息,写入子窗口的片段标识符,子窗口通过监听hashchange事件得到通知。

var src = originURL + '#' + data
document.getElementById('myFrame').src = src

window.onhashchange = checkMessage
function checkMessage () {
    var message = window.location.hase
}

子窗口改变父窗口片段标识符parent.location.href = target + '#' + hash

window.postMessage()

跨文档通信(Cross-document messaging),使用window.postMessage方法(第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源),允许窗口通信,不论这两个窗口是否同源。

// 父窗口打开一个子窗口 并向其发送消息
var popup = window.open('http://bb.com', 'title')
popup.postMessage('hello world!', 'http: //bbb.com')

// 子窗口向父窗口发消息
window.opener.postMessage('Nice to see you''http://aaa.comh')

父子窗口都可以通过message事件,监听对方的消息

window.addEventListener('message', function (event) {
    console.log(event.data)
}, false)
// event.data 消息内容
// event.origin 消息发向的网址->判断信息是发给本窗口的
// event.source 发送消息的窗口

通过postMessage,读写其他窗口的LocalStorage也成为了可能。

window.onmessage = function (e) {
    if(e.origin !== 'http://bbb.com') return
    var payload = JSON.parse(e.data)
    localStorage.setItem(payload.key, JSON.stringify(payload.data))
}
//子窗将父窗口发来的消息,写入自己的LocalStorage
var win = document.getElementsByTagName('frame')[0].contentWindow
var obj = { name: 'Jack'}
win.postMessage(
	JSON.stringify({key: 'storage', data: obj}),
    'http://bbb.com'
)

加强版的子窗口接收消息的代码如下。

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

加强版的父窗口发送消息代码如下。

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(
  JSON.stringify({key: 'storage', method: 'set', data: obj}),
  'http://bbb.com'
);
// 读取对象
win.postMessage(
  JSON.stringify({key: 'storage', method: "get"}),
  "*"
);
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  console.log(JSON.parse(e.data).name);
};

3. AJAX

三种办法规避同源策略对AJAX请求的限制

3.1 JSONP

  1. 网页添加一个<script>,向服务器请求一个脚本,这不受同源政策限制(直接作为代码运行),

    <script src="http://baidu?callbak=bar"></script>请求的脚本网址有一个callback,用来告诉服务器,客户端的回调函数名bar

  2. 服务器收到请求后,拼接一个字符串,将JSON数据放在函数名里面,作为字符串返回bar({})

  3. 客户端会将服务器返回的字符串,作为代码解析,因为浏览器任务,这是<script>标签请求的脚本。客户端只要定义了bar()函数,就能在函数体内,拿到服务器返回的JSON数据。

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};
// foo({
	'ip': '8.8.8.8'
})

3.2 WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀,该协议不实行同源策略。

浏览器发出的 WebSocket 请求的头信息(摘自维基百科)。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

3.2 CORS跨资源分享

JSONP只能发GET请求,CORS允许任何类型的请求

https://wangdoc.com/javascript/bom/cors.html

CROS请求分两类:简单请求和非简单请求

简单请求同时满足以下条件(除此之外都是非简单请求):

  1. 请求方法是HEAD、GET、POST之一
  2. HTTP的头信息不超过以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

3.2.1 简单请求

基本流程

浏览器发现跨域AJAX请求是简单请求,就自动在头信息中,添加一个Origin字段。该字段说明本次请求来自哪个域(协议、域名、端口),服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,抛出一个错误被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie,默认情况Cookie不包括在CORS请求中
Access-Control-Expose-Headers: FooBar  // 可以返回`FooBar`字段的值
Content-Type: text/html; charset=utf-8

withCredentials属性

CORS请求默认不包含Cookie信息,是为了降低CSRF攻击的风险。

如果服务器要求浏览器发送Cookie,Access-Control-Allow-Origin就不能设置为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源策略,只有用服务器域名设置的Cookie才会上传,且(跨域)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

在AJAX请求中打开withCredentials属性

var xhr = ne XMLHttpRequest()
xhr.withCredentials = true

3.2.2 非简单请求

预检请求

非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为“预检”。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求。这是为了防止这些新增请求,对传统的没有CORS支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量的DELETEPUT请求,这些传统的表单不可能发出跨域请求。

预检请求的回应

服务器收到“预检”请求之后,检查OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段后,确认允许跨域请求,就做出回应。

Access-Control-Allow-Origin字段,表示“域名”可以请求数据;如果服务器否定了“预检”的请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段,或明确表示请求不符合条件。Access-Control-Allow-Origin字段不会包括发出请求的“域名”

浏览器的正常请求和回应

一旦服务器通过了“预检”请求,以后每次浏览器正常的CORS请求,就跟简单请求一样,会有一个Origin字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息的字段。

CORS与JSONP使用的目的相同,但是它比JSONP更强大(请求的类型),JSONP优势在于支持老式浏览器,以及可以向不支持CORS的网址请求数据。

Storage接口

1. 概述

Storage接口用于脚本在浏览器保存数据,两个对象部署了这个接口:window.sessionStoragewindow.localStorage

sessionStorage保存的数据用于浏览器的一次会话,当会话结束(通常是窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。每个域名的存储空间视浏览器而定。

2. 属性和方法

window.localStorage.length:返回保存的数据项个数

Storage.setItem(key, value):接收的参数都是字符串,存入浏览器;window.localStorage.key = 'value'直接赋值也可以

Storage.getItem(key):参数是键名,获取数据

Storage.removeItem(key):清楚某个键名对应的键值

Storage.clear():清除所有保存的数据

Storage.key():接受一个整数作为参数(从零开始),返回该位置对应的键值

window.sessionStorage.setItem('key', 'value')
window.sessionStorage.keu(0) // 'key'

2. Storage事件

Storage接口存储的数据发生变化时,会触发storage事件window.addEventListener('storage', onStorageChange)

实例对象有如下属性:

  • StorageEvent.key:表示发生变动的键名,如果事件是由clear引起的,该属性返回null
  • StorageEvent.newValue:表示新的键值
  • StorageEvent.oldValue:表示旧的键值
  • StorageEvent.storageArea:返回键值对所在的整个对象。也就是说,可以拿到当前域名储存的所有键值对
  • StorageEvent.url:表示原始触发storage事件的哪个网页的网址

注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。可以通过这种机制,实现多个窗口之间的通信。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!