XSS 攻擊指的是跨站腳本攻擊,是一種代碼注入攻擊。攻擊者通過在網(wǎng)站注入惡意腳本,使之在用戶的瀏覽器上運行,從而盜取用戶的信息如 cookie 等。
XSS 的本質是因為網(wǎng)站沒有對惡意代碼進行過濾,與正常的代碼混合在一起了,瀏覽器沒有辦法分辨哪些腳本是可信的,從而導致了惡意代碼的執(zhí)行。
攻擊者可以通過這種攻擊方式可以進行以下操作:
XSS 可以分為存儲型、反射型和 DOM 型:
1)存儲型 XSS 的攻擊步驟:
這種攻擊常?于帶有?戶保存數(shù)據(jù)的?站功能,如論壇發(fā)帖、商品評論、?戶私信等。
2)反射型 XSS 的攻擊步驟:
反射型 XSS 跟存儲型 XSS 的區(qū)別是:存儲型 XSS 的惡意代碼存在數(shù)據(jù)庫?,反射型 XSS 的惡意代碼存在 URL ?。
反射型 XSS 漏洞常?于通過 URL 傳遞參數(shù)的功能,如?站搜索、跳轉等。 由于需要?戶主動打開惡意的 URL 才能?效,攻擊者往往會結合多種?段誘導?戶點擊。
3)DOM 型 XSS 的攻擊步驟:
DOM 型 XSS 跟前兩種 XSS 的區(qū)別:DOM 型 XSS 攻擊中,取出和執(zhí)?惡意代碼由瀏覽器端完成,屬于前端JavaScript ?身的安全漏洞,?其他兩種 XSS 都屬于服務端的安全漏洞。
可以看到XSS危害如此之大, 那么在開發(fā)網(wǎng)站時就要做好防御措施,具體措施如下:
- CSP 指的是內容安全策略,它的本質是建立一個白名單,告訴瀏覽器哪些外部資源可以加載和執(zhí)行。我們只需要配置規(guī)則,如何攔截由瀏覽器自己來實現(xiàn)。
- 通常有兩種方式來開啟 CSP,一種是設置 HTTP 首部中的 Content-Security-Policy,一種是設置 meta 標簽的方式 ?
<meta http-equiv="Content-Security-Policy">
?
CSRF 攻擊指的是跨站請求偽造攻擊,攻擊者誘導用戶進入一個第三方網(wǎng)站,然后該網(wǎng)站向被攻擊網(wǎng)站發(fā)送跨站請求。如果用戶在被攻擊網(wǎng)站中保存了登錄狀態(tài),那么攻擊者就可以利用這個登錄狀態(tài),繞過后臺的用戶驗證,冒充用戶向服務器執(zhí)行一些操作。
CSRF 攻擊的本質是利用 cookie 會在同源請求中攜帶發(fā)送給服務器的特點,以此來實現(xiàn)用戶的冒充。
常見的 CSRF 攻擊有三種:
CSRF 攻擊可以使用以下方法來防護:
中間? (Man-in-the-middle attack, MITM) 是指攻擊者與通訊的兩端分別創(chuàng)建獨?的聯(lián)系, 并交換其所收到的數(shù)據(jù), 使通訊的兩端認為他們正在通過?個私密的連接與對?直接對話, 但事實上整個會話都被攻擊者完全控制。在中間?攻擊中,攻擊者可以攔截通訊雙?的通話并插?新的內容。
攻擊過程如下:
?絡劫持分為兩種:
(1)DNS劫持: (輸?京東被強制跳轉到淘寶這就屬于dns劫持)
(2)HTTP劫持: (訪問?歌但是?直有貪玩藍?的?告),由于http明?傳輸,運營商會修改你的http響應內容(即加?告)
DNS劫持由于涉嫌違法,已經被監(jiān)管起來,現(xiàn)在很少會有DNS劫持,?http劫持依然?常盛?,最有效的辦法就是全站HTTPS,將HTTP加密,這使得運營商?法獲取明?,就?法劫持你的響應內容。
從本質上說,進程和線程都是 CPU 工作時間片的一個描述:
進程是資源分配的最小單位,線程是CPU調度的最小單位。
一個進程就是一個程序的運行實例。詳細解釋就是,啟動一個程序的時候,操作系統(tǒng)會為該程序創(chuàng)建一塊內存,用來存放代碼、運行中的數(shù)據(jù)和一個執(zhí)行任務的主線程,我們把這樣的一個運行環(huán)境叫進程。進程是運行在虛擬內存上的,虛擬內存是用來解決用戶對硬件資源的無限需求和有限的硬件資源之間的矛盾的。從操作系統(tǒng)角度來看,虛擬內存即交換文件;從處理器角度看,虛擬內存即虛擬地址空間。
如果程序很多時,內存可能會不夠,操作系統(tǒng)為每個進程提供一套獨立的虛擬地址空間,從而使得同一塊物理內存在不同的進程中可以對應到不同或相同的虛擬地址,變相的增加了程序可以使用的內存。
進程和線程之間的關系有以下四個特點:
(1)進程中的任意一線程執(zhí)行出錯,都會導致整個進程的崩潰。
(2)線程之間共享進程中的數(shù)據(jù)。
(3)當一個進程關閉之后,操作系統(tǒng)會回收進程所占用的內存,**當一個進程退出時,操作系統(tǒng)會回收該進程所申請的所有資源;即使其中任意線程因為操作不當導致內存泄漏,當進程退出時,這些內存也會被正確回收。
(4)進程之間的內容相互隔離。**進程隔離就是為了使操作系統(tǒng)中的進程互不干擾,每一個進程只能訪問自己占有的數(shù)據(jù),也就避免出現(xiàn)進程 A 寫入數(shù)據(jù)到進程 B 的情況。正是因為進程之間的數(shù)據(jù)是嚴格隔離的,所以一個進程如果崩潰了,或者掛起了,是不會影響到其他進程的。如果進程之間需要進行數(shù)據(jù)的通信,這時候,就需要使用用于進程間通信的機制了。
Chrome瀏覽器的架構圖:
從圖中可以看出,最新的 Chrome 瀏覽器包括:
這些進程的功能:
所以,打開一個網(wǎng)頁,最少需要四個進程:1 個網(wǎng)絡進程、1 個瀏覽器進程、1 個 GPU 進程以及 1 個渲染進程。如果打開的頁面有運行插件的話,還需要再加上 1 個插件進程。
雖然多進程模型提升了瀏覽器的穩(wěn)定性、流暢性和安全性,但同樣不可避免地帶來了一些問題:
瀏覽器的渲染進程的線程總共有五種:
(1)GUI渲染線程
負責渲染瀏覽器頁面,解析HTML、CSS,構建DOM樹、構建CSSOM樹、構建渲染樹和繪制頁面;當界面需要重繪或由于某種操作引發(fā)回流時,該線程就會執(zhí)行。
注意:GUI渲染線程和JS引擎線程是互斥的,當JS引擎執(zhí)行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執(zhí)行。
(2)JS引擎線程
JS引擎線程也稱為JS內核,負責處理Javascript腳本程序,解析Javascript腳本,運行代碼;JS引擎線程一直等待著任務隊列中任務的到來,然后加以處理,一個Tab頁中無論什么時候都只有一個JS引擎線程在運行JS程序;
注意:GUI渲染線程與JS引擎線程的互斥關系,所以如果JS執(zhí)行的時間過長,會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。
(3)事件觸發(fā)線程
事件觸發(fā)線程屬于瀏覽器而不是JS引擎,用來控制事件循環(huán);當JS引擎執(zhí)行代碼塊如setTimeOut時(也可是來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件觸發(fā)線程中;當對應的事件符合觸發(fā)條件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理;
注意:由于JS的單線程關系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執(zhí)行);
(4)定時器觸發(fā)進程
定時器觸發(fā)進程即setInterval與setTimeout所在線程;瀏覽器定時計數(shù)器并不是由JS引擎計數(shù)的,因為JS引擎是單線程的,如果處于阻塞線程狀態(tài)就會影響記計時的準確性;因此使用單獨線程來計時并觸發(fā)定時器,計時完畢后,添加到事件隊列中,等待JS引擎空閑后執(zhí)行,所以定時器中的任務在設定的時間點不一定能夠準時執(zhí)行,定時器只是在指定時間點將任務添加到事件隊列中;
注意:W3C在HTML標準中規(guī)定,定時器的定時時間不能小于4ms,如果是小于4ms,則默認為4ms。
(5)異步http請求線程
(1)管道通信
管道是一種最基本的進程間通信機制。管道就是操作系統(tǒng)在內核中開辟的一段緩沖區(qū),進程1可以將需要交互的數(shù)據(jù)拷貝到這段緩沖區(qū),進程2就可以讀取了。
管道的特點:
(2)消息隊列通信
消息隊列就是一個消息的列表。用戶可以在消息隊列中添加消息、讀取消息等。消息隊列提供了一種從一個進程向另一個進程發(fā)送一個數(shù)據(jù)塊的方法。 每個數(shù)據(jù)塊都被認為含有一個類型,接收進程可以獨立地接收含有不同類型的數(shù)據(jù)結構??梢酝ㄟ^發(fā)送消息來避免命名管道的同步和阻塞問題。但是消息隊列與命名管道一樣,每個數(shù)據(jù)塊都有一個最大長度的限制。
使用消息隊列進行進程間通信,可能會收到數(shù)據(jù)塊最大長度的限制約束等,這也是這種通信方式的缺點。如果頻繁的發(fā)生進程間的通信行為,那么進程需要頻繁地讀取隊列中的數(shù)據(jù)到內存,相當于間接地從一個進程拷貝到另一個進程,這需要花費時間。
(3)信號量通信
共享內存最大的問題就是多進程競爭內存的問題,就像類似于線程安全問題。我們可以使用信號量來解決這個問題。信號量的本質就是一個計數(shù)器,用來實現(xiàn)進程之間的互斥與同步。例如信號量的初始值是 1,然后 a 進程來訪問內存1的時候,我們就把信號量的值設為 0,然后進程b 也要來訪問內存1的時候,看到信號量的值為 0 就知道已經有進程在訪問內存1了,這個時候進程 b 就會訪問不了內存1。所以說,信號量也是進程之間的一種通信方式。
(4)信號通信
信號(Signals )是Unix系統(tǒng)中使用的最古老的進程間通信的方法之一。操作系統(tǒng)通過信號來通知進程系統(tǒng)中發(fā)生了某種預先規(guī)定好的事件(一組事件中的一個),它也是用戶進程之間通信和同步的一種原始機制。
(5)共享內存通信
共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創(chuàng)建,但多個進程都可以訪問(使多個進程可以訪問同一塊內存空間)。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現(xiàn)進程間的同步和通信。
(6)套接字通信
上面我們說的共享內存、管道、信號量、消息隊列,他們都是多個進程在一臺主機之間的通信,那兩個相隔幾千里的進程能夠進行通信嗎?答是必須的,這個時候 Socket 這家伙就派上用場了,例如我們平時通過瀏覽器發(fā)起一個 http 請求,然后服務器給你返回對應的數(shù)據(jù),這種就是采用 Socket 的通信方式了。
所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處于這種僵持狀態(tài)時,若無外力作用,它們都將無法再向前推進。
系統(tǒng)中的資源可以分為兩類:
產生死鎖的原因:
(1)競爭資源
(2)進程間推進順序非法
若P1保持了資源R1,P2保持了資源R2,系統(tǒng)處于不安全狀態(tài),因為這兩個進程再向前推進,便可能發(fā)生死鎖。例如,當P1運行到P1:Request(R2)時,將因R2已被P2占用而阻塞;當P2運行到P2:Request(R1)時,也將因R1已被P1占用而阻塞,于是發(fā)生進程死鎖
產生死鎖的必要條件:
預防死鎖的方法:
實現(xiàn)多個標簽頁之間的通信,本質上都是通過中介者模式來實現(xiàn)的。因為標簽頁之間沒有辦法直接通信,因此我們可以找一個中介者,讓標簽頁和中介者進行通信,然后讓這個中介者來進行消息的轉發(fā)。通信方法如下:
Service Worker 是運行在瀏覽器背后的獨立線程,一般可以用來實現(xiàn)緩存功能。使用 Service Worker的話,傳輸協(xié)議必須為 HTTPS。因為 Service Worker 中涉及到請求攔截,所以必須使用 HTTPS 協(xié)議來保障安全。
Service Worker 實現(xiàn)緩存功能一般分為三個步驟:首先需要先注冊 Service Worker,然后監(jiān)聽到 install
事件以后就可以緩存需要的文件,那么在下次用戶訪問的時候就可以通過攔截請求的方式查詢是否存在緩存,存在緩存的話就可以直接讀取緩存文件,否則就去請求數(shù)據(jù)。以下是這個步驟的實現(xiàn):
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注冊成功')
})
.catch(function(err) {
console.log('servcie worker 注冊失敗')
})
}
// sw.js
// 監(jiān)聽 `install` 事件,回調中緩存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})
// 攔截所有請求事件
// 如果緩存中已經有請求的數(shù)據(jù)就直接用緩存,否則去請求數(shù)據(jù)
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
打開頁面,可以在開發(fā)者工具中的 Application
看到 Service Worker 已經啟動了:
在 Cache 中也可以發(fā)現(xiàn)所需的文件已被緩存:
瀏覽器緩存的全過程:
很多網(wǎng)站的資源后面都加了版本號,這樣做的目的是:每次升級了 JS 或 CSS 文件后,為了防止瀏覽器進行緩存,強制改變版本號,客戶端瀏覽器就會重新下載新的 JS 或 CSS 文件 ,以保證用戶能夠及時獲得網(wǎng)站的最新更新。
資源緩存的位置一共有 3 種,按優(yōu)先級從高到低分別是:
fetch
?函數(shù)獲取 數(shù)據(jù)。也就是說,如果沒有在 Service Worker 命中緩存,會根據(jù)緩存查找優(yōu)先級去查找數(shù)據(jù)。但是不管是從 Memory Cache 中還是從網(wǎng)絡請求中獲取的數(shù)據(jù),瀏覽器都會顯示是從 Service Worker 中獲取的內容。
Disk Cache:Push Cache 是 HTTP/2 中的內容,當以上三種緩存都沒有命中時,它才會被使用。并且緩存時間也很短暫,只在會話(Session)中存在,一旦會話結束就被釋放。其具有以下特點:
no-cache
? 和 ?no-store
? 的資源使用強緩存策略時,如果緩存資源有效,則直接使用緩存資源,不必再向服務器發(fā)起請求。
強緩存策略可以通過兩種方式來設置,分別是 http 頭信息中的 Expires 屬性和 Cache-Control 屬性。
(1)服務器通過在響應頭中添加 Expires 屬性,來指定資源的過期時間。在過期時間以內,該資源可以被緩存使用,不必再向服務器發(fā)送請求。這個時間是一個絕對時間,它是服務器的時間,因此可能存在這樣的問題,就是客戶端的時間和服務器端的時間不一致,或者用戶可以對客戶端時間進行修改的情況,這樣就可能會影響緩存命中的結果。
(2)Expires 是 http1.0 中的方式,因為它的一些缺點,在 HTTP 1.1 中提出了一個新的頭部屬性就是 Cache-Control 屬性,它提供了對資源的緩存的更精確的控制。它有很多不同的值,
Cache-Control
可設置的字段:
public
?:設置了該字段值的資源表示可以被任何對象(包括:發(fā)送請求的客戶端、代理服務器等等)緩存。這個字段值不常用,一般還是使用max-age=來精確控制;private
?:設置了該字段值的資源只能被用戶瀏覽器緩存,不允許任何代理服務器緩存。在實際開發(fā)當中,對于一些含有用戶信息的HTML,通常都要設置這個字段值,避免代理服務器(CDN)緩存;no-cache
?:設置了該字段需要先和服務端確認返回的資源是否發(fā)生了變化,如果資源未發(fā)生變化,則直接使用緩存好的資源;no-store
?:設置了該字段表示禁止任何緩存,每次都會向服務端發(fā)起新的請求,拉取最新的資源;max-age=
?:設置緩存的最大有效期,單位為秒;s-maxage=
?:優(yōu)先級高于max-age=,僅適用于共享緩存(CDN),優(yōu)先級高于max-age或者Expires頭;max-stale[=]
?:設置了該字段表明客戶端愿意接收已經過期的資源,但是不能超過給定的時間限制。一般來說只需要設置其中一種方式就可以實現(xiàn)強緩存策略,當兩種方式一起使用時,Cache-Control 的優(yōu)先級要高于 Expires。
no-cache和no-store很容易混淆:
如果命中強制緩存,我們無需發(fā)起新的請求,直接使用緩存內容,如果沒有命中強制緩存,如果設置了協(xié)商緩存,這個時候協(xié)商緩存就會發(fā)揮作用了。
上面已經說到了,命中協(xié)商緩存的條件有兩個:
max-age=xxx
? 過期了no-cache
?使用協(xié)商緩存策略時,會先向服務器發(fā)送一個請求,如果資源沒有發(fā)生修改,則返回一個 304 狀態(tài),讓瀏覽器使用本地的緩存副本。如果資源發(fā)生了修改,則返回修改后的資源。
協(xié)商緩存也可以通過兩種方式來設置,分別是 http 頭信息中的 Etag 和 Last-Modified 屬性。
(1)服務器通過在響應頭中添加 Last-Modified 屬性來指出資源最后一次修改的時間,當瀏覽器下一次發(fā)起請求時,會在請求頭中添加一個 If-Modified-Since 的屬性,屬性值為上一次資源返回時的 Last-Modified 的值。當請求發(fā)送到服務器后服務器會通過這個屬性來和資源的最后一次的修改時間來進行比較,以此來判斷資源是否做了修改。如果資源沒有修改,那么返回 304 狀態(tài),讓客戶端使用本地的緩存。如果資源已經被修改了,則返回修改后的資源。使用這種方法有一個缺點,就是 Last-Modified 標注的最后修改時間只能精確到秒級,如果某些文件在1秒鐘以內,被修改多次的話,那么文件已將改變了但是 Last-Modified 卻沒有改變,這樣會造成緩存命中的不準確。
(2)因為 Last-Modified 的這種可能發(fā)生的不準確性,http 中提供了另外一種方式,那就是 Etag 屬性。服務器在返回資源的時候,在頭信息中添加了 Etag 屬性,這個屬性是資源生成的唯一標識符,當資源發(fā)生改變的時候,這個值也會發(fā)生改變。在下一次資源請求時,瀏覽器會在請求頭中添加一個 If-None-Match 屬性,這個屬性的值就是上次返回的資源的 Etag 的值。服務接收到請求后會根據(jù)這個值來和資源當前的 Etag 的值來進行比較,以此來判斷資源是否發(fā)生改變,是否需要返回資源。通過這種方式,比 Last-Modified 的方式更加精確。
當 Last-Modified 和 Etag 屬性同時出現(xiàn)的時候,Etag 的優(yōu)先級更高。使用協(xié)商緩存的時候,服務器需要考慮負載平衡的問題,因此多個服務器上資源的 Last-Modified 應該保持一致,因為每個服務器上 Etag 的值都不一樣,因此在考慮負載平衡時,最好不要設置 Etag 屬性。
總結:
強緩存策略和協(xié)商緩存策略在緩存命中時都會直接使用本地的緩存副本,區(qū)別只在于協(xié)商緩存會向服務器發(fā)送一次請求。它們緩存不命中時,都會向服務器發(fā)送請求來獲取資源。在實際的緩存機制中,強緩存策略和協(xié)商緩存策略是一起合作使用的。瀏覽器首先會根據(jù)請求的信息判斷,強緩存是否命中,如果命中則直接使用資源。如果不命中則根據(jù)頭信息向服務器發(fā)起請求,使用協(xié)商緩存,如果協(xié)商緩存命中的話,則服務器不返回資源,瀏覽器直接使用本地資源的副本,如果協(xié)商緩存不命中,則瀏覽器返回最新的資源給瀏覽器。
對于瀏覽器的緩存,主要針對的是前端的靜態(tài)資源,最好的效果就是,在發(fā)起請求之后,拉取相應的靜態(tài)資源,并保存在本地。如果服務器的靜態(tài)資源沒有更新,那么在下次請求的時候,就直接從本地讀取即可,如果服務器的靜態(tài)資源已經更新,那么我們再次請求的時候,就到服務器拉取新的資源,并保存在本地。這樣就大大的減少了請求的次數(shù),提高了網(wǎng)站的性能。這就要用到瀏覽器的緩存策略了。
所謂的瀏覽器緩存指的是瀏覽器將用戶請求過的靜態(tài)資源,存儲到電腦本地磁盤中,當瀏覽器再次訪問時,就可以直接從本地加載,不需要再去服務端請求了。
使用瀏覽器緩存,有以下優(yōu)點:
瀏覽器的主要功能是將用戶選擇的 web 資源呈現(xiàn)出來,它需要從服務器請求資源,并將其顯示在瀏覽器窗口中,資源的格式通常是 HTML,也包括 PDF、image 及其他格式。用戶用 URI(Uniform Resource Identifier 統(tǒng)一資源標識符)來指定所請求資源的位置。
HTML 和 CSS 規(guī)范中規(guī)定了瀏覽器解釋 html 文檔的方式,由 W3C 組織對這些規(guī)范進行維護,W3C 是負責制定 web 標準的組織。但是瀏覽器廠商紛紛開發(fā)自己的擴展,對規(guī)范的遵循并不完善,這為 web 開發(fā)者帶來了嚴重的兼容性問題。
瀏覽器可以分為兩部分,shell 和 內核。其中 shell 的種類相對比較多,內核則比較少。也有一些瀏覽器并不區(qū)分外殼和內核。從 Mozilla 將 Gecko 獨立出來后,才有了外殼和內核的明確劃分。
瀏覽器內核主要分成兩部分:
最開始渲染引擎和 JS 引擎并沒有區(qū)分的很明確,后來 JS 引擎越來越獨立,內核就傾向于只指渲染引擎。
(1) IE 瀏覽器內核:Trident 內核,也是俗稱的 IE 內核;
(2) Chrome 瀏覽器內核:統(tǒng)稱為 Chromium 內核或 Chrome 內核,以前是 Webkit 內核,現(xiàn)在是 Blink內核;
(3) Firefox 瀏覽器內核:Gecko 內核,俗稱 Firefox 內核;
(4) Safari 瀏覽器內核:Webkit 內核;
(5) Opera 瀏覽器內核:最初是自己的 Presto 內核,后來加入谷歌大軍,從 Webkit 又到了 Blink 內核;
(6) 360瀏覽器、獵豹瀏覽器內核:IE + Chrome 雙內核;
(7) 搜狗、遨游、QQ 瀏覽器內核:Trident(兼容模式)+ Webkit(高速模式);
(8) 百度瀏覽器、世界之窗內核:IE 內核;
(9) 2345瀏覽器內核:好像以前是 IE 內核,現(xiàn)在也是 IE + Chrome 雙內核了;
(10)UC 瀏覽器內核:這個眾口不一,UC 說是他們自己研發(fā)的 U3 內核,但好像還是基于 Webkit 和 Trident ,還有說是基于火狐內核。
值得注意的是,和?多數(shù)瀏覽器不同,Chrome 瀏覽器的每個標簽?都分別對應?個呈現(xiàn)引擎實例。每個標簽?都是?個獨?的進程。
瀏覽器渲染主要有以下步驟:
大致過程如圖所示:
注意:這個過程是逐步完成的,為了更好的用戶體驗,渲染引擎將會盡可能早的將內容呈現(xiàn)到屏幕上,并不會等到所有的html 都解析完成之后再去構建和布局 render 樹。它是解析完一部分內容就顯示一部分內容,同時,可能還在通過網(wǎng)絡下載其余內容。
(1)針對JavaScript:JavaScript既會阻塞HTML的解析,也會阻塞CSS的解析。因此我們可以對JavaScript的加載方式進行改變,來進行優(yōu)化:
(1)盡量將JavaScript文件放在body的最后
(2) body中間盡量不要寫 ?<script>
?標簽
(3)?<script>
?標簽的引入資源方式有三種,有一種就是我們常用的直接引入,還有兩種就是使用 async 屬性和 defer 屬性來異步引入,兩者都是去異步加載外部的JS文件,不會阻塞DOM的解析(盡量使用異步加載)。三者的區(qū)別如下:
(2)針對CSS:使用CSS有三種方式:使用link、@import、內聯(lián)樣式,其中l(wèi)ink和@import都是導入外部樣式。它們之間的區(qū)別:
外部樣式如果長時間沒有加載完畢,瀏覽器為了用戶體驗,會使用瀏覽器會默認樣式,確保首次渲染的速度。所以CSS一般寫在headr中,讓瀏覽器盡快發(fā)送請求去獲取css樣式。
所以,在開發(fā)過程中,導入外部樣式使用link,而不用@import。如果css少,盡可能采用內嵌樣式,直接寫在style標簽中。
(3)針對DOM樹、CSSOM樹:
可以通過以下幾種方式來減少渲染的時間:
(4)減少回流與重繪:
table
?布局, 一個小的改動可能會使整個 ?table
?進行重新布局documentFragment
?,在它上面應用所有DOM操作,最后再把它添加到文檔中display: none
?,操作結束后再把它顯示出來。因為在display屬性為none的元素上進行的DOM操作不會引發(fā)回流和重繪。瀏覽器針對頁面的回流與重繪,進行了自身的優(yōu)化——渲染隊列
瀏覽器會將所有的回流、重繪的操作放在一個隊列中,當隊列中的操作到了一定的數(shù)量或者到了一定的時間間隔,瀏覽器就會對隊列進行批處理。這樣就會讓多次的回流、重繪變成一次回流重繪。
將多個讀操作(或者寫操作)放在一起,就會等所有的讀操作進入隊列之后執(zhí)行,這樣,原本應該是觸發(fā)多次回流,變成了只觸發(fā)一次回流。
JavaScript 的加載、解析與執(zhí)行會阻塞文檔的解析,也就是說,在構建 DOM 時,HTML 解析器若遇到了 JavaScript,那么它會暫停文檔的解析,將控制權移交給 JavaScript 引擎,等 JavaScript 引擎運行完畢,瀏覽器再從中斷的地方恢復繼續(xù)解析文檔。也就是說,如果想要首屏渲染的越快,就越不應該在首屏就加載 JS 文件,這也是都建議將 script 標簽放在 body 標簽底部的原因。當然在當下,并不是說 script 標簽必須放在底部,因為你可以給 script
標簽添加 defer 或者 async 屬性。
Webkit 和 Firefox 都做了這個優(yōu)化,當執(zhí)行 JavaScript 腳本時,另一個線程解析剩下的文檔,并加載后面需要通過網(wǎng)絡加載的資源。這種方式可以使資源并行加載從而使整體速度更快。需要注意的是,預解析并不改變 DOM 樹,它將這個工作留給主解析過程,自己只解析外部資源的引用,比如外部腳本、樣式表及圖片。
理論上,既然樣式表不改變 DOM 樹,也就沒有必要停下文檔的解析等待它們。然而,存在一個問題,JavaScript 腳本執(zhí)行時可能在文檔的解析過程中請求樣式信息,如果樣式還沒有加載和解析,腳本將得到錯誤的值,顯然這將會導致很多問題。所以如果瀏覽器尚未完成 CSSOM 的下載和構建,而我們卻想在此時運行腳本,那么瀏覽器將延遲 JavaScript 腳本執(zhí)行和文檔的解析,直至其完成 CSSOM 的下載和構建。也就是說,在這種情況下,瀏覽器會先下載和構建 CSSOM,然后再執(zhí)行 JavaScript,最后再繼續(xù)文檔的解析。
為盡快完成首次渲染,我們需要最大限度減小以下三種可變因素:
(1)關鍵資源的數(shù)量。
(2)關鍵路徑長度。
(3)關鍵字節(jié)的數(shù)量。
關鍵資源是可能阻止網(wǎng)頁首次渲染的資源。這些資源越少,瀏覽器的工作量就越小,對 CPU 以及其他資源的占用也就越少。同樣,關鍵路徑長度受所有關鍵資源與其字節(jié)大小之間依賴關系圖的影響:某些資源只能在上一資源處理完畢之后才能開始下載,并且資源越大,下載所需的往返次數(shù)就越多。最后,瀏覽器需要下載的關鍵字節(jié)越少,處理內容并讓其出現(xiàn)在屏幕上的速度就越快。要減少字節(jié)數(shù),我們可以減少資源數(shù)(將它們刪除或設為非關鍵資源),此外還要壓縮和優(yōu)化各項資源,確保最大限度減小傳送大小。
優(yōu)化關鍵渲染路徑的常規(guī)步驟如下:
(1)對關鍵路徑進行分析和特性描述:資源數(shù)、字節(jié)數(shù)、長度。
(2)最大限度減少關鍵資源的數(shù)量:刪除它們,延遲它們的下載,將它們標記為異步等。
(3)優(yōu)化關鍵字節(jié)數(shù)以縮短下載時間(往返次數(shù))。
(4)優(yōu)化其余關鍵資源的加載順序:您需要盡早下載所有關鍵資產,以縮短關鍵路徑長度
首先渲染的前提是生成渲染樹,所以 HTML 和 CSS 肯定會阻塞渲染。如果你想渲染的越快,你越應該降低一開始需要渲染的文件大小,并且扁平層級,優(yōu)化選擇器。然后當瀏覽器在解析到 script 標簽時,會暫停構建 DOM,完成后才會從暫停的地方重新開始。也就是說,如果你想首屏渲染的越快,就越不應該在首屏就加載 JS 文件,這也是都建議將 script 標簽放在 body 標簽底部的原因。
當然在當下,并不是說 script 標簽必須放在底部,因為你可以給 script 標簽添加 defer 或者 async 屬性。當 script 標簽加上 defer 屬性以后,表示該 JS 文件會并行下載,但是會放到 HTML 解析完成后順序執(zhí)行,所以對于這種情況你可以把 script 標簽放在任意位置。對于沒有任何依賴的 JS 文件可以加上 async 屬性,表示 JS 文件下載和解析不會阻塞渲染。
Cookie是最早被提出來的本地存儲方式,在此之前,服務端是無法判斷網(wǎng)絡中的兩個請求是否是同一用戶發(fā)起的,為解決這個問題,Cookie就出現(xiàn)了。Cookie的大小只有4kb,它是一種純文本文件,每次發(fā)起HTTP請求都會攜帶Cookie。
Cookie的特性:
如果需要域名之間跨域共享Cookie,有兩種方法:
Cookie的使用場景:
LocalStorage是HTML5新引入的特性,由于有的時候我們存儲的信息較大,Cookie就不能滿足我們的需求,這時候LocalStorage就派上用場了。
LocalStorage的優(yōu)點:
LocalStorage的缺點:
LocalStorage的常用API:
// 保存數(shù)據(jù)到 localStorage
localStorage.setItem('key', 'value');
// 從 localStorage 獲取數(shù)據(jù)
let data = localStorage.getItem('key');
// 從 localStorage 刪除保存的數(shù)據(jù)
localStorage.removeItem('key');
// 從 localStorage 刪除所有保存的數(shù)據(jù)
localStorage.clear();
// 獲取某個索引的Key
localStorage.key(index)
LocalStorage的使用場景:
SessionStorage和LocalStorage都是在HTML5才提出來的存儲方案,SessionStorage 主要用于臨時保存同一窗口(或標簽頁)的數(shù)據(jù),刷新頁面時不會刪除,關閉窗口或標簽頁之后將會刪除這些數(shù)據(jù)。
SessionStorage與LocalStorage對比:
SessionStorage的常用API:
// 保存數(shù)據(jù)到 sessionStorage
sessionStorage.setItem('key', 'value');
// 從 sessionStorage 獲取數(shù)據(jù)
let data = sessionStorage.getItem('key');
// 從 sessionStorage 刪除保存的數(shù)據(jù)
sessionStorage.removeItem('key');
// 從 sessionStorage 刪除所有保存的數(shù)據(jù)
sessionStorage.clear();
// 獲取某個索引的Key
sessionStorage.key(index)
SessionStorage的使用場景
Cookie由以下字段組成:
/test
?,那么只有 ?/test
?路徑下的頁面可以讀取此cookie。HTTPOnly
?屬性 ,該屬性用來設置cookie能否通過腳本來訪問,默認為空,即可以通過腳本訪問。在客戶端是不能通過js代碼去設置一個httpOnly類型的cookie的,這種類型的cookie只能通過服務端來設置。該屬性用于防止客戶端腳本通過 ?document.cookie
?屬性訪問Cookie,有助于保護Cookie不被跨站腳本攻擊竊取或篡改。但是,HTTPOnly的應用仍存在局限性,一些瀏覽器可以阻止客戶端腳本對Cookie的讀操作,但允許寫操作;此外大多數(shù)瀏覽器仍允許通過XMLHTTP對象讀取HTTP響應中的Set-Cookie頭。總結:
服務器端可以使用 Set-Cookie 的響應頭部來配置 cookie 信息。一條cookie 包括了5個屬性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的時間,domain 是域名、path是路徑,domain 和 path 一起限制了 cookie 能夠被哪些 url 訪問。secure 規(guī)定了 cookie 只能在確保安全的情況下傳輸,HttpOnly 規(guī)定了這個 cookie 只能被服務器訪問,不能使用 js
腳本訪問。
瀏覽器端常用的存儲技術是 cookie 、localStorage 和 sessionStorage。
上面幾種方式都是存儲少量數(shù)據(jù)的時候的存儲方式,當需要在本地存儲大量數(shù)據(jù)的時候,我們可以使用瀏覽器的 indexDB 這是瀏覽器提供的一種本地的數(shù)據(jù)庫存儲機制。它不是關系型數(shù)據(jù)庫,它內部采用對象倉庫的形式存儲數(shù)據(jù),它更接近 NoSQL 數(shù)據(jù)庫。
IndexedDB 具有以下特點:
跨域問題其實就是瀏覽器的同源策略造成的。
同源策略限制了從同一個源加載的文檔或腳本如何與另一個源的資源進行交互。這是瀏覽器的一個用于隔離潛在惡意文件的重要的安全機制。同源指的是:協(xié)議、端口號、域名必須一致。
下表給出了與 URL http://store.company.com/dir/page.html 的源進行對比的示例:
URL | 是否跨域 | 原因 |
---|---|---|
http://store.company.com/dir/page.html | 同源 | 完全相同 |
http://store.company.com/dir/inner/another.html | 同源 | 只有路徑不同 |
https://store.company.com/secure.html | 跨域 | 協(xié)議不同 |
http://store.company.com:81/dir/etc.html | 跨域 | 端口不同 ( http:// 默認端口是80) |
http://news.company.com/dir/other.html | 跨域 | 主機不同 |
同源策略:protocol(協(xié)議)、domain(域名)、port(端口)三者必須一致。
同源政策主要限制了三個方面:
同源政策的目的主要是為了保證用戶的信息安全,它只是對 js 腳本的一種限制,并不是對瀏覽器的限制,對于一般的 img、或者script 腳本請求都不會有跨域的限制,這是因為這些操作都不會通過響應結果來進行可能出現(xiàn)安全問題的操作。
下面是MDN對于CORS的定義:
跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain)上的Web應用被準許訪問來自不同源服務器上的指定的資源。當一個資源從與該資源本身所在的服務器不同的域、協(xié)議或端口請求一個資源時,資源會發(fā)起一個跨域HTTP 請求。
CORS需要瀏覽器和服務器同時支持,整個CORS過程都是瀏覽器完成的,無需用戶參與。因此實現(xiàn)CORS的關鍵就是服務器,只要服務器實現(xiàn)了CORS請求,就可以跨源通信了。
瀏覽器將CORS分為簡單請求和非簡單請求:
簡單請求不會觸發(fā)CORS預檢請求。若該請求滿足以下兩個條件,就可以看作是簡單請求:
1)請求方法是以下三種方法之一:
2)HTTP的頭信息不超出以下幾種字段:
若不滿足以上條件,就屬于非簡單請求了。
(1)簡單請求過程:
對于簡單請求,瀏覽器會直接發(fā)出CORS請求,它會在請求的頭信息中增加一個Orign字段,該字段用來說明本次請求來自哪個源(協(xié)議+端口+域名),服務器會根據(jù)這個值來決定是否同意這次請求。如果Orign指定的域名在許可范圍之內,服務器返回的響應就會多出以下信息頭:
Access-Control-Allow-Origin: http://api.bob.com // 和Orign一直
Access-Control-Allow-Credentials: true // 表示是否允許發(fā)送Cookie
Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8 // 表示文檔類型
如果Orign指定的域名不在許可范圍之內,服務器會返回一個正常的HTTP回應,瀏覽器發(fā)現(xiàn)沒有上面的Access-Control-Allow-Origin頭部信息,就知道出錯了。這個錯誤無法通過狀態(tài)碼識別,因為返回的狀態(tài)碼可能是200。
在簡單請求中,在服務器內,至少需要設置字段:Access-Control-Allow-Origin
(2)非簡單請求過程
非簡單請求是對服務器有特殊要求的請求,比如請求方法為DELETE或者PUT等。非簡單請求的CORS請求會在正式通信之前進行一次HTTP查詢請求,稱為預檢請求。
瀏覽器會詢問服務器,當前所在的網(wǎng)頁是否在服務器允許訪問的范圍內,以及可以使用哪些HTTP請求方式和頭信息字段,只有得到肯定的回復,才會進行正式的HTTP請求,否則就會報錯。
預檢請求使用的請求方法是OPTIONS,表示這個請求是來詢問的。他的頭信息中的關鍵字段是Orign,表示請求來自哪個源。除此之外,頭信息中還包括兩個字段:
服務器在收到瀏覽器的預檢請求之后,會根據(jù)頭信息的三個字段來進行判斷,如果返回的頭信息在中有Access-Control-Allow-Origin這個字段就是允許跨域請求,如果沒有,就是不同意這個預檢請求,就會報錯。
服務器回應的CORS的字段如下:
Access-Control-Allow-Origin: http://api.bob.com // 允許跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服務器支持的所有跨域請求的方法
Access-Control-Allow-Headers: X-Custom-Header // 服務器支持的所有頭信息字段
Access-Control-Allow-Credentials: true // 表示是否允許發(fā)送Cookie
Access-Control-Max-Age: 1728000 // 用來指定本次預檢請求的有效期,單位為秒
只要服務器通過了預檢請求,在以后每次的CORS請求都會自帶一個Origin頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin頭信息字段。
在非簡單請求中,至少需要設置以下字段:
'Access-Control-Allow-Origin'
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'
OPTIONS請求次數(shù)過多就會損耗頁面加載的性能,降低用戶體驗度。所以盡量要減少OPTIONS請求次數(shù),可以后端在請求的返回頭部添加:Access-Control-Max-Age:number。它表示預檢請求的返回結果可以被緩存多久,單位是秒。該字段只對完全一樣的URL的緩存設置生效,所以設置了緩存時間,在這個時間范圍內,再次發(fā)送請求就不需要進行預檢請求了。
在CORS請求中,如果想要傳遞Cookie,就要滿足以下三個條件:
withCredentials
?默認情況下在跨域請求,瀏覽器是不帶 cookie 的。但是我們可以通過設置 withCredentials 來進行傳遞 cookie.
// 原生 xml 的設置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 設置方式
axios.defaults.withCredentials = true;
true
?false
?jsonp的原理就是利用 <script>
標簽沒有跨域限制,通過 <script>
標簽src屬性,發(fā)送帶有callback參數(shù)的GET請求,服務端將接口返回數(shù)據(jù)拼湊到callback函數(shù)中,返回給瀏覽器,瀏覽器解析執(zhí)行,從而前端拿到callback函數(shù)返回的數(shù)據(jù)。
1)原生JS實現(xiàn):
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 傳參一個回調函數(shù)名給后端,方便后端返回時執(zhí)行這個在前端定義的回調函數(shù)
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回調執(zhí)行函數(shù)
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
服務端返回如下(返回時即執(zhí)行全局函數(shù)):
handleCallback({"success": true, "user": "admin"})
2)Vue axios實現(xiàn):
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
后端node.js代碼:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = querystring.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回設置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
JSONP的缺點:
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是為數(shù)不多可以跨域操作的window屬性之一,它可用于解決以下方面的問題:
用法:postMessage(data,origin)方法接受兩個參數(shù):
1)a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" rel="external nofollow" rel="external nofollow" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2傳送跨域數(shù)據(jù)
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回數(shù)據(jù)
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
2)b.html:(domain2.com/b.html)
<script>
// 接收domain1的數(shù)據(jù)
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 處理后再發(fā)回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
nginx代理跨域,實質和CORS跨域原理一樣,通過配置文件設置請求響應頭Access-Control-Allow-Origin…等字段。
1)nginx配置解決iconfont跨域
瀏覽器跨域訪問js、css、img等常規(guī)靜態(tài)資源被同源策略許可,但iconfont字體文件(eot|otf|ttf|woff|svg)例外,此時可在nginx的靜態(tài)資源服務器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
2)nginx反向代理接口跨域
跨域問題:同源策略僅是針對瀏覽器的安全策略。服務器端調用HTTP接口只是使用HTTP協(xié)議,不需要同源策略,也就不存在跨域問題。
實現(xiàn)思路:通過Nginx配置一個代理服務器域名與domain1相同,端口不同)做跳板機,反向代理訪問domain2接口,并且可以順便修改cookie中domain信息,方便當前域cookie寫入,實現(xiàn)跨域訪問。
nginx具體配置:
#proxy服務器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啟用
add_header Access-Control-Allow-Origin http://www.domain1.com; #當前端只跨域不帶cookie時,可為*
add_header Access-Control-Allow-Credentials true;
}
}
node中間件實現(xiàn)跨域代理,原理大致與nginx相同,都是通過啟一個代理服務器,實現(xiàn)數(shù)據(jù)的轉發(fā),也可以通過設置cookieDomainRewrite參數(shù)修改響應頭中cookie中域名,實現(xiàn)當前域的cookie寫入,方便接口登錄認證。
1)非vue框架的跨域
使用node + express + http-proxy-middleware搭建一個proxy服務器。
var xhr = new XMLHttpRequest();
// 前端開關:瀏覽器是否讀寫cookie
xhr.withCredentials = true;
// 訪問http-proxy-middleware代理服務器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目標接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改響應頭信息,實現(xiàn)跨域并允許帶cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改響應信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以為false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
2)vue框架的跨域
node + vue + webpack + webpack-dev-server搭建的項目,跨域請求接口,直接修改webpack.config.js配置。開發(fā)環(huán)境下,vue渲染服務和接口代理服務都是webpack-dev-server同一個,所以頁面與代理接口之間不再跨域。
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目標接口
changeOrigin: true,
secure: false, // 當代理某些https服務報錯時用
cookieDomainRewrite: 'www.domain1.com' // 可以為false,表示不修改
}],
noInfo: true
}
}
此方案僅限主域相同,子域不同的跨域應用場景。實現(xiàn)原理:兩個頁面都通過js強制設置document.domain為基礎主域,就實現(xiàn)了同域。
1)父窗口:(domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html" rel="external nofollow" ></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
2)子窗口:(child.domain.com/a.html)
<script>
document.domain = 'domain.com';
// 獲取父窗口中變量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
實現(xiàn)原理:a欲與b跨域相互通信,通過中間頁c來實現(xiàn)。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通信。
具體實現(xiàn):A域:a.html -> B域:b.html -> A域:c.html,a與b不同域只能通過hash值單向通信,b與c也不同域也只能單向通信,但c與a同域,所以c可通過parent.parent訪問a頁面所有對象。
1)a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" rel="external nofollow" rel="external nofollow" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html傳hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 開放給同域c.html的回調方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2)b.html:(.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" rel="external nofollow" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 監(jiān)聽a.html傳來的hash值,再傳給c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3)c.html:(http://www.domain1.com/c.html)
<script>
// 監(jiān)聽b.html傳來的hash值
window.onhashchange = function () {
// 再通過操作同域a.html的js回調,將結果傳回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
window.name屬性的獨特之處:name值在不同的頁面(甚至不同域名)加載后依舊存在,并且可以支持非常長的 name 值(2MB)。
1)a.html:(domain1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加載跨域頁面
iframe.src = url;
// onload事件會觸發(fā)2次,第1次加載跨域頁,并留存數(shù)據(jù)于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy頁)成功后,讀取同域window.name中數(shù)據(jù)
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域頁)成功后,切換到同域代理頁面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 獲取數(shù)據(jù)以后銷毀這個iframe,釋放內存;這也保證了安全(不被其他域frame js訪問)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 請求跨域b頁面數(shù)據(jù)
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
2)proxy.html:(domain1.com/proxy.html)
中間代理頁,與a.html同域,內容為空即可。
3)b.html:(domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
通過iframe的src屬性由外域轉向本地域,跨域數(shù)據(jù)即由iframe的window.name從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。
WebSocket protocol是HTML5一種新的協(xié)議。它實現(xiàn)了瀏覽器與服務器全雙工通信,同時允許跨域通訊,是server push技術的一種很好的實現(xiàn)。
原生WebSocket API使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容。
1)前端代碼:
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js" rel="external nofollow" ></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 連接成功處理
socket.on('connect', function() {
// 監(jiān)聽服務端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 監(jiān)聽服務端關閉
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
2)Nodejs socket后臺:
var http = require('http');
var socket = require('socket.io');
// 啟http服務
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 監(jiān)聽socket連接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 斷開處理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
客戶端想獲得一個服務器的數(shù)據(jù),但是因為種種原因無法直接獲取。于是客戶端設置了一個代理服務器,并且指定目標服務器,之后代理服務器向目標服務器轉交請求并將獲得的內容發(fā)送給客戶端。這樣本質上起到了對真實服務器隱藏真實客戶端的目的。實現(xiàn)正向代理需要修改客戶端,比如修改瀏覽器配置。
服務器為了能夠將工作負載分不到多個服務器來提高網(wǎng)站性能 (負載均衡)等目的,當其受到請求后,會首先根據(jù)轉發(fā)規(guī)則來確定請求應該被轉發(fā)到哪個服務器上,然后將請求轉發(fā)到對應的真實服務器上。這樣本質上起到了對客戶端隱藏真實服務器的作用。
一般使用反向代理后,需要通過修改 DNS 讓域名解析到代理服務器 IP,這時瀏覽器無法察覺到真正服務器的存在,當然也就不需要修改配置了。
兩者區(qū)別如圖示:
正向代理和反向代理的結構是一樣的,都是 client-proxy-server 的結構,它們主要的區(qū)別就在于中間這個 proxy 是哪一方設置的。在正向代理中,proxy 是 client 設置的,用來隱藏 client;而在反向代理中,proxy 是 server 設置的,用來隱藏 server。
Nginx 是一款輕量級的 Web 服務器,也可以用于反向代理、負載平衡和 HTTP 緩存等。Nginx 使用異步事件驅動的方法來處理請求,是一款面向性能設計的 HTTP 服務器。
傳統(tǒng)的 Web 服務器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是這個主要的區(qū)別帶給了 Nginx 在性能上的優(yōu)勢。
Nginx 架構的最頂層是一個 master process,這個 master process 用于產生其他的 worker process,這一點和Apache 非常像,但是 Nginx 的 worker process 可以同時處理大量的HTTP請求,而每個 Apache process 只能處理一個。
事件是用戶操作網(wǎng)頁時發(fā)生的交互動作,比如 click/move, 事件除了用戶觸發(fā)的動作外,還可以是文檔加載,窗口滾動和大小調整。事件被封裝成一個 event 對象,包含了該事件發(fā)生時的所有相關信息( event 的屬性)以及可以對事件進行的操作( event 的方法)。
事件是用戶操作網(wǎng)頁時發(fā)生的交互動作或者網(wǎng)頁本身的一些操作,現(xiàn)代瀏覽器一共有三種事件模型:
事件委托本質上是利用了瀏覽器事件冒泡的機制。因為事件在冒泡過程中會上傳到父節(jié)點,父節(jié)點可以通過事件對象獲取到目標節(jié)點,因此可以把子節(jié)點的監(jiān)聽函數(shù)定義在父節(jié)點上,由父節(jié)點的監(jiān)聽函數(shù)統(tǒng)一處理多個子元素的事件,這種方式稱為事件委托(事件代理)。
使用事件委托可以不必要為每一個子元素都綁定一個監(jiān)聽事件,這樣減少了內存上的消耗。并且使用事件代理還可以實現(xiàn)事件的動態(tài)綁定,比如說新增了一個子節(jié)點,并不需要單獨地為它添加一個監(jiān)聽事件,它綁定的事件會交給父元素中的監(jiān)聽函數(shù)來處理。
如果有一個列表,列表之中有大量的列表項,需要在點擊列表項的時候響應一個事件:
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果給每個列表項一一都綁定一個函數(shù),那對于內存消耗是非常大的,效率上需要消耗很多性能。因此,比較好的方法就是把這個點擊事件綁定到他的父層,也就是 ul 上,然后在執(zhí)行事件時再去匹配判斷目標元素,所以事件委托可以減少大量的內存消耗,節(jié)約效率。
給上述的例子中每個列表項都綁定事件,在很多時候,需要通過 AJAX 或者用戶操作動態(tài)的增加或者去除列表項元素,那么在每一次改變的時候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;如果用了事件委托就沒有這種麻煩了,因為事件是綁定在父層的,和目標元素的增減是沒有關系的,執(zhí)行到目標元素是在真正響應執(zhí)行事件函數(shù)的過程中去匹配的,所以使用事件在動態(tài)綁定事件的情況下是可以減少很多重復工作的。
// 來實現(xiàn)把 #list 下的 li 元素的事件代理委托到它的父層元素也就是 #list 上:
// 給父層元素綁定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性處理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判斷是否匹配目標元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
在上述代碼中, target 元素則是在 #list 元素之下具體被點擊的元素,然后通過判斷 target 的一些屬性(比如:nodeName,id 等等)可以更精確地匹配到某一類 #list li 元素之上;
當然,事件委托也是有局限的。比如 focus、blur 之類的事件沒有事件冒泡機制,所以無法實現(xiàn)事件委托;mousemove、mouseout 這樣的事件,雖然有事件冒泡,但是只能不斷通過位置去計算定位,對性能消耗高,因此也是不適合于事件委托的。
當然事件委托不是只有優(yōu)點,它也是有缺點的,事件委托會影響頁面性能,主要影響因素有:
DOM
?層數(shù);在必須使用事件委托的地方,可以進行如下的處理:
ajax
?的局部刷新區(qū)域body
?元素上,進行綁定場景:給頁面的所有的a標簽添加click事件,代碼如下:
document.addEventListener("click", function(e) {
if (e.target.nodeName == "A")
console.log("a");
}, false);
但是這些a標簽可能包含一些像span、img等元素,如果點擊到了這些a標簽中的元素,就不會觸發(fā)click事件,因為事件綁定上在a標簽元素上,而觸發(fā)這些內部的元素時,e.target指向的是觸發(fā)click事件的元素(span、img等其他元素)。
這種情況下就可以使用事件委托來處理,將事件綁定在a標簽的內部元素上,當點擊它的時候,就會逐級向上查找,知道找到a標簽為止,代碼如下:
document.addEventListener("click", function(e) {
var node = e.target;
while (node.parentNode.nodeName != "BODY") {
if (node.nodeName == "A") {
console.log("a");
break;
}
node = node.parentNode;
}
}, false);
因為 js 是單線程運行的,在代碼執(zhí)行時,通過將不同函數(shù)的執(zhí)行上下文壓入執(zhí)行棧中來保證代碼的有序執(zhí)行。在執(zhí)行同步代碼時,如果遇到異步事件,js 引擎并不會一直等待其返回結果,而是會將這個事件掛起,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務。當異步事件執(zhí)行完畢后,再將異步事件對應的回調加入到一個任務隊列中等待執(zhí)行。任務隊列可以分為宏任務隊列和微任務隊列,當當前執(zhí)行棧中的事件執(zhí)行完畢后,js 引擎首先會判斷微任務隊列中是否有任務可以執(zhí)行,如果有就將微任務隊首的事件壓入棧中執(zhí)行。當微任務隊列中的任務都執(zhí)行完成后再去執(zhí)行宏任務隊列中的任務。
Event Loop 執(zhí)行順序如下所示:
可以把執(zhí)行棧認為是一個存儲函數(shù)調用的棧結構,遵循先進后出的原則。
當開始執(zhí)行 JS 代碼時,根據(jù)先進后出的原則,后執(zhí)行的函數(shù)會先彈出棧,可以看到,foo
函數(shù)后執(zhí)行,當執(zhí)行完畢后就從棧中彈出了。
平時在開發(fā)中,可以在報錯中找到執(zhí)行棧的痕跡:
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
可以看到報錯在 foo
函數(shù),foo
函數(shù)又是在 bar
函數(shù)中調用的。當使用遞歸時,因為??纱娣诺暮瘮?shù)是有限制的,一旦存放了過多的函數(shù)且沒有得到釋放的話,就會出現(xiàn)爆棧的問題
function bar() {
bar()
}
bar()
Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西。
Node 的 Event Loop 分為 6 個階段,它們會按照順序反復運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數(shù)去執(zhí)行。當隊列為空或者執(zhí)行的回調函數(shù)數(shù)量到達系統(tǒng)設定的閾值,就會進入下一階段。
(1)Timers(計時器階段):初次進入事件循環(huán),會從計時器階段開始。此階段會判斷是否存在過期的計時器回調(包含 setTimeout 和 setInterval),如果存在則會執(zhí)行所有過期的計時器回調,執(zhí)行完畢后,如果回調中觸發(fā)了相應的微任務,會接著執(zhí)行所有微任務,執(zhí)行完微任務后再進入 Pending callbacks 階段。
(2)Pending callbacks:執(zhí)行推遲到下一個循環(huán)迭代的I / O回調(系統(tǒng)調用相關的回調)。
(3)Idle/Prepare:僅供內部使用。
(4)Poll(輪詢階段):
(5)Check(查詢階段):會檢查是否存在 setImmediate 相關的回調,如果存在則執(zhí)行所有回調,執(zhí)行完畢后,如果回調中觸發(fā)了相應的微任務,會接著執(zhí)行所有微任務,執(zhí)行完微任務后再進入 Close callbacks 階段。
(6)Close callbacks:執(zhí)行一些關閉回調,比如socket.on('close', ...)等。
下面來看一個例子,首先在有些情況下,定時器的執(zhí)行順序其實是隨機的
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
對于以上代碼來說,setTimeout
可能執(zhí)行在前,也可能執(zhí)行在后
setTimeout(fn, 0) === setTimeout(fn, 1)
?,這是由源碼決定的setTimeout
?回調setImmediate
?回調先執(zhí)行了當然在某些情況下,他們的執(zhí)行順序一定是固定的,比如以下代碼:
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
在上述代碼中,setImmediate
永遠先執(zhí)行。因為兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執(zhí)行,當回調執(zhí)行完畢后隊列為空,發(fā)現(xiàn)存在 setImmediate
回調,所以就直接跳轉到 check 階段去執(zhí)行回調了。
上面都是 macrotask 的執(zhí)行情況,對于 microtask 來說,它會在以上每個階段完成前清空 microtask 隊列,下圖中的 Tick 就代表了 microtask
setTimeout(() => {
console.log('timer21')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
})
對于以上代碼來說,其實和瀏覽器中的輸出是一樣的,microtask 永遠執(zhí)行在 macrotask 前面。
最后來看 Node 中的 process.nextTick
,這個函數(shù)其實是獨立于 Event Loop 之外的,它有一個自己的隊列,當每個階段完成后,如果存在 nextTick 隊列,就會清空隊列中的所有回調函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
對于以上代碼,永遠都是先把 nextTick 全部打印出來。
順序
//macro-task:script(全部的代碼) setInterval setTimeout setImmediate I/O
//micro-task:process.nextTick Promise
事件觸發(fā)有三個階段:
window
?往事件觸發(fā)處傳播,遇到注冊的捕獲事件會觸發(fā)window
?傳播,遇到注冊的冒泡事件會觸發(fā)事件觸發(fā)一般來說會按照上面的順序進行,但是也有特例,如果給一個 body
中的子節(jié)點同時注冊冒泡和捕獲事件,事件觸發(fā)會按照注冊的順序執(zhí)行。
// 以下會先打印冒泡然后是捕獲
node.addEventListener(
'click',
event => {
console.log('冒泡')
},
false
)
node.addEventListener(
'click',
event => {
console.log('捕獲 ')
},
true
)
通常使用 addEventListener
注冊事件,該函數(shù)的第三個參數(shù)可以是布爾值,也可以是對象。對于布爾值 useCapture
參數(shù)來說,該參數(shù)默認值為 false
,useCapture
決定了注冊的事件是捕獲事件還是冒泡事件。對于對象參數(shù)來說,可以使用以下幾個屬性:
capture
?:布爾值,和 ?useCapture
?作用一樣once
?:布爾值,值為 ?true
?表示該回調只會調用一次,調用后會移除監(jiān)聽passive
?:布爾值,表示永遠不會調用 ?preventDefault
?一般來說,如果只希望事件只觸發(fā)在目標上,這時候可以使用 stopPropagation
來阻止事件的進一步傳播。通常認為 stopPropagation
是用來阻止事件冒泡的,其實該函數(shù)也可以阻止捕獲事件。
stopImmediatePropagation
同樣也能實現(xiàn)阻止事件,但是還能阻止該事件目標執(zhí)行別的注冊事件。
node.addEventListener(
'click',
event => {
event.stopImmediatePropagation()
console.log('冒泡')
},
false
)
// 點擊 node 只會執(zhí)行上面的函數(shù),該函數(shù)不會執(zhí)行
node.addEventListener(
'click',
event => {
console.log('捕獲 ')
},
true
)
V8 實現(xiàn)了準確式 GC,GC 算法采用了分代式垃圾回收機制。因此,V8 將內存(堆)分為新生代和老生代兩部分。
(1)新生代算法
新生代中的對象一般存活時間較短,使用 Scavenge GC 算法。
在新生代空間中,內存空間分為兩部分,分別為 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閑的。新分配的對象會被放入 From 空間中,當 From 空間被占滿時,新生代 GC 就會啟動了。算法會檢查 From 空間中存活的對象并復制到 To 空間中,如果有失活的對象就會銷毀。當復制完成后將 From 空間和 To 空間互換,這樣 GC 就結束了。
(2)老生代算法
老生代中的對象一般存活時間較長且數(shù)量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。
先來說下什么情況下對象會出現(xiàn)在老生代空間中:
老生代中的空間很復雜,有如下幾個空間
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不變的對象空間
NEW_SPACE, // 新生代用于 GC 復制算法的空間
OLD_SPACE, // 老生代常駐對象空間
CODE_SPACE, // 老生代代碼對象空間
MAP_SPACE, // 老生代 map 對象
LO_SPACE, // 老生代大空間對象
NEW_LO_SPACE, // 新生代大空間對象
FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
在老生代中,以下情況會先啟動標記清除算法:
在這個階段中,會遍歷堆中所有的對象,然后標記活的對象,在標記完成后,銷毀所有沒有被標記的對象。在標記大型對內存時,可能需要幾百毫秒才能完成一次標記。這就會導致一些性能上的問題。為了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標志。在增量標記期間,GC 將標記工作分解為更小的模塊,可以讓 JS 應用邏輯在模塊間隙執(zhí)行一會,從而不至于讓應用出現(xiàn)停頓情況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名為并發(fā)標記。該技術可以讓 GC 掃描和標記對象時,同時允許 JS 運行。
清除對象后會造成堆內存出現(xiàn)碎片的情況,當碎片超過一定限制后會啟動壓縮算法。在壓縮過程中,將活的對象向一端移動,直到所有對象都移動完成然后清理掉不需要的內存。
更多建議: