本小节主要记录了网页中一些其他的知识点:
- 模型
- 网页对象
- 存储对象
- 通信对象
一、模型
1.1 浏览器模型
浏览器的核心是两部分:渲染引擎和 JavaScript引擎。
1.1.1 渲染引擎
渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。渲染引擎处理网页,通常分成四个阶段:
- 解析代码:HTML代码解析为 DOM,CSS代码解析为 CSSOM(CSS Object Model)。
- 对象合成:将 DOM和 CSSOM合成一棵渲染树。
- 布局:计算出渲染树的布局(layout)。
- 绘制:将渲染树绘制到屏幕。
以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以会看到这种情况:网页的HTML代码还没下载完,但浏览器已经显示出内容。
来看一下非常重要的重流和重绘。渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。
页面生成以后,脚本操作和样式表操作都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,如改变窗口大小、页面滚动、设置鼠标悬停
a:hover
效果、在输入框中输入文本等。重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。
作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如重绘
table
布局和flex
布局,开销都会比较大。
1.1.2 JavaScript引擎
JavaScript引擎的主要作用是,读取网页中的JavaScript代码,对其处理后运行。早期浏览器内部对 JavaScript 的处理过程如下:
- 对代码进行词法分析,将代码分解成词元。
- 对词元进行语法分析,将代码整理成语法树。
- 使用翻译器,将代码转为字节码。
- 使用解释器,将字节码转为机器码。
逐行全部解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写 JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。通常一个程序被经常用到的只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。
字节码不能直接运行,而是运行在一个虚拟机之上,一般也把虚拟机称为JavaScript引擎。并非所有的 JavaScript虚拟机运行时都有字节码,有的JavaScript虚拟机基于源码,即只要有可能,就通过 JIT编译器直接把源码编译成机器码运行,省略字节码步骤。这样做的目的,是为了尽可能地优化代码、提高性能。
1.2 script 元素
浏览器加载 JavaScript 脚本,主要通过<script>
元素完成。正常的网页加载流程是这样的:
- 渲染引擎一边下载HTML网页,一边开始解析。
- 解析过程中发现
<script>
元素后暂停解析,把网页的控制权转交给JavaScript引擎。- JavaScript引擎下载脚本后执行代码。
- 执行完毕后,把网页控制权交还渲染引擎,恢复往下解析 HTML 网页。
浏览器会并行下载脚本,但是执行还是按照定义的顺序来执行。
较好的做法是将<script>
标签都放在页面底部而不是头部的原因如下:
- 如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
- 因为在 DOM 结构生成之前就调用 DOM 节点JS会报错,如果脚本都在网页尾部加载就不存在这个问题。
对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。
在<script>
标签中还常见如下属性:
defer
属性的作用是延迟脚本的执行,等到 DOM 加载生成后再执行脚本。该属性的运行流程如下:
- 渲染引擎一边下载HTML网页,一边开始解析。
- 解析过程中,发现带有
defer
属性的<script>
标签,继续往下解析 HTML 网页,同时开启另一个进程并行下载<script>
元素加载的外部脚本。- 渲染引擎完成解析 HTML 网页,此时再交由JavaScript引擎执行已经下载完成的脚本。
defer
属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,依旧保证脚本的执行顺序。
async
属性的作用是使用另一个进程下载脚本,下载时不会阻塞渲染。该属性的运行流程如下:
- 渲染引擎一边下载HTML网页,一边开始解析。
- 解析过程中,发现带有
async
属性的<script>
标签,继续往下解析 HTML 网页,同时开启另一个进程并行下载<script>
标签中的外部脚本。- 脚本下载完成,渲染引擎暂停解析 HTML 网页,交由JavaScript引擎执行。
- 脚本执行完毕,JavaScript引擎交由渲染引擎恢复解析 HTML 网页。
async
属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。
一般来说,如果脚本之间有依赖关系,就使用defer
属性;如果脚本之间没有依赖关系,就使用async
属性。如果同时使用和defer
和async
属性,浏览器行为由async
属性决定。
二、网页对象
2.1 window
window
对象指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属。
对象属性
网页对象属性
-
window.document
:指向document
对象 -
window.location
:指向Location
对象 -
window.localStorage
:指向本地储存的localStorage数据 -
window.sessionStorage
:指向本地储存的sessionStorage数据
工具对象属性:
-
window.locationbar
:地址栏对象,对象的visible
属性是一个布尔值,表示该组件是否可见。 -
window.menubar
:菜单栏对象,对象的visible
属性是一个布尔值,表示该组件是否可见。 -
window.toolbar
:工具栏对象,对象的visible
属性是一个布尔值,表示该组件是否可见。 -
window.statusbar
:状态栏对象,对象的visible
属性是一个布尔值,表示该组件是否可见。
对象事件
window.onload
属性事件发生在文档在浏览器窗口加载完毕时
2.2 Location
Location
对象是浏览器提供的原生对象,提供URL相关的信息和操作方法。通过window.location
和document.location
属性,可以拿到这个对象。
对象属性
-
Location.href
:整个 URL。如果对它写入新的 URL 地址,浏览器会立刻跳转到这个新地址 -
Location.origin
:URL 的协议、主机名和端口。 -
Location.protocol
:当前URL的协议,包括冒号 -
Location.hostname
:当前URL的主机(不包括端口)。而Location.host包括端口 -
Location.port
:当前URL的端口号 -
Location.pathname
:URL 的路径部分,从根路径/
开始 -
Location.search
:查询字符串部分,从问号?
开始。
辅助方法
-
encodeURI()
方法用于转码整个URL。它的参数是一个字符串,代表整个 URL。 -
decodeURI()
方法用于整个URL的解码。它接受一个参数,就是转码后的URL。
三、存储对象
3.1 Cookie
Cookie是服务器保存在浏览器的一小段文本信息,浏览器每次向服务器发出请求,就会自动附上这段信息。不同浏览器对Cookie数量和大小的限制是不一样的。一般来说单个域名设置的 Cookie 不应超过30个,每个Cookie的大小不能超过4KB。超过限制以后Cookie将被忽略,不会被设置。
Cookie 有一些主要用途:
- 对话(session)管理:保存登录、购物车等需要记录的信息。
- 个性化信息:保存用户的偏好,比如网页的字体大小、背景色等等。
cookie的属性
cookie除了使用K-V形式设置Cookie的名字Cookie的值外,它还有以下属性:
-
Expires
属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个Cookie。它的值是UTC格式,可以使用Date.prototype.toUTCString()
进行格式转换。如果不设置该属性或设为null
,Cookie只在当前会话有效,浏览器窗口一旦关闭该Cookie就会被删除。 -
Max-Age
属性指定从现在开始Cookie存在的秒数,比如60 * 60 * 24 * 365
(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。如果同时指定了Expires
和Max-Age
,那么Max-Age
的值将优先生效。 -
Domain
属性指定浏览器发出HTTP请求时,哪些域名要附带这个Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个Cookie。如果指定了该属性,那么子域名也会附带这个 Cookie。 -
Path
属性指定浏览器发出HTTP请求时,哪些路径要附带这个Cookie。只要浏览器发现,Path
属性是HTTP请求路径的开头一部分,就会在头信息里面带上这个Cookie。比如PATH
属性是/
,那么请求/docs
路径也会包含它。 -
Secure
属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。 -
HttpOnly
属性指定该Cookie无法通过JS脚本拿到,主要让document.cookie
、XMLHttpRequest
和Request API
拿不到该属性。这样就防止该Cookie被脚本读到。只有浏览器发出HTTP请求时,才会带上该Cookie。
cookie的交互
cookie的生成:在浏览器保存Cookie,就要在HTTP回应的头信息里面,放置一个或多个Set-Cookie
字段。一个Set-Cookie
字段可同时包括该cookie的多个属性,没有次序的要求。这个过程是服务端手动处理的。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
cookie的发送:向浏览器发送Cookie,就要在HTTP回应的头信息里面,放置一个或多个Cookie
字段。一个Cookie
字段可同时包含多个 Cookie,使用分号分隔。这个过程是浏览器自动处理的。
Cookie: name=value; name2=value2; name3=value3
3.2 Storage
Storage接口用于脚本在浏览器保存数据,保存的数据都以“键值对”的形式存在。sessionStorage
保存的数据用于浏览器的一次会话,当会话结束数据被清空;localStorage
保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。目前每个域名的存储上限视浏览器而定,Chrome是2.5MB,Firefox和Opera是5MB。
对象属性
-
StorageEvent.storageArea
:返回键值对所在的整个对象。也就是说可以从这个属性上拿到当前域名储存的所有键值对。 -
StorageEvent.url
:表示原始触发storage事件的那个网页的网址。 -
StorageEvent.key
:表示发生变动的键名。如果storage事件是由clear()
方法引起,该属性返回null
。 -
StorageEvent.newValue
:表示新的键值。如果storage事件是由clear()
方法引起,该属性返回null
。 -
StorageEvent.oldValue
:表示旧的键值。如果该键值对是新增的,该属性返回null
。
对象方法
-
Storage.key()
接受一个整数作为参数(从0开始),返回该位置对应的键值。 -
Storage.getItem()
方法用于读取数据。它只有一个参数:键名。如果键名不存在,该方法返回null
。 -
Storage.setItem()
方法用于存入数据。它接受两个参数:第一个是键名;第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。 -
Storage.removeItem()
方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。 -
Storage.clear()
方法用于清除所有保存的数据。
四、通信对象
4.1 跨域通信
所谓“同源”指的是“三个相同”:
- 协议相同
- 域名相同
- 端口相同
常见的跨源通行方式有如下:
WebSocket
WebSocket是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。具体使用情况后续再补充。
JSONP
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单易用,老式浏览器全部支持,服务端改造非常小。去电就是JSONP只能发GET
请求。
- 第一步,网页添加一个
<script>
元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。
<script src="http://api.foo.com?callback=bar"></script>
注意请求的脚本网址有一个callback
参数(?callback=bar
),用来告诉服务器客户端的回调函数名称。
- 第二步,服务器收到请求后,拼接一个字符串,将JSON数据放在函数名里面,作为字符串返回(
bar({...})
)。 - 第三步,客户端会将服务器返回的字符串,作为代码解析。因为浏览器认为这是
script
标签请求的脚本内容。这时客户端只要定义了bar()
函数,就能在该函数体内,拿到服务器返回的JSON数据。作为参数的JSON数据被视为 JavaScript对象而不是字符串,因此避免了使用JSON.parse
的步骤。
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);
};
CORS
CORS是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,属于跨源AJAX请求的根本解决方法。它允许浏览器向跨域的服务器发出XMLHttpRequest
请求,从而克服了AJAX只能同源使用的限制。相比JSONP只能发GET
请求,CORS允许任何类型的请求。
CORS通信与普通的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求。因此实现 CORS 通信的关键是服务器。只要服务器实现了CORS接口就可跨域通信。
CORS 请求分成两类:简单请求和非简单请求。只要同时满足以下两大条件就属于简单请求,凡是不同时满足上面两个条件就属于非简单请求。非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
CORS简单请求
简单请求发送:
对于简单请求,浏览器直接发出CORS请求。具体来说就是在头信息之中,增加一个Origin
字段。
GET /cors HTTP/1.1
Host: api.alice.com
Origin: http://api.bob.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
简单请求响应:
服务器收到查询请求以后,检查了Origin
字段以后,确认允许跨源请求就可以做出回应,返回的响应会多出几个头信息字段。如果服务器否定了查询请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段,或者明确表示请求不符合条件。浏览器发现这个回应的头信息没有包含Access-Control-Allow-Origin
字段就知道出错了,从而抛出一个错误。该错误被XMLHttpRequest
的onerror
回调函数捕获。注意这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头:
Access-Control-Allow-Origin
:该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
来表示接受任意域名的请求。注意如果服务器要求浏览器发送Cookie,该字段就不能设为星号,必须指定明确的、与请求网页一致的域名。Access-Control-Allow-Credentials
:该字段可选。表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
即表示服务器明确许可,浏览器可以把Cookie包含在请求中,一起发给服务器。设为true
同时要求开发者必须在AJAX请求中打开withCredentials
属性,否则即使服务器要求发送 Cookie,浏览器也不会发送。Access-Control-Expose-Headers
:该字段可选。CORS请求时XMLHttpRequest
对象的getResponseHeader()
方法只能拿到服务器返回头的6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定getResponseHeader('FooBar')
可以返回FooBar
字段的值。
CORS非简单请求
非简单请求发送:
非简单请求的CORS请求,会在正式通信之前增加一次HTTP查询请求。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP方法和头信息字段。只有得到肯定答复后浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
这是为了防止这些新增的请求,对传统的没有CORS支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量
DELETE
和PUT
请求,这些传统的表单不可能跨域发出的请求。
4.2 XMLHttpRequest
XMLHttpRequest
对象是AJAX的主要接口,用于浏览器与服务器之间的通信。尽管名字里面有XML
和Http
,它实际上可以使用多种协议(比如file
或ftp
),发送任何格式的数据(包括字符串和二进制)。下面是XMLHttpRequest
对象简单用法的完整例子:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
// 通信成功时,状态值为4
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
xhr.open('GET', '/endpoint', true);
xhr.send(null);
对象属性
XMLHttpRequest.withCredentials
属性是一个布尔值,表示跨域请求时用户信息(比如Cookie和认证的HTTP头信息)是否会包含在请求之中,默认为false
,即向example.com
发出跨域请求时,不会发送example.com
设置在本机上的Cookie(如果有的话)。如果需要跨域AJAX请求发送Cookie,需要withCredentials
属性设为true
。注意同源的请求不需要设置这个属性。XMLHttpRequest.upload
属性返回一个对象,表示上传文件的相关事件。主要就是监听这个对象的各种事件。XMLHttpRequest.timeout
属性返回一个整数,表示多少毫秒后,如果请求仍然没有得到结果就会自动终止。如果该属性等于0,就表示没有时间限制。XMLHttpRequest.readyState
返回一个整数,表示实例对象的当前状态。它可能返回以下值:
- 0,表示XMLHttpRequest实例已经生成,但是实例的
open()
方法还没有被调用。- 1,表示
open()
方法已经调用,但是实例的send()
方法还没有调用,仍然可以使用实例的setRequestHeader()
方法,设定HTTP请求的头信息。- 2,表示实例的
send()
方法已经调用,并且服务器返回的头信息和状态码已经收到。- 3,表示正在接收服务器传来的数据体(body 部分)。这时如果实例的
responseType
属性等于text
或者空字符串,responseText
属性就会包含已经收到的部分信息。- 4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。
XMLHttpRequest.status
属性返回一个整数,表示服务器回应的HTTP状态码。XMLHttpRequest.responseType
属性是一个字符串,表示服务器返回数据的类型。XMLHttpRequest.response
属性表示服务器返回的数据体(即HTTP回应的 body 部分),它可能是任何数据类型(如字符串、对象、二进制对象等)。XMLHttpRequest.responseURL
属性是字符串,表示发送数据的服务器的网址。XMLHttpRequest.responseText
属性返回从服务器接收到的字符串。只有HTTP请求完成接收以后,该属性才会包含完整的数据。
对象事件
-
XMLHttpRequest.ontimeout
:timeout事件的监听函数 -
XMLHttpRequest.onloadstart
:loadstart事件(请求发出)的监听函数 -
XMLHttpRequest.onprogress
:progress事件(正在发送和加载数据)的监听函数 -
XMLHttpRequest.onloadend
:loadend 事件(请求完成,不管成功或失败)的监听函数 -
XMLHttpRequest.onabort
:abort 事件(请求中止,如用户调用了abort()
方法)的监听函数 -
XMLHttpRequest.onerror
:error 事件(请求失败)的监听函数 -
XMLHttpRequest.onload
:load事件(请求成功完成)的监听函数 -
XMLHttpRequest.onreadystatechange
:readystatechange事件(实例的readyState
属性变化)的监听函数
对象方法
-
XMLHttpRequest.open()
方法用于指定HTTP请求的参数,或者说初始化XMLHttpRequest实例对象。注意如果对使用过open()
方法的AJAX请求,再次使用这个方法,等同于调用abort()
,即终止请求。 -
XMLHttpRequest.overrideMimeType()
方法用来指定MIME类型,覆盖服务器返回的MIME类型,从而让浏览器进行不一样的处理。比如服务器返回的数据类型是text/xml
,由于种种原因浏览器解析不成功报错,这时就拿不到数据。为了拿到原始数据可以把MIME类型改成text/plain
,这样浏览器就不会去自动解析,从而可拿到原始文本。注意该方法必须在send()
方法之前调用。 -
XMLHttpRequest.setRequestHeader()
方法用于设置浏览器发送的HTTP请求的头信息。如果该方法多次调用且设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。注意该方法必须在send()
方法之前调用。 -
XMLHttpRequest.getResponseHeader()
方法返回HTTP头信息指定字段的值,如果还没有收到服务器回应或者指定字段不存在,则返回null
。 -
XMLHttpRequest.send()
方法用于实际发出HTTP请求。 -
XMLHttpRequest.abort()
方法用来终止已经发出的HTTP请求。调用这个方法以后,readyState
属性变为4,status
属性变为0。
至于其他的还有File对象、Blob对象、ArrayBuffer对象等,到时候随用随查吧。