banner
 Sayyiku

Sayyiku

Chaos is a ladder
telegram
twitter

動態加載 JS 文件

對於 Vue、React 等框架開發的單頁應用,在某些頁面開發特殊功能時經常需要依賴第三方 JS 檔案,如果在全局引入 CDN 資源可能會加載冗余檔案,此時最好使用動態加載方式。

動態加載 JS 腳本指僅在某些特殊頁面引入依賴檔案,而非全局引入,這樣可以避免在這些頁面並未打開時造成加載無用的資源,提高頁面加載速度的同時,也讓整個專案更加模塊化。

文件物件模型(DOM)允許使用 JavaScript 動態創建 HTML。<script> 元素也是如此,它與頁面其他元素沒有什麼不同,所以可以手動創建 <script> 來加載 JS 檔案。

defer 與 async#

<script> 元素有兩個屬性 deferasync 分別代表兩種 JS 腳本的加載執行模式。

defer:此布林屬性被設置為向瀏覽器指示腳本在文件被解析後執行。
async:設置此布林屬性,以指示瀏覽器如果可能的話,應異步執行腳本。

對於 defer,可以認為是將外鏈的 js 放在了頁面底部。js 的加載不會阻塞頁面的渲染和資源的加載。defer 會按照原本的 js 的順序執行。

對於 async,它的作用是能夠異步的加載和執行腳本,同樣不會阻塞頁面的渲染和資源的加載,一旦加載到就會立刻執行。在有 async 的情況下,js 一旦下載好了就會執行,所以很有可能不是按照原本的順序來執行的。如果多個腳本檔案前後具有相互依賴性,用 async 就很有可能出錯。

所以通俗來講,瀏覽器首先會請求 HTML 文件,然後對其中的各種資源調用相應的資源加載器進行異步網路請求,同時進行 DOM 渲染,直到遇 <script> 到標籤的時候,主進程才會停止渲染等待此資源加載完畢然後執行,繼而繼續進行 DOM 解析。如果加了 async 屬性就相當於單獨開了一個進程去獨立加載和執行,而 defer 是和將 <script> 放到 <body> 底部一樣的效果。

defer 與 async

上圖藍色線代表網路讀取,紅色線代表執行時間,綠色線代表 HTML 解析。

const loadJS = (url, defer) => {
  const recaptchaScript = document.createElement('script')
  recaptchaScript.setAttribute('src', url)
  if (defer) {
    recaptchaScript.defer = true
  } else {
    recaptchaScript.async = true
  }
  recaptchaScript.onload = () => {
    console.log('加載完成', url)
  }
  document.head.appendChild(recaptchaScript)
}

下面舉個例子,這裡加載五個 JS 腳本,其中 jquery-uifullcalendar 都依賴 jquery,而 locale 依賴 fullcalendar,這種情況需要讓 JS 檔案按照一定的依賴關係按次序加載資源。

const assets = [
  'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js',
  'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
  'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment.min.js',
  'https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.0/fullcalendar.min.js',
  'https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.0/locale/zh-cn.js',
]

assets.forEach((url) => loadJS(url, true))

現實很骨感#

然而在現實環境當中,瀏覽器對於延遲腳本並不一定會按照順序執行,也不一定會在 DOMContentLoaded 事件觸發前執行,因此僅使用 defer 來控制腳本檔案的執行順序有很大的風險,但可以通過監聽 onload 事件來判斷檔案是否加載完成,配合 Promise 等待上一個腳本檔案加載完成後再加載下一個檔案,從而實現按次序加載執行腳本。

const loadJS = (url) => {
  return new Promise((resolve) => {
    const recaptchaScript = document.createElement('script')
    recaptchaScript.setAttribute('src', url)
    recaptchaScript.defer = true
    recaptchaScript.onload = () => {
      resolve()
    }
    document.head.appendChild(recaptchaScript)
  }).catch(console.error)
}

// 按次序加載 JS 檔案
const loadAssets = async () => {
  for (const url of assets) {
    await loadJS(url, true)
  }
}

Just enjoy it ฅ●ω●ฅ

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。