web前端知識點(webpack篇)

前端雜貨發表於2020-07-12

webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle

四個概念

  • entry:入口起點,可以配置多頁面。
  • output:出口,專案編譯完成後之後檔案輸出路徑。
  • loader:webpack 自身只理解 JavaScript,loader 能夠去處理非 JavaScript 檔案並轉化 JavaScript,處理原始檔,一次處理一個。
  • plugins:用來擴充套件 webpack 功能,外掛能夠執行很多工。如:打包優化、壓縮等。

核心物件

  • Tapable:控制鉤子的釋出與訂閱。
  • Compiler:webpack 的編譯器,繼承 Tapable 物件,可以廣播和監聽 webpack 事件,webpack 生命週期值存在一個 Compiler 物件。Compiler 在 webpack 啟動時建立例項,compiler 例項中包含 webpack 的完整配置,包括 loaders, plugins 資訊。
  • Compilation:編譯器編譯之後的資源資訊物件,繼承 Tapable 物件,可以廣播和監聽 webpack 事件,Compilation 例項僅代表一次 webpack 構建和生成編譯資源的的過程。

webpack 開發模式開啟 watch 選項,每次檢測到入口檔案模組變化時,會建立一次新的編譯: 生成一次新的編譯資源和新的 compilation 物件,這個 compilation 物件包含了當前編譯的模組資源 module,編譯生成的資源,變化的檔案,依賴的的狀態。

module、chunk、bundle

  • module:js 模組,就是被 require 或 export 的模組,如:ES 模組、commonjs 模組、 AMD 模組。
  • chunk:程式碼塊,是進行依賴分析的時候,程式碼分割出來的程式碼塊,包括一個或多個 module,是被分組了的 module 集合,code spliting 之後的就是chunk。
  • bundle:檔案,最終打包出來的檔案,通常一個 bundle 對應一個 chunk。

打包原理

根據檔案間的依賴關係對其進行靜態分析,然後將這些模組按指定規則生成靜態資源。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖,其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle。webpack 有兩種組織模組的依賴方式,同步、非同步。非同步依賴將作為分割點,形成一個新的塊;在優化了依賴樹之後,每一個非同步區塊都將作為一個檔案被打包。

構建流程

  • 生成 options(將 webpack.config.js 和 shell 中的引數合併到 options 物件)。
  • 例項化 compiler 物件 (webpack 全域性物件,包含 entry、output、loader、plugins等所有配置物件)。
  • 例項化 compilation 物件(compiler.run 方法執行,開始編譯過程,生成 compilation 物件)。
  • 分析入口 js 檔案,呼叫 AST 引擎處理入口檔案,生成抽象語法樹 AST,根據 AST 構建模組的所有依賴。
  • 通過 loader 處理入口檔案的所有依賴,轉換為 js 模組,生成 AST,然後繼續遞迴遍歷,直至所有依賴被分析完畢。
  • 對生成的所有 module 進行處理,呼叫 plugins,合併,拆分,生成 chunk。
  • 將 chunk 生成為對應 bundle 檔案,輸出到目錄。

HMR(熱更新)

模組熱替換功能會在應用程式應用過程中進行替換、新增或者刪除模組,無需載入整個頁面。主要通過以下幾種方式:

  • 保留在完全重新載入頁面時丟失的應用程式狀態。
  • 只更遜變更內容,以節省寶貴的開發時間。
  • 調整樣式更加快速,幾乎相當於在瀏覽器偵錯程式中更改樣式。

webpack 的熱更新,只是提供一套介面和基礎的模組替換的實現。開發者需要在程式碼中通過熱更新的介面(module.hot.xxx)向 webpack 宣告依賴模組和當前模組是否能夠更新,以及更新的前後進行的處理。如果接受更新,那麼需要開發者自己來在模組替換前清理或者保留必要的資料、狀態,並在模組被替換後恢復之前的資料、狀態。在 vue-cli 等腳手架中,熱更新的開發者這邊的工作是由 vue-loader、css-loader 等載入器已經完成了,再配合 webpack 的 module.hot 等 API 來完成了熱更新。

大致熱更新流程:webpack 進行檔案變化監聽,服務端和客戶端使用 websocket 進行通訊,服務端傳遞模組變化資訊給客戶端,客戶端根據檔案變化訊息獲取變更模組程式碼,進行模組的替換。

  • 執行時開啟熱更新配置,在打包的 bundle 裡面包含 HMR runtime 及 websocket 功能。
  • 服務端與客戶端建立 websocket 長連線。
  • webpack 監聽檔案變化,檔案儲存時觸發 webpack 重新編譯,編譯後程式碼儲存在記憶體中。
  • 編譯時會生成 hot-update.json (已改動模組的 json)、hot-update.js (已改動模組的 js)。
  • 通過 websocket 向客戶端傳送 hash 值。
  • 客戶端對比 hash 值,一致走快取,不一致再通過 Ajax 獲取 hot-update.json,json 包含模組 hash 值,再通過 jsonp 請求獲取 hot-update.js。
  • 熱更新 js 模組,若失敗,則 live reload 重新整理瀏覽器代替熱更新(若模組未配置熱更新,則同樣 live reload)。

webpack如何管理模組

當編譯器開始執行、解析和對映應用程式時,manifest 資料集合會保留所有模組的詳細要點。當完成打包併傳送到瀏覽器時,會在執行通過 manifest 來解析和載入模組。無論選擇哪種模組語法,import 或者 require 都已轉換為 __webpack_require__ 方法,此方法指向模組識別符號。通過使用 manifest 中的資料,runtime 能夠查詢模組識別符號,檢索出背後對應的模組。

style-loader 與 css-loader

css-loader 用來識別 css 檔案,轉為 js 物件。

style-loader 將 css-loader 編譯之後的 js 物件轉為 css 並插入 DOM 中。

tree shaking

移除 JavaScript 上下文中的未引用程式碼(dead-code)。

// 用法  package.json
{
  sideEffects: false
}

這個有一定的副作用:如果一個檔案執行一些特殊功能,而不是僅僅暴露一個或多個 export,比如 polyfill,影響全域性作用域,通常又不提供 export,那麼它將會被移除。

如果所有程式碼都不含副作用,則可以標記為 false,如果有,那麼就提供一個陣列:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

在專案中,任何匯入的檔案都受 tree shaking 影響,比如匯入的 css 檔案,我們則需要將它們新增進去:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}

 shimming全域性變數

webpack 中如果需要用到的不規範的第三方庫,有可能會引起全域性依賴,如 jQuery 的 $。但是如果我們需要用到一個全域性變數,可以使用 ProvidePlugin 後,能夠在通過 webpack 編譯的每個模組中,通過訪問一個變數來獲取到 package 包。如果 webpack 知道這個變數在某個模組中被使用了,那麼 webpack 將在最終 bundle 中引入我們給定的 package。

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
 },
 plugins: [
   new webpack.ProvidePlugin({
     _: 'lodash'
   })
 ]
}

polyfills 載入優化

// 傳統引入方式
npm install --save babel-polyfill

// src/main.js
import 'babel-polyfill';

......

polyfills 雖然是一種模組引入方式,但是並不推薦在主 bundle 中引入 polyfills,因為這不利於具備這些模組功能的現代瀏覽器使用者,會使他們下載體積很大、但卻不需要的指令碼檔案。

讓我們把 import 放入一個新檔案,並加入 whatwg-fetch polyfill:

npm install --save whatwg-fetch

// 新建 src/polyfills.js
import 'babel-polyfill';
import 'whatwg-fetch';

// webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: './src/index.js',
  entry: {
    polyfills: './src/polyfills.js',
    index: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};
// index.html 然後根據條件引入
<!doctype html>
<html>
  <head>
    <title>Getting Started</title>
  <script>
    var modernBrowser = (
      'fetch' in window &&
      'assign' in Object
    );

    if ( !modernBrowser ) {
      var scriptElement = document.createElement('script');

      scriptElement.async = false;
      scriptElement.src = '/polyfills.bundle.js';
      document.head.appendChild(scriptElement);
    }
  </script>
  </head>
  <body>
    <script src="index.bundle.js"></script>
  </body>
</html>

 webpack 優化 

  • 保持 node.js 最新版本:較新的版本能夠建立更高效的模組樹以及提高解析速度。
  • loaders 應用於最少數的必要模組:
    // no
    {
      test: /\.js$/,
      loader: "babel-loader"
    }
    
    // yes
    {
      test: /\.js$/,
      include: path.resolve(__dirname, "src"),
      loader: "babel-loader"
    }
  • 儘量少使用不同的 loader/plugin 工具:每個額外的 loader/plugin 都有啟動時間。
  • 使用 Dlls:使用 DllPlugin 將更改不頻繁的程式碼進行單獨編譯。這將改善引用程式的編譯速度,即使它增加了構建過程的複雜性。
  • 儘量保持 chunks 小巧:減少編譯的整體大小,以提高構建效能。
  • Worker Poll:thread-loader 可以將非常消耗資源的 loaders 轉存到 worker pool 中。
  • 持久化快取:使用 cache-loader 啟用持久化快取。使用 package.json 中的 "postinstall" 清除快取目錄。
  • 關閉 Source Maps:Source Maps很耗效能。
  • 開發環境中避免用到只有在生產環境中才能用到的工具:
    UglifyJsPlugin
    ExtractTextPlugin
    [hash]/[chunkhash]
    AggressiveSplittingPlugin
    AggressiveMergingPlugin
    ModuleConcatenationPlugin

     

 

 

未完待續......

 

相關文章