1. onload 事件
圖檔可以綁定一個
onload
事件,表示當圖檔加載完成後才觸發執行腳本。
<body>
<img src="./img/01.png" />
<script>
let img = document.querySelector('img');
img.onload = function(){
// 擷取到圖檔的寬度
console.log(img.offsetWidth);
}
</script>
</body>
複制
window.onload 與 img.onload 的不同
window.onload
事件表示頁面加載完成後才加載 JavaScript 代碼。這裡的 “頁面加載完成” 指的是在文檔裝載完成後會觸發 load 事件,此時,在文檔中的所有對象都在
DOM
中,所有圖檔,腳本,連結以及子框都完成了裝載。而
img.onload
僅僅指的是圖檔裝載完成。
有了
onload
事件我們可以把腳本寫在
<head>
标簽中。
<head>
<script>
window.onload = function(){
let img = document.querySelector('img');
img.style.height = "300px";
console.log(img.offsetWidth);
}
</script>
</head>
複制
DOMContentLoaded 事件
當初始的 HTML 文檔被完全加載和解析完成之後,
DOMContentLoaded
事件被觸發,而無需等待樣式表、圖像和子架構的完成加載。而
load
事件用于檢測一個完全加載的頁面。
比如下面的例子:
<body>
<img src="../canvas/img/05.jpg" class="img" />
<script>
var img = document.querySelector(".img");
console.log(img.offsetWidth);
document.addEventListener("DOMContentLoaded", function(){
console.log("DOMContentLoaded", img.offsetWidth);
}, false);
window.onload = function(){
console.log("load", img.offsetWidth);
}
img.onload = function(){
console.log("img", img.offsetWidth);
}
</script>
</body>
複制
列印順序是:
0 // 圖檔還沒有加載過來
DOMContentLoaded 0 // HTML 文檔解析完成,但圖檔還沒有加載過來
img 423 // 圖檔加載好
load 423 // 頁面已經完全加載好
複制
需要注意的是,如果在事件所屬的
DOMContentLoaded
标簽上有外聯的樣式表(
script
标簽),
link
事件必須等待它之前的樣式表加載解析完成才會觸發。
DOMContentLoaded
readystatechange 事件
document 上有一個
readystatechange
事件,它表示當文檔的
readyState
屬性發生改變時會觸發。
document.readyState
會傳回一個字元串,它有以下幾種可能:
-
表示正在加載;loading
-
表示文檔已被解析,"正在加載" 狀态結束,但是諸如圖像,樣式表和架構之類的子資源仍在加載(interactive
事件就是在此階段觸發的)。DOMContentLoaded
-
文檔和所有子資源已完成加載。complete
狀态的事件即将被觸發。load
readystatechange
事件一般會觸發兩次,一次是從
loading
變成
interactive
狀态(會在
DOMContentLoaded
事件之前觸發),另一次是從
interactive
變成
complete
狀态(會在
load
事件之前觸發)。
2. <script> 标簽
HTML中的
<script>
标簽用來加載外部腳本或者編寫内聯腳本。
頁面在執行時,遇到
<script>
标簽都會讓頁面等待腳本的解析和執行。無論 JavaScript 代碼是内嵌的還是外鍊的,頁面的下載下傳和渲染都必須停下來等待腳本執行完成。
無阻塞腳本
除了上面
onload
事件和
DOMContentLoaded
可以延時腳本執行外,
<script>
标簽還可以添加
defer
或者
async
屬性延遲腳本的執行。這兩個屬性的屬性值都是布爾類型。
async
和
defer
屬性的相同點是采用并行下載下傳(頁面執行到帶
async
或
defer
屬性的标簽時不會阻塞頁面渲染,而是邊下載下傳腳本邊渲染頁面)。需要注意的是:這兩個屬性不能用在内嵌腳本中,隻能用在外聯腳本标簽上。
- 帶有
屬性的腳本将在文檔完成解析後,觸發defer
事件之前執行。DOMContentLoaded
- 帶有
屬性的腳本會在腳本加載完成後異步執行該腳本(無論此刻是 HTML 解析階段還是async
觸發之後,亦或是DOMContentLoaded
事件之後)。load
動态生成的 <script>
标簽
<script>
比如下面的代碼:
<script>
var script = document.createElement('script');
script.src = "./async.js";
document.body.appendChild(script);
</script>
複制
動态生成的
<script>
标簽相當于帶有
async
屬性的
<script>
。當被插入到文檔中後腳本就會自動執行。
<script>
元素的 load
事件
<script>
load
動态生成的
<script>
标簽可以接受一個
onload
事件,表示腳本加載完成時會被觸發。
<script>
var script = document.createElement("script");
script.src = './index.js';
script.onload = function(){
alert("腳本加載完成!");
}
document.body.appendChild(script);
</script>
複制
通過 XMLHttpRequest 實作腳本注入
通過 Ajax 請求也可以動态加載 js 檔案。這種方式相當于建立一個新的
<script>
标簽。當新建立的标簽被添加到頁面時,代碼就會立刻執行。
var xhr = new XMLHttpRequest();
xhr.open('GET','/index.js',true);
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(cxhr.status === 200){
var script = document.createElement('script');
script.type = "text/jsvascript";
// script 的内容是 相應的 JavaScript 文本
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
}
複制
使用這種方式加載代碼時,JavaScript檔案必須與所請求的頁面處于相同的域。
組織腳本
由于每個
<script>
腳本都會阻塞頁面渲染(當然除了有
defer
和
async
屬性的标簽)。那麼在開發中應怎樣改善這一情況呢?為了提高頁面性能或者說體驗,可以通過以下方式進行優化:
- 把多個腳本檔案進行合并,這樣可以減少網絡請求數量。但并不是合并越多越好,檔案太大還會導緻阻塞事件變長。
- 盡量
标簽添加在<script>
标簽的最下方,這樣可以避免阻塞渲染。<body>
- 使用
事件避免阻塞渲染。onload
- 把一段内嵌腳本放在
标簽之後會導緻頁面阻塞去等待樣式表的下載下傳,這樣做是可以確定内嵌腳本在執行時能獲得最精确的樣式資訊。<link>
3. 浏覽器渲染機制
當打開一個網頁時,浏覽器都做了些什麼?
- 浏覽器根據 DNS 伺服器得到域名的 IP 位址;
- 浏覽器從 URL 中解析出端口号,拿到 ip 和端口号浏覽器會建立與目标 web 伺服器的 TCP 連接配接(進行 TCP 三次握手);
- 浏覽器向伺服器發送一條 HTTP 請求;
- 伺服器給浏覽器傳回一條 HTTP 響應封包;
- 關閉連接配接,浏覽器解析文檔;
這裡主要說一下第四步,浏覽器是如何解析和渲染頁面的。
- 浏覽器拿到 HTML 文檔,浏覽器會把 HTML 頁面結構解析轉換
(document object model) 樹形結構(DOM
)。DOM tree
- 當遇到 css 代碼時,開始解析 CSS,然後生成一個與 DOM 結構相似的樹形結構,被稱為
(CSS 對象模型);CSSOM
- 如果遇到 JavaScript 腳本,頁面會等待腳本加載,然後執行(會阻塞 DOM 樹和 CSSOM 樹的建構,而如果使用
或者defer
的标簽則不會),通過async
和DOM API
來操作CSSOM API
和DOM Tree
。CSS Rule Tree
- 頁面解析完成後,浏覽器會通過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree(渲染樹)。
- 根據渲染樹計算每個節點的幾何資訊(重排,layout);
- 渲染繪制(重繪,painting),根據計算好的資訊繪制整個頁面;
CSSOM 和 DOM 都是一組 API,這些 API 可以通過 JavaScript 操縱。DOM 允許使用者動态讀取或修改 HTML 文檔結構,而 CSSOM 允許使用者動态讀取和修改 CSS 樣式。
4. 重繪與重排
重排(reflow):重排也被稱為 “回流”,根據字面意思就是重新布局頁面。例如當我們改變了視窗尺寸或者元素尺寸發生變化時就有可能引發回流。以下是會引起重排的操作:
- 頁面首次渲染;
- 浏覽器視窗大小發生變化(如:
事件觸發時);resize
- 元素尺寸或者位置發生改變(
、width
、height
、margin
、display
、border
等);position
- 元素的内容發生變化(如:字的數量、圖檔尺寸);
- 元素字型大小發生變化;
- 設定 style 屬性;
- 計算
和offsetWidth
屬性;offsetHeight
- 激活 css 僞類(例如
);:hover
以下是常見的引起重排的屬性和方法:
-
、clientWidth
(它是元素内部的高度或寬度(機關像素),包含内邊距,但不包括水準滾動條、邊框和外邊距。隻讀屬性)clientHeight
-
、scrollWidth
(該元素在不使用滾動條的情況下為了适應視口中所用内容所需的最小高度或寬度,隻讀屬性);scrollHeight
-
讓目前的元素滾動到浏覽器視窗的可視區域内。scrollIntoView()
-
使界面滾動到給定元素的指定坐标位置。scrollTo()
-
擷取到計算後的 css 樣式值;getComputedStyle()
-
傳回元素的大小及其相對于視口的位置。getBoundingClientRect()
-
、scrollTop
擷取或設定一個元素的内容垂直滾動的像素數。scrollLeft
-
、clientTop
一個元素頂部或左側邊框的寬度(以像素表示)。不包括頂部外邊距或内邊距。隻讀屬性。clientLeft
-
、offsetTop
它傳回目前元素相對于其offsetLeft
元素的頂部或左上角内邊距的距離。隻讀屬性。offsetParent
重繪(repaint):字面意思就是“重新繪制”,相較于重排,重繪對于頁面的影響就小得多了,重繪并不會影響元素在文檔中的位置,例如改變字型顔色。下面是常見的引起浏覽器重繪的屬性:
color | border-style | visibility |
text-decoration | background-image | background-position |
outline-color | outline | outline-style |
outline-width | box-shadow | background-size |
性能優化
盡量減少浏覽器重排,會很耗費 CPU 資源。下面幾個方面可以優化渲染性能:
- 避免動态設定大量的 style 屬性;
- 盡量使用修改 class 名的方式操作樣式或動畫;
- 用事件委托來減少事件處理器的數量;
- 減少 DOM 通路次數,例如可以對DOM樣式資訊進行緩存;
- 少使用 HTML 集合來周遊;
- 不使用 table 布局因為 table 中某個元素旦觸發了 reflow,那麼整個 table 的元素都會觸發 reflow。
- 動畫盡量在有絕對定位(
)或固定定位(absolute
)的元素上使用(這樣不影響其他元素布局);fixed
5. 同源政策
同源政策是一個重要的安全政策,它用于限制一個
origin
(源) 的文檔或者它加載的腳本如何能與另一個源的資源進行互動。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介。
如果兩個 URL 的 protocol(協定)、port (端口,如果有指定的話)和 host(主機) 都相同的話,則這兩個 URL 是同源。
與 URL http://store.company.com/dir/page.html 的源進行對比的示例:
- http://store.company.com/dir2/other.html 同源,隻有路徑不同。
- https://store.company.com/secure.html 不同源,因為協定不同(一個 http,一個 https)。
- http://store.company.com:81/dir/etc.html 不同源,端口不同 ( http:// 預設端口是80);
- http://news.company.com/dir/other.html 不同源,主機不同。
需要注意的是,同源政策認為域和子域屬于不同的域,比如 a.com 和 script.a.com 是不同的域。
同源政策隻是一個規範,并不強制要求,但現在所有支援 javaScript 的浏覽器都會使用這個政策. 以至于該政策成為浏覽器最核心最基本的安全功能。同源政策下的 web 世界,域的壁壘高築,進而保證各個網頁互相獨立,互相之間不能直接通路。例如 A 網址的 js 代碼想要向 B 網站發起網絡請求,A、B兩個站點是不同的源,一般情況下 A 網站是不能通路到 B 網站的資料的。因為這種通路操作是“跨域”的(向别的域名發起網絡請求),如果能通路到,那豈不是在竊取别人網站的資料。
script 标簽中的
src
和 img 标簽的
src
屬性并沒有跨域的限制,是以完全可以引入其他域下的圖檔和腳本。但要慎重,如果第三方腳本是惡意的,那麼很可能會帶來安全隐患。因為 src 屬性不受跨域限制,也帶來了許多 web 安全問題。
要想實作跨域操作,也有許多種辦法,例如:後端設定
CORS
權限,允許部分域可以通路;基于script 标簽做 jsonp 形式的通路;
6. 内容安全政策(CSP)
内容安全政策 (CSP, Content Security Policy) 是一個附加的安全層,用于幫助檢測和緩解某些類型的攻擊,包括跨站腳本 (
XSS
) 和資料注入等攻擊。這些攻擊可用于實作從資料竊取到網站破壞或作為惡意軟體分發版本等用途。
不支援
CSP
的浏覽器會忽略它,像平常一樣運作,預設對網頁内容使用标準的同源政策。如果網站不提供
CSP
頭部,浏覽器同樣會使用标準的同源政策。
開啟
CSP
就像配置 HTTP 頭部一樣簡單(在 HTTP 響應頭中設定)。
Content-Security-Policy
是用于設定
CSP
的頭部字段(有時你會看到一些關于
X-Content-Security-Policy
頭部的提法, 那是舊版本)。除此之外,
<meta>
元素也可以被用來配置該政策, 例如:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
複制
CSP
的主要目标是減少和報告
XSS
攻擊。CSP 通過指定有效域——即浏覽器認可的可執行腳本的有效來源——使伺服器管理者有能力減少或消除 XSS 攻擊所依賴的載體。一個 CSP 相容的浏覽器将會僅執行從白名單域擷取到的腳本檔案,忽略所有的其他腳本(畢竟 script 标簽不受同源政策限制,而 CSP 可以禁止某些域的腳本執行)。
制定政策
一個政策由一系列政策指令所組成,每個政策指令都描述了一個針對某個特定類型資源以及生效範圍的政策。你的政策應當包含一個
default-src
政策指令,表示在其他資源類型沒有符合自己的政策時應用該政策。也可以指定别的政策,如
script-src
指令來防止内聯腳本運作, 并杜絕
eval()
的使用。
style-src
指令去限制來自一個
<style>
元素或者
style
屬性的內聯樣式。
其他的政策:
-
定義了child-src
的有效源,将不符合要求的請求視為網絡錯誤;Web Worker
-
用于控制允許通過腳本接口加載的連結位址。其中受到影響的 API 有:connect-src
、<a> ping
、Fetch
、XMLHttpRequest
、WebSocket
;EventSource
-
定義了font-src
加載字型的有效源規則。@font-face
-
指定JavaScript的有效來源。這不僅包括直接加載到script-src
元素中的 URL ,還包括可以觸發腳本執行的内聯腳本事件處理程式(<script>
);onclick
-
指定有效來源的frame-src
;<frame>
-
指定圖像和圖示的有效來源;img-src
-
将哪些清單應用于資源;manifest-src
-
使用media-src
和<audio>
元素指定用于加載媒體的有效源;<video>
-
指定object-src
,<object>
和<embed>
元素的有效來源;<applet>
-
指定樣式表的有效來源;style-src
-
指定的有效來源worker-src
,Worker
或SharedWorker
腳本。ServiceWorker
每個政策允許指定一個或多個源。
Content-Security-Policy: default-src <source> <source>;
複制
假如設定了其他指令,那麼
default-src
不會對它們起作用(設定别的政策用
;
分割開),例如:
Content-Security-Policy: default-src 'self'; script-src https://example.com
複制
與下面的代碼等價:
Content-Security-Policy:connect-src 'self';
font-src 'self';
frame-src 'self';
img-src 'self';
manifest-src 'self';
media-src 'self';
object-src 'self';
script-src https://example.com;
style-src 'self';
worker-src 'self'
複制
<source>
可以是以下之一:
-
以域名或者 IP 位址表示的主機名外加可選的 URL 協定名以及端口号。站點位址中可能會包含一個可選的前置通配符(星号<host-source>
),同時也可以将通配符(也是*
)應用于端口号,表示在這個源中可以使用任意合法的端口号。如 http://*.example.com: 比對從使用 http: 的 example.com 的任意子域的資源加載。*
-
協定名如'http:' 或者 'https:'。必須帶有冒号,不要有單引号。<scheme-source>
-
指向與要保護的檔案所在的源,包括相同的 URL scheme 與端口号。必須有單引号。'self'
-
允許使用内聯資源,例如内聯'unsafe-inline'
元素;内聯事件處理器以及内聯<script>
元素。必須有單引号。<style>
-
允許使用'unsafe-eval'
以及相似的函數來從字元串建立代碼。必須有單引号。eval()
-
不允許任何内容。必須有單引号。'none'
-
特定使用一次性加密内聯腳本的白名單。伺服器必須在每一次傳輸政策時生成唯一的一次性值。否則将存在繞過資源政策的可能。'nonce-<base64值>'
-
使用<hash-source>
、sha256
或sha384
編碼過的内聯腳本或樣式。當生成哈希值的時候,不要包含sha512
或<script>
标簽,同時注意字母大小寫與空格——包括首尾空格——都是會影響生成的結果的。<style>
-
指定對于含有标記腳本(通過附加一個随機數或散列)的信任,應該傳播到由該腳本加載的所有腳本。與此同時,任何白名單以及源表達式例如 'self' 或者 'unsafe-inline' 都會被忽略。'strict-dynamic'
-
在違規報告中包含違規代碼示例。'report-sample'
示例
- 一個網站管理者想要所有内容均來自站點的同一個源 (不包括其子域名)
Content-Security-Policy: default-src 'self'
複制
- 一個網站管理者允許内容來自信任的域名及其子域名 (域名不必須與CSP設定所在的域名相同)
Content-Security-Policy: default-src 'self' *.trusted.com
複制
- 一個線上銀行網站的管理者想要確定網站的所有内容都要通過SSL方式擷取,以避免攻擊者竊聽使用者發出的請求。
Content-Security-Policy: default-src https://onlinebanking.jumbobank.com
複制
- 一個網站管理者允許網頁應用的使用者在他們自己的内容中包含來自任何源的圖檔, 但是限制音頻或視訊需從信任的資源提供者(獲得),所有腳本必須從特定主機伺服器擷取可信的代碼。
Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com
複制
- 一個線上郵箱的管理者想要允許在郵件裡包含HTML,同樣圖檔允許從任何地方加載,但不允許JavaScript或者其他潛在的危險内容(從任意位置加載)。
Content-Security-Policy: default-src 'self' *.mailsite.com; img-src *
複制
Content-Security-Policy-Report-Only
這個響應頭允許 web 開發人員監測違反 CSP 政策的源。這些違反報告由 JSON 文檔組成通過一個HTTP POST請求發送到指定的 URI。這個頭也可以使用
Content-Security-Policy
頭的政策,如果這兩個頭同時出現在一個響應中,兩個政策均有效。在
Content-Security-Policy
頭部中指定的政策有強制性 ,而
Content-Security-Policy-Report-Only
中的政策僅産生報告而不具有強制性。
關于
Content-Security-Policy-Report-Only
的用法可以參考 MDN:Content-Security-Policy-Report-Only
參考
Content-Security-Policy-Report-Only :https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only