浏览器环境概述
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 工作原理
正常的网页加载流程是这样的
- 浏览器一边下载HTML网页,一边开始解析。也就是说不等下载完,就开始解析。
- 解析过程中,浏览器发现
<script>
元素,就暂定解析,把网页渲染的控制权转交给JS引擎。 - 如果
<script>
元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码 - JS引擎执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页。
加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后(JS代码可以修改DOM),再继续渲染。
解析和执行CSS,也会产生阻塞。Firefox浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行
2.2 defer属性
解决脚本文件下载阻塞网页渲染的问题,<script>
加入defer属性,延迟脚本的执行,等到DOM加载生成后,再执行脚本。其属性的运行流程如下:
- 浏览器开始解析HTML网页
- 解析过程中,发现带有
defer
属性的<script>
元素 - 浏览器继续往下解析HTML网页,同时并行下载
<script>
元素加载的外部脚本。 - 浏览器完成解析HTML网页,此时再回头执行已经下载完成的脚本
2.3 async属性
其作用是使用另一个进程下载脚本,下载时不会阻塞渲染
- 浏览器开始解析HTML网页
- 解析过程中,发现带有
async
的<script>
标签 - 浏览器继续往下解析HTML网页,同时并行下载
<script>
标签中的外部脚本 - 脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本
- 脚本执行完毕,浏览器恢复解析HTMl网页
一般来说,如果脚本之间没有依赖关系,就是用async属性,如果脚本之间有依赖关系,就使用defer属性。同时使用以async属性为主。
3. JS的动态加载
如果不指定协议,浏览器默认HTTP协议下载。但是有时候会希望根据页面本身的协议来决定加载协议
<script src="//example.js"></script>
4. 浏览器的组成
4.1 渲染引擎
其作用是将网页代码渲染为用户视觉可以感知的平面文档。
- chrome:Blink引擎
- safai:WebKit引擎
- firefox:Gecko引擎
渲染引擎处理网页,通常分为四个阶段
- 解析代码:HTML解析为DOM,CSS代码解析为CSS Object Model
- 对象合成:将DOM和CSSOM合成一个渲染树
- 布局:计算出渲染树的布局
- 绘制:将渲染树绘制到屏幕
上述四步并非严格顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。
重流和重绘
渲染树转换为网页布局,称为“布局流”,布局显示到页面的过程是“绘制”。
优化渲染:
- 读取或写入DOM要一次性
- 缓存DOM信息
- 用CSS class 一次性改变样式
- 使用
documentFragment
操作DOM - 动画使用
absolute
或fixed
定位 - 只在必要时才显示隐藏元素
- 使用虚拟DOM库
- 使用
window.requestAnimationFrame()
,因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流
4.2 JS引擎
其主要作用是,读取网页中的JS代码,对其处理后运行。
浏览器内部对JS的处理如下:
- 读取代码,进行词法分析,将代码分解成词元
- 对词元进行语法分析,将代码整理成”语法树“
- 使用翻译器,将代码转为字节码
- 使用字节码解释器,将字节码转为机器码
现在浏览器:字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存。
字节码不能直接运行,而是运行在一个虚拟机上,一般也把虚拟机称为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.cookie
、XMLHttpRequest
对象和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
-
网页添加一个
<script>
,向服务器请求一个脚本,这不受同源政策限制(直接作为代码运行),<script src="http://baidu?callbak=bar"></script>
请求的脚本网址有一个callback
,用来告诉服务器,客户端的回调函数名bar
。 -
服务器收到请求后,拼接一个字符串,将JSON数据放在函数名里面,作为字符串返回
bar({})
-
客户端会将服务器返回的字符串,作为代码解析,因为浏览器任务,这是
<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请求分两类:简单请求和非简单请求
简单请求同时满足以下条件(除此之外都是非简单请求):
- 请求方法是HEAD、GET、POST之一
- 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
字段,就知道出错了,抛出一个错误被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为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 非简单请求
预检请求
非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段类型是application/json
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为“预检”。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求。这是为了防止这些新增请求,对传统的没有CORS支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量的DELETE
和PUT
请求,这些传统的表单不可能发出跨域请求。
预检请求的回应
服务器收到“预检”请求之后,检查Origin
、Access-Control-Request-Method
和Access-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.sessionStorage
和window.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
引起的,该属性返回nullStorageEvent.newValue
:表示新的键值StorageEvent.oldValue
:表示旧的键值StorageEvent.storageArea
:返回键值对所在的整个对象。也就是说,可以拿到当前域名储存的所有键值对StorageEvent.url
:表示原始触发storage事件的哪个网页的网址
注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。可以通过这种机制,实现多个窗口之间的通信。
来源:CSDN
作者:牛肉烧酒
链接:https://blog.csdn.net/qq_37625230/article/details/103580697