在2017年底,主流的常青瀏覽器都支援了ES6模組,表面上看來,只要將script標籤的type屬性設為module,就可以開心使用模組了;然而,專案可能依賴第三方程式庫,而程式庫不見得對ES6模組友善,也可能是從非同源伺服端取得模組,此時,該怎麼整合這些程式庫呢?

從script標籤談起

預設情況下,瀏覽器在遇到傳統script標籤時,會停止文件剖析,執行script標籤間內嵌的程式庫,待執行完畢再繼續文件的剖析;若是透過src外聯.js檔案,瀏覽器會「同步地」下載.js檔案,在下載完成並執行完程式碼之前,後續的其他資源下載、頁面剖析等,都會被阻斷;因此,若想在文件資源完整載入後,再執行指定的程式碼,經常會借助window.onload事件。

不過,文件資源的完整載入,是指HTML、CSS、圖片等都載入完成,而不單指DOM樹建立完成;若想在文件剖析完成、DOM樹生成時就執行程式碼,現代的建議是將script放在文件尾端,通常是body標籤之前,因為此時DOM樹已經建立,操作DOM節點就不是問題了。

為了能進一步控制.js的下載方式與執行順序,HTML5為script標籤增加了async與defer屬性,但這兩個屬性只有在透過src外聯.js檔案,才有作用,因為,傳統內嵌程式碼的script標籤,會忽略async、defer屬性,依舊依頁面的出現順序執行。

若script標籤加上async屬性,瀏覽器會「非同步地」下載.js檔案,在下載完成前,不會阻斷後續資源的下載與頁面剖析,然而一旦async的.js下載完成,瀏覽器就會暫停頁面剖析,先執行.js的程式碼後,再繼續處理頁面剖析。如果有多個async屬性的.js,先下載完的就會先執行,因為下載完成的順序是無法預測的,因此「執行順序也就無法預測」。

若要指定執行順序,script標籤可加上defer屬性,瀏覽器會「非同步地」下載.js檔案,不會阻斷後續資源的下載與頁面剖析,完成後也不會馬上執行程式碼,而是在DOM樹生成、其他非defer的.js執行完後,才執行被加上defer屬性的.js,如果有多個defer屬性的.js,會按照「頁面上出現的順序」執行。

瀏覽器與ES6模組

如果瀏覽器支援ES6模組,我們可以將script的type屬性設定為"module",若是內嵌程式碼,script標籤的範圍就是模組範圍,若是以src外聯.js檔案,表示該檔案是個模組檔案,預設會是「defer」的行為,外聯.js時,會非同步地下載檔案,等到模組都下載完後、DOM樹生成之後,才會執行模組內容,而執行順序是按照頁面上出現的順序,無論內嵌或外聯.js檔案,執行對象是模組的頂層程式碼。

script標籤的type設為"module"時,可以附加async屬性,這時必須配合src外聯檔案(經測試,內嵌程式碼時加async無效),除了認定指定的檔案是個模組檔案外,其餘與方才談到的async行為相同,也就是先下載完先執行。

由於script的type屬性設定為"module"時,就視為一個模組,因此多個模組的script間基本上無法溝通,除非模組程式碼在window全域物件上設定了特性,例如,以模組方式載入jQuery程式庫的話,window就會有個$特性,另一個模組才可以使用$,這是以script載入模組,又要應用jQuery這類程式庫的一個方式;除此之外,通常會有個程式進入點的模組,在其中使用import來靜態地載入其他相依的模組。

import也是非同步地載入模組,若有多個import,會全部下載完成後再依import的順序執行;無論import幾次,或者使用script type="module"多次,同一來源的模組都只會被載入與執行一次。

由於import是靜態地匯入模組,若想動態地匯入模組,可以透過import()函式,在過去瀏覽器多半都實作這個函式了,而撰寫本文時,ECMAScript中〈import()〉(https://bit.ly/2Yqiksv)提案已達階段四,預計於2020年(也就是ES11)正式發布;import()會傳回Promise,任務達成後可以取得模組物件,從中可以取得模組匯出的公用API。

在瀏覽器相容性上,可以處理模組的瀏覽器,會忽略被設定了nomodule屬性的script標籤,因此,在不支援模組的瀏覽器,可以如下撰寫(假設m_fallback.js不是使用ES6模組):

<script type="module" src="m.js"></script>

 

<script nomodule src="m_fallback.js"></script>

模組與CORS

傳統script標籤本身就可以跨域下載.js檔案並執行,必要時,開發者也可撰寫程式碼,動態地建立script元素並指定src,在附加至DOM樹後,就會動態地下載.js並執行,過去被用來實現JSONP,以繞過瀏覽器同源策略實現跨域請求。

在先前專欄〈深入認識跨域請求〉中就談過,JSONP並不是正式定義的跨域請求機制,只是用來繞過瀏覽器同源策略的技法,後來,W3C規範了CORS(https://bit.ly/2UZWB5g),現在有些伺服端在提供資源時,也會要求遵守CORS協定,而HTML5也為CORS提供了支援,對於script標籤(以及img、video等)可以使用crossorigin屬性,令對.js的請求回應遵循CORS協定。

然而,傳統script標籤預設不遵循CORS,若非同源的.js伺服端沒有實現CORS,傳統.js就不用在script加上crossorigin,加了的話反而行不通,因為,此時.js的請求除了附加Origin標頭之外,瀏覽器預期非同源伺服端要回應Access-Control-Allow-Origin等標頭,若伺服端不提供,瀏覽器就會拋出存取錯誤。

在傳統script標籤上,只出現crossorigin屬性,或是crossorigin設為空字串之際,都相當於設定crossorigin="anonymous",這時會遵循CORS(請求模式會是"cors"),非同源請求會附上Origin標頭,然而,不會附上憑證(如cookie、證書等,也就是CORS的憑證模式會是"same-origin"),如果非同源的.js伺服端要求提供憑證,crossorigin必須設為"use-credentials"(也就是CORS的憑證模式會是"include")。

在瀏覽器中,模組必定遵循CORS,也就是script的type設為"module"時,就相當於自動加上了crossorigin屬性,此時,無論是透過src外聯,或者在程式碼中import,甚至是動態地呼叫import(),若來源模組非同源,非同源伺服端提供模組檔案時就必須實現CORS,瀏覽器在非同源請求模組檔案時,預設並不附上憑證,如果需要提供憑證,必須自行設置crossorigin為"use-credentials"。

釐清程式庫採取的方式

ES6模組畢竟是標準,瀏覽器中的ES6模組預設就是非同步地下載,按照頁面的出現順序執行,跨域請求時必然遵守CORS,定義上比較完整,在理想的情況下,我們應該採用這種作法。相對而言,就是細節較多,非同源的伺服端也必須實現CORS,如果有興趣認識更多,你可以參考〈ECMAScript modules in browsers〉(https://bit.ly/2r8QMH3)。

然而,就現今來說,並不會如此理想,像是〈Real World Experience with ES6 Modules in Browsers〉(https://bit.ly/2MrrPkv)談到,程式庫可能提供傳統JS與模組版本,須考慮到一些轉譯程式碼的工具採用,甚至可能有自訂載入器的需求,因此,唯有對傳統JS與ES6模組匯入的細節有更多掌握的情況下,面對這類複雜,才有處理的可能性。

專欄作者

熱門新聞

Advertisement