webpack 4 入門

ryoma發表於2019-03-05

導讀

寫這篇文章是為了讓自己在自學 webpack 的過程中有所產出,於是邊讀 webpack 中文文件 邊寫下了這篇文章,裡面的很多例項都是直接挪用的文件中的例項,但在一些概念的理解上我加入了自己的想法「未必精確」,所以讀的時候要抱著「懷疑的態度」。

文章內容不僅僅是簡單的「概念堆疊」,還有一些「重點」概念的「深入理解」,不過篇幅有限我不希望這篇文章變成一份冗長的偽文件,所以全部的內容都是圍繞 webpack 的 4個 核心概念延展開來的,每個配置後面我都會盡量跟上一個例項以更加形象的展示配置的具體作用。

站在我的角度上,讀完這篇文章並不能讓你精通 webpack 但是理解 webpack 中的重要概念,自己編寫一個 webpack.config.js 配置檔案還是可以的。

webpack 簡介

本質上,webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(static module bundler)。在 webpack 處理應用程式時,它會在內部建立一個依賴圖(dependency graph),用於對映到專案需要的每個模組,然後將所有這些依賴生成到一個或多個bundle。

來自 webpack 中文文件

目前都是使用一些成熟的 CLI 工具,一般都內建 webpack 所以我對 webpack 的認知一直比較少,只是大概的瞭解它是用來管理專案中的 .js 檔案依賴,然後打包整個專案的。

核心概念

1. 入口(entry)

對應屬性:entry 預設值:./src/index.js

作用說明: 用來規定 webpack 應該使用哪個模組作為構建內部依賴圖的起點。 webpack 會找出所有「入口模組」(直接或間接)依賴的「模組」和 [library]。

程式碼示例:

// weboack.config.js
module.exports = {
  entry: './path/to/entry/file.js'
}
複製程式碼

2. 出口(output)

對應屬性:output 主輸出檔案默路徑:./dist/main.js 其他檔案預設路徑:./dist/<filename>

作用說明: 用來規定 webpack 在那裡輸出 bundles 以及如何命名這些檔案。

// weboack.config.js
const path = require('path') // Node.js 核心模組,用於操作檔案路徑

程式碼示例:
module.exports = {
  entry: './path/to/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '<WhateverYouLike>.js'
  }
}
複製程式碼

3. 處理器(loader)

對應屬性:module->rules

作用說明: 作為開箱即用的自帶特性,webpack 自身只支援處理 JavaScript 檔案。而 loader 能夠讓 webpack 處理那些非 JavaScript 檔案,並且先將它們轉換為有效「模組」,然後新增到「依賴圖」中,提供給應用程式使用。

屬性特徵:

  1. test: 利用「正規表示式」規定 loader 用於哪些或哪個檔案。
  2. use: 規定執行時使用哪個 loader

程式碼示例:

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

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: 'raw-loader'
      }
    ]
  }
}
複製程式碼

程式碼作用: 當執行包含 .txt 檔案的 require() 或 import 語句時,在它打包之前,先使用 raw-loader 轉換。

4. 外掛(plugins)

對應屬性:plugings

作用說明: 打包優化、資源管理和注入環境變數。

程式碼例項:

// webpack.config.js
const HtmlWebpackPlugn = require('html-webpack-plugin') // 提前通過 npm 安裝
const webpack = require('webpack') //用於訪問內建外掛

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
}
複製程式碼

核心概念解析及擴充

1. 入口(entry)

單入口及其簡寫

// webpack.config.js
module.exports = {
  entry: {
    main: './path/to/entry/file.js'
  }
}

// 可簡寫為如下形式
module.exports = {
  enrty: './path/to/enrty/file.js'
}

/*
 * 當你需要為只有一個入口的應用程式或工具(library)快速設定 webpack 配置時,
 * 簡寫會是個很不錯的選擇。然而,使用此語法在擴充套件配置時有失靈活性。
 */
複製程式碼

思考:當你向 entry 傳入一個陣列時會發生什麼? 解釋:向 entry 傳入「檔案路徑陣列」將建立「多個主入口」。在你想要多個依賴檔案一起注入,並且將它們的依賴導向到一個 chunk 時,傳入陣列的方式就很有用。

物件語法

用法:entry: {<enrtyChunkName: String>: <Path: String | Array>}

// webpack.config.js
module.exports = {
  entry: {
    app: './src/app.js',
    vendors: './src/vendors.js'
  }
}

// 物件語法會比較繁瑣。然而,這是應用程式中定義入口的最可擴充套件的方式。
複製程式碼

常見場景

1. 分離應用程式主體和第三方庫
// webpack.config.js
module.exports = {
  entry: {
    app: './src/app.js'
    vendors: './src/vendors.js'
  }
}

/*
 * webpack 從 app.js 和 vendors.js 開始建立依賴圖。
 * 這些依賴圖是彼此完全分離、互相獨立的(每個 bundle 中都有一個 webpack 引導)。
 * 這種方式比較常見於,只有一個入口起點(不包括 vendor)的單頁應用程式中。
 * 
 * 此設定允許你使用 CommonsChunkPlugin 從應用程式依賴圖中提取 vendor 到 vendor 依賴圖,並把引用 vendor 的部分替換為 __webpack_require__() 呼叫。
 * 如果應用程式依賴圖中沒有 vendor 程式碼,那麼你可以在 webpack 中實現被稱為長效快取的通用模式。
 * 說實話,目前看不懂上面這段話,所以也不曉得怎麼通俗的表述。
 */
複製程式碼
2. 多頁面應用程式
// webpack.config.js
module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }
}

/*
 *  webpack 分離 3 個的依賴圖
 *
 * 在多頁應用中,每當頁面跳轉時伺服器將為你獲取一個新的 HTML 文件。
 * 頁面重新載入新文件,並且資源被重新下載。這給了我們特殊的機會去做很多事:
 * 使用 CommonsChunkPlugin 使所有頁面的應用程式共享程式碼建立依賴圖,
 * 入口增多,多頁應用能夠複用不同入口的大量重複程式碼/模組。
 */
複製程式碼

2. 出口(output)

注意,即使可以存在多個入口,但只配置一個出口設定。

用法

webpack 中配置 output 的最低要求是,將它的值是一個包括以下兩點的物件:

  1. filename: 輸出檔案的檔名。
  2. path: 輸出目錄的絕對路徑。
// webpack.config.js
module.exports = {
  output: {
    filename: '<WhateverYouLike>.js',
    path: '/path/to/project'
  }
}

// 此配置將一個單獨的 .js 檔案輸出到 /path/to/project 目錄中。
複製程式碼

配合多個入口設定

如果配置建立了多個單獨的入口,則應該使用 佔位符 來確保每個檔案具有唯一的名稱。

// webpack.config.js
module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  }
};

// 寫入到硬碟:./dist/app.js, ./dist/search.js
複製程式碼

常用佔位符

內部ID:[id]

入口名稱:[name]

基於構建的hash(每次構建都會改變):[hash]

基於內容的hash(檔案內容改變才會改變):[chunkhash]

高階進階

官網所謂高階進階其實就是利用雜湊佔位符構建隨版本迭代的檔案命名方式這裡不展示了。

比較有用的是如何動態設定 publicPath:

首先,何為 publicPath,以及周邊概念
  1. output.publicPath: 所有資源的基礎路徑,它被稱為公共路徑,以 / 結束,示例:
// webpack.config.js
module.exports = {
  ...
  output: {
    publicPath: '/assets/',
    chunkFilename: '[id].chunk.js'
  }
};

/*
 * HTML loader 輸入出:<link href="/assets/spinner.gif" />
 * CSS:background-image: url(/assets/spinner.gif);
 * 靜態資源最終訪問路徑 = output.publicPath + loader 或外掛等配置路徑
 */
複製程式碼
  1. devServer.publicPath: 確定從哪裡提供 bundle

假設伺服器執行在 http://localhost:8080 並且 output.filename 被設定為 bundle.js。預設 publicPath/,所以你的包可以通過 http://localhost:8080/bundle.js 訪問。

可以修改通過 devServer.publicPath 來修改請求資源時的伺服器字首,示例:

// webpack.config.js
module.exports = {
  ...
  devServer: {
    publicPath: '/assets/'
  }
};

/*
 * 現在可以通過 http://localhost:8080/assets/bundle.js 訪問 bundle。
 * 確保 publicPath 總是以斜槓(/)開頭和結尾。
 * devServer.publicPath 也可以是一個完整的 URL。
 * 一般情況下都要保證 devServer.publicPath 與 output.publicPath 保持一致。
 */
複製程式碼
  1. devServer.contentBase: 告訴伺服器從哪裡提供內容,只有在提供靜態檔案時才需要

預設情況下,將使用當前工作目錄作為提供內容的目錄,但是你可以修改為其他目錄,示例:

// webpack.config.js
module.exports = {
  ...
  devServer: {
    // 推薦使用絕對路徑。
    contentBase: path.join(__dirname, 'public')
  }
};

// 也可以從多個目錄提供內容
module.exports = {
  ...
  devServer: {
    contentBase: [path.join(__dirname, 'public'), path.join(__dirname, 'assets')]
  }
};

// 具體作用不詳,官網並沒有給出說明也懶得查了
複製程式碼
其次,如何動態設定 publicPath
// webpack.config.js
...
const BASE_URL = process.env.NODE_ENV === 'production'
  ? '/'
  : '/'

module.exports = {
  ...
  publicPath: BASE_URL,
  ...
}
// 方法來自 iview-admin vue.config.js
// 我不知道我理解的動態設定對不對,不過官網給的 __webpack_public_path__ 我沒看明白
複製程式碼

3. 處理器(loader)

loader 用於對模組的原始碼進行轉換,可以使你在「載入」模組時預處理檔案。

loader 類似於其他構建工具中「任務(task)」,提供了處理前端構建步驟的方法。

loader 可以將檔案從不同的語言(如 TypeScript)轉換為 JavaScript,或將內聯影象轉換為 data URL。允許你直接在 JavaScript 模組中 import CSS 檔案。

示例

配置 loader 使 webpack 載入 CSS 檔案,或者將 TypeScript 轉為 JavaScript

首先安裝相對應的 loader

npm install --save-dev css-loader
npm install --save-dev ts-loader
複製程式碼

然後配置 webpack 對每個 .css 使用 css-loader,所有 .ts 檔案使用 ts-loader

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' },
      { test: /\.ts$/, use: 'ts-loader' }
    ]
  }
}
複製程式碼

使用 loader 的三種方式

  1. 配置:在 webpack.config.js 檔案中指定 loader。(推薦)

前面展示過了,這裡就不重複了。

  1. 內聯:在每個 import 語句中顯式指定 loader

可以在 import 語句或任何等效於 import 的方式中指定 loader。使用 ! 將資源中的 loader 分開。分開的每個部分都相對於當前目錄解析,示例:

import Styles from 'style-loader!css-loader?modules!./styles.css';
複製程式碼
  1. CLI:利用 shell 命令指定 loader
webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
複製程式碼

loader 特性

  • loader 支援鏈式傳遞。loader 鏈中每個 loader,都對前一個 loader 處理後的資源進行轉換。loader 鏈會按照相反的順序執行。第一個 loader 將(應用轉換後的資源作為)返回結果傳遞給下一個 loader,依次這樣執行下去。最終,在鏈中最後一個 loader,返回 webpack 所預期的 JavaScript
  • loader 可以是同步的,也可以是非同步的。
  • loader 執行在 Node.js 中,並且能夠執行任何可能的操作。
  • loader 接收查詢引數,用於對 loader 傳遞配置。
  • loader 也能夠使用 options 物件進行配置。
  • 除了使用 package.json 常見的 main 屬性,還可以將普通的 npm 模組匯出為 loader,做法是在 package.json 裡定義一個 loader 欄位。
  • 外掛可以為 loader 帶來更多特性。
  • loader 能夠產生額外的任意檔案。

解析 loader

loader 遵循標準的 模組解析。多數情況下,loader 將從模組路徑(通常將模組路徑認為是 node_modules)解析。

loader 模組需要匯出為一個函式,並且使用 Node.js 相容的 JavaScript 編寫。通常使用 npm 進行管理,但是也可以將自定義 loader 作為應用程式中的檔案。按照約定,loader 通常被命名為 xxx-loader(例如 json-loader)。有關詳細資訊,請檢視 如何編寫 loader?

4. 外掛(plugins)

外掛是 webpack 的支柱功能。webpack 自身也構建於外掛系統之上。

外掛目的在於解決 loader 無法實現的其他事。

剖析

webpack 外掛是一個具有 apply 方法的 JavaScript 物件。apply 屬性會被 webpack compiler 呼叫,並且 compiler 物件可在整個編譯生命週期訪問。

// ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    // compiler hook 的 tap 方法的第一個引數,應該是駝峰式命名的外掛名稱。
    // 建議為此使用一個常量,以便它可以在所有 hook 中複用。
    compiler.hooks.run.tap(pluginName, compilation => {
      console.log('webpack 構建過程開始!');
    });
  }
}
// 外掛編寫屬於比較深入的內容,這裡不過多探討,目前僅需要知道實現原理即可
複製程式碼

用法

由於外掛可以攜帶引數/選項,你必須在 webpack 配置中,向 plugins 屬性傳入 new 例項。

配置寫法

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin') //通過 npm 安裝
const webpack = require('webpack') //訪問內建的外掛
const path = require('path')

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    filename: 'my-first-webpack.bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: 'babel-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
}
複製程式碼

瞭解更多

1. 模式(mode)

對應屬性:mode | String

作用說明: 通過將 mode 引數設定為 development, productionnone,可以啟用對應環境下 webpack 內建的優化。預設值為 production

用法

  1. 在配置檔案中設定
// webpack.config.js
module.exports = {
  ...
  mode: 'production'
};
複製程式碼
  1. 通過 CLI 引數設定
webpack --mode=production
複製程式碼

支援模式

選項 描述
development 會將 process.env.NODE_ENV 的值設為 development。啟用 NamedChunksPlugin 和 NamedModulesPlugin。
production 會將 process.env.NODE_ENV 的值設為 production。啟用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.
None 不選用任何預設優化選項

根據 mode 改變編譯行為

// webpack.config.js
var config = {
  entry: './app.js'
  ...
}

module.exports = (env, argv) => {

  if (argv.mode === 'development') {
    config.devtool = 'source-map';
  }

  if (argv.mode === 'production') {
    ...
  }

  return config
}
複製程式碼

2. 模組(modules)

在模組化程式設計中,開發者將程式分解成離散功能塊,並稱之為「模組」。

每個模組具有比完整程式更小的接觸面,使得校驗、除錯、測試輕而易舉。 精心編寫的「模組」提供了可靠的抽象和封裝界限,使得應用程式中每個模組都具有條理清楚的設計和明確的目的。

webpack 將「模組」的概念應用於專案中的任何檔案。

什麼是 webpack 模組

對比 Node.js 模組,webpack 「模組」能夠以各種方式表達它們的依賴關係,幾個例子如下:

樣式:(url(...)) ES2015: import CommonJS: require() HTML: <img src=...> AMD: define | require css/sass/less: @import

支援的模組型別

webpack 通過 loader 可以支援各種語言和前處理器編寫模組。loader 描述了 webpack 如何處理「非 JavaScript(non-JavaScript) 模組」,並且在 bundle 中引入這些「依賴」。

目前 webpack 已經但不限於支援以下語言的 loader:

3. 模組解析(module resolution)

resolver 是一個庫,用於幫助找到模組的絕對路徑。 它幫助 webpack 從每個如 require/import 語句中,找到需要引入到 bundle 中的模組程式碼。 當打包模組時,webpack 使用 enhanced-resolve 來解析檔案路徑。

webpack 中的解析規則

使用 enhanced-resolvewebpack 能夠解析三種檔案路徑:

1. 絕對路徑
// 已經取得檔案的絕對路徑,因此不需要進一步再做解析。
import '/home/me/file';
import 'C:\\Users\\me\\file';
複製程式碼
2. 相對路徑
// 在這種情況下,使用 import 或 require 的資原始檔所在的目錄,被認為是上下文目錄。
// 在 import/require 中給定的相對路徑,會拼接此上下文路徑,以產生模組的絕對路徑。
import '../src/file1';
import './file2';
複製程式碼
3. 模組路徑
import 'module';
import 'module/lib/file';
// 解釋很囉嗦,感興趣可以自己去看一下文件
複製程式碼

快取

每次檔案系統訪問都會被快取,以便更快觸發對同一檔案的多個並行或序列請求。在 觀察模式下,只有修改過的檔案會從快取中摘出。如果關閉觀察模式,會在每次編譯前清理快取。

4. 依賴圖(dependency graph)

任何時候,一個檔案依賴於另一個檔案,webpack 就把此視為檔案之間有「依賴關係」。這使得 webpack 可以接收非程式碼資源(例如 imagesweb fonts),並且可以把它們作為「依賴」提供給你的應用程式。

webpack 從命令列或配置檔案中定義的「入口」開始,遞迴地構建一個依賴圖,這個依賴圖包含著應用程式所需的每個模組,然後將所有這些模組打包為少量可由瀏覽器載入的 bundle(通常只有一個)。

5. 瀏覽器相容性

webpack 支援所有 ES5 相容(IE8 及以下不提供支援)的瀏覽器。webpackimport()require.ensure() 需要環境中有 Promise。如果你想要支援舊版本瀏覽器,你應該在使用這些 webpack 提供的表示式之前,先 載入一個 polyfill

總結

通過整理這篇文件我已經對 webpack 有了一個初步的認識和了解了。

當然如果你要真正的在專案中投入使用 webpack 僅僅閱讀這一篇文章是不夠的,你還需要去深入地閱讀了解文件裡的各種配置引數和其他常用的前端構建工具或前處理器配合 webpack 進行除錯使用。

前路漫漫,與君共勉。

參考

相關文章