webpack 基礎

概述

關於本文

最近有機會系統地總結一下 webpack 相關的東西,寫篇文章來記錄一下。寫這篇文章一半是爲了本身看,一半給別人看。若是有人看了這篇文章感受頗有用,真是我莫大的榮幸。 首先這不是 《深刻淺出 webpack》, 也不是《一篇文章帶你玩轉 webpack》,更不是 《學習 webpack 這一篇就夠了》。css

如何學 webpack

看文檔是最簡單,最直接的方法,不過官方文檔有些地方寫的不太清楚,這時候,那些文章和博客的做用就比較明顯了,畢竟這些是文章的做者們摸索出來的。好比這篇文章沒有大篇幅複製官方文檔,而是力求多介紹些沒有在文檔中沒體現的部分。html

關於 webpack

webpack 是如今最流行的打包工具,即便如今的 webpack 也能夠像 parcel 同樣實現零配置打包;vue-cli3 也提供了開箱即用的零配置打包方案。可是,這些方案不必定能知足本身的需求。不少時候還須要咱們本身根據本身的需求配置 webpack。vue

安裝

  • webpack4.x 需同時安裝 webpack-cli
  • 推薦局部安裝,而後能夠配合 npx 命令運行局部的 webpack 和 webpack-cli

在命令行使用 npx 命令 調用 webpack: npx webpack npx 會調用 node_modules/.bin 下的 webpack 文件, 在這個文件裏,會先調用 webpack-cli 以讀取命令行參數。node

概念

四個核心概念

  • 入口(entry),指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖的開始。
  • 出口(output),告訴 webpack 在哪裏輸出它所建立的 bundles,以及如何命名這些文件。
  • loader, 將全部類型的文件,轉換爲應用程序的依賴圖(和最終的 bundle)能夠直接引用的模塊。
  • 插件, 在 webpack 打包過程當中增長額外的功能。

bundle, chunck 和 module

官方解釋 大概說明了這幾個概念;jquery

  1. 從前後順序 module:模塊,webpack 模塊(項目的資源) chunck: 代碼塊 (中間產物) bundle: 打包後的結果webpack

  2. 從「包含」 關係 module <= chunck <= bundlegit

module

在 webpack 中任何資源都被做爲模塊處理,webpack 支持 js 各類模塊化標準的模塊,和經過 loader 轉化來的模塊。github

chunck

chunkname, webpack 中每一個 chunk 都會有一個名稱:web

  • 若是,entry 是一個 string 或 array, 就只會生成一個 chunk 名稱爲 main
  • 若是,entry 是 object,就可能會出現多個 chunk,chunk 的名稱是 object 的鍵名。
  • 若是是運行時產生的 chunk,如 CommonsChunkPluginmini-css-extract-plugin 插件產生的 chunk,須要在插件配置中指定 chunk 的名稱。

bundle

webpack 是個 JavaScript 打包器,bundle 是指 webpack 打包後的結果。正則表達式

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle

基本配置

官方文檔 中文文檔

配置文件爲: webpack.config.jswebpackfile.js

入口

基本配置:

const config = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }
};
複製代碼

出口

基礎配置:

{
  output: {
    filename: '[name].js',
    path: '/home/proj/public/assets'
  }
}
複製代碼

注意: path 需爲絕對路徑

webpack-dev-server

注意,這是第三方的模塊, 須要另外安裝。 webpack-dev-server 不是構建本地開發環境的惟一選擇,其餘方式有看這裏。 其實 webpack-dev-server 會啓動一個 express 服務,因此想要更到的自由度,能夠直接使用 express 的 webpack-dev-middleware。

devServer 選項用來配置 wepack-dev-server,基本配置:

{
  devServer: {
    contentBase: path.join(__dirname, 'dist'), // 提供靜態文件的目錄
    compress: true,  // gzip 壓縮
    publicPath: '/assets/', //  bundle 的可訪問路徑
    port: 9000
  }
}
複製代碼

live reloading 和 hot module replacement(HMR)

webpack-dev-server 默認已經實現了 live reloading,無需額外配置。 CSS 的 HMR 可使用 style-loader。 Js 的 HMR ,須要在項目代碼裏寫重載代碼。通常藉助開發框架提供的提供的 HMR 方案, 好比 vue 提供的 vue-loader。

模式

webpack4 新增了 mode 選項,能夠指定開發模式/生產模式。

{
  mode: 'production'
}
複製代碼

若是值爲 developmentproduction, 會啓用 webpack 內置的優化策略。

loader

在 webpack 中使用非 js 模塊(或須要特殊處理的 js 模塊,好比 ES 模塊)時,須要使用 loader 進行轉換;使用 module.rules 配置 loader 的規則:

module: {
    rules: [
        {
            test: /\.html$/,
            use: 'html-withimg-loader'
        }
    ]
}
複製代碼

webpack 的配置比較靈活,不少的配置項均可以接受不一樣數據類型的值,好比,上面的 test 屬性,能夠接受字符串、正則表達式,數組,對象,甚至是函數。 一樣,use 屬性的配置也比較靈活,當只使用一個 loader 時,能夠只寫一個字符串(如 css-loader);當使用多個 loader 時,能夠寫一個數組(['css-loader', 'less-loader']),當須要給每一個 loader 一些其餘的配置時,能夠把上面的字符串替換爲一個對象:

{
    test: /\.(png|jpe?g|gif|svg)$/,
    use: {
        loader: 'url-loader',
        options: {
            limit: 8192,
            fallback: 'file-loader',
            name: 'img/[name]-[hash].[ext]'
         }
     }
}
複製代碼

注意: 多個 loader 的執行順序是:自右向左,自下向上; 更多 loader

插件

配置 webpack,很大一部分是在配置插件。 基本的作法是,new 一個插件實例,傳入一些配置信息:

{
    plugins: [
        new HtmlWebpackPlugin({
             title: 'My App'
        })
    ]
}
複製代碼

更多插件及相關配置 看這裏

常見的幾個插件;

  • HtmlWebpackPlugin,建立 HTML 文件
  • TerserJSPlugin,使用 UglifyJS 壓縮混淆 js 代碼
  • MiniCssExtractPlugin, 提取 CSS
  • CleanWebpackPlugin, 清除文件
  • webpack.DefinePlugin, 定義全局常量

注意,隨着 webpack 的升級,一些老項目和網上的一些文章的插件已通過時了,好比:

  1. 抽離 CSS 爲獨立文件,使用 mini-css-extract-plugin, 而再也不建議使用 extract-text-webpack-plugin

  2. webpack 默認安裝 terser-webpack-plugin, 再也不須要使用 uglifyjs-webpack-plugin。由於後者對 ES6+ 的處理問題

  3. 抽離公共代碼的 CommonsChunkPlugin 如今使用功能更強大的 optimization.splitChunks 代替。

模塊解析

webpack 與 Nodejs 的模塊解析類似,查找規則基本相同,前者在功能上有所加強。 模塊路徑解析相關的配置都在 resolve 字段下,經常使用的有:

  • resolve.alias, 設置模塊別名
  • resolve.extensions,自動解析肯定的擴展名
resolve:  {
    extensions:  ['.js',  '.vue',  '.json'],
    alias:  {
        'service':  path.resolve(__dirname, 'src/js/service'),
        'utils':  path.resolve(__dirname, 'src/js/utils')
    }
}
複製代碼

source map

js 的 source map

經過 devtool 配置,也可使用 SourceMapDevToolPlugin 進行更細粒度的配置。 關於 devtool文檔 給出了多種方式,每一種的運算速度都不同,有的適合開發環境有的適合生產環境,能夠根據本身的需求選擇合適的配置。

css 的 source map

須要在樣式處理的各個 loader 中打開 source map 配置。

優化

loader 優化

loader 能夠經過 includeexclude 配置來縮小文件的搜索範圍。

babel-loader 官方提供了還提供了 cacheDirectory 配置,緩存編譯結果,避免重複編譯。

{
    test: /\.js$/,
    exclude: /node_modules/,
    use:  {
        loader:  'babel-loader',
        options:  {
            presets:  ['@babel/preset-env'],
            cacheDirectory:  true
        }
    }
}
複製代碼

模塊查找優化

resolve.modules

配置 webpack 去哪一個目錄下尋找第三方模塊。其默認值爲, ['node_modules']。 和 Nodejs 的模塊查找機制相似,webpack 會先從當前目錄的 ./node_modules 目錄開始查找想要的模塊,若是沒找到,就去 ../node_modules 中查找,再沒有就去 ../../node_modules, 以此類推。 通常狀況下,咱們的第三方模塊都是安裝在根目錄的 node_modules 目錄下的,此時能夠經過 resolve.modules 指定固定的目錄,以減小尋找。

modules: [path.resolve(__dirname, 'node_modules')]
複製代碼

resolve.alias

建立 importrequire 的別名,使模塊引入變得更簡單。

有些庫,安裝到 node_modules 目錄下的文件包括兩部分:

  • 模塊化的代碼,使用這些代碼要通過 webpack 的編譯;
  • 已經打包好的單獨的文件。

當第三方模塊的完整性較強且模塊入口爲模塊化代碼,咱們不但願 webpack 再次編譯的時候,可使用 resolve.alias 將導入模塊換成使用已打包好的文件。

反之亦然,當只使用模塊一部分功能,但願有效利用優化(如 Tree Shaking),且第三方模塊入口爲已打包的單獨文件時,可使用 resolve.alias 將導入的模塊換成使用模塊化的代碼。

resolve.extensions

當導入預計沒有後綴時,webpack 會自動加上後綴去嘗試詢問文件是否存在。resolve.extensions 即是用於配置在嘗試過程當中用到的後綴名列表。

  • 後綴名列表要儘量短;
  • 頻率出現高的文件後綴名放到前面;
  • 在源碼中,導入語句時,儘量加上文件後綴。

module.noParse

讓 webpack 忽略對部分沒有采用模塊化 的文件的遞歸解析。

動態連接庫

項目中引入的第三方模塊,每一次打包都要進行編譯,特別消耗性能。DLLPlugin 提升打包速度的基本原理是,單獨打包一次這些第三方模塊,生成一個單獨的文件,以後在正式打包時就能夠直接從這個文件中引入以前打包好的第三方模塊。

注意: DLL 經常使用於打包第三方模塊,但不侷限於第三方模塊。

先從基本原理提及,好比咱們要單獨打包 A,B 兩個庫:

  1. 單獨打包出一個 js(動態連接庫),暴露出一個變量,這個變量包含模塊 A 和 B 以及其依賴模塊,這些模塊能夠經過其在變量中的 ID 訪問。
  2. 動態連接庫打包時,須要一個清單文件來存儲這些依賴的和 ID 的對應關係。
  3. 在 HTML 中引入單獨打包好的 js 文件,使其暴露的變量在瀏覽器中可訪問。
  4. 真正打包項目時,須要去 2 中的清單中查找是否已經打包,當遇到已經打包過的模塊,則再也不打包。

對應的配置:

  1. 要單獨打包動態連接庫 js,通常新建一個單獨的 webpack 配置文件。爲了便於多人合做,這個配置文件、打包好的動態連接庫及其清單文件最好都使用版本管理。
  2. 這個單獨的 webpack 配置文檔,同其它 webpack 配置文檔基本同樣,最大的不一樣是要使用 webpack.DLLPlugin 生成清單文件。
  3. 瀏覽器中若是要能訪問暴露出的變量,須要在 HTML 中使用 script 標籤引入動態連接庫,最笨的方法手寫(:。
  4. 真正打包項目時,須要經過 webpack.DllReferencePlugin 插件來檢查模塊是否存在動態連接庫裏。

例子:官方提供的例子

其餘玩法

  • 能夠打包出多個動態連接庫。
  • 能夠打包更多的資源,如 css。
  • 結合 npm script 或 webpack-html-plugin 能夠實現過程更高的自動化。

externals

externals 配置選項提供了「從輸出的 bundle 中排除依賴」的方法。相反,所建立的 bundle 依賴於那些存在於用戶環境(consumer's environment)中的依賴。

externals的優點在於,能夠從 CND 引入資源,有效利用瀏覽器緩存。對於 jQuery 這樣的庫來講 externals 是一個不錯的選擇。

與 DLL(動態連接庫) 對比

externals 最終的打包結果(以 jQuery 爲例):

jquery:  function(module,  exports)  {
    eval('module.exports = jQuery;');
}
複製代碼

也就是 webpack 對 externals 的處理僅僅是把全局變量(jQuery)做爲一個依賴模塊進行管理,因此 externals 不具有管理依賴的依賴的能力,也就沒法避免依賴的依賴重複打包

DLL 也有相似的結果 (假如以var __dll__vendor 導出動態連接庫):

'dll-reference __dll__vendor':  function(module, exports) {
    eval('module.exports = __dll__vendor;');
}
複製代碼

打開頁面,能夠在控制檯訪問到這些打包進動態連接庫的模塊:

__dll__vendor.m[id];  // 經過id能夠訪問已經打包進動態連接庫的模塊
複製代碼

externals 相同,DLL 也把一個全局的變量做爲一個依賴進行管理。不一樣的是,這個變量的結構和 webpack 運行時模塊管理的結構是同樣的,其中,依賴和依賴的依賴,均可以經過 ID 訪問到,能夠避免重複打包

提取公共代碼

optimization.splitChunks 選項經過配置 SplitChunksPlugin 來實現提取公共代碼。通常的應用場景是:

  • 提取單/多頁面公共第三方庫代碼
  • 提取單/多頁面公共業務代碼

webpack 在生產環境的提取公共代碼策略和如下因素有關:

  • 代碼被多少次共享或是第三方庫
  • 代碼塊大小
  • 並行按需加載的請求數量
  • 頁面初始化時的並行請求的數量

更多細節請移步官方文檔

咱們在作公共代碼的提取相關配置的時候,也是從上面的幾個角度去配置的。

按需加載

即懶加載,在初始化頁面的時候不一次性加載全部代碼,當須要更多內容時,再對用到的內容進行即時加載。

目前可用寫法:

  • ES 規範的 import() 語法(推薦)
  • 早期 webpack 提供的 require.ensure
import(/* webpackChunkName: "print" */ './print').then(module => {
    // ES6 模塊的 import() 方法引入模塊時,必須指向模塊的 .default 值
     var print = module.default;
});
複製代碼

上面代碼中,/* webpackChunkName: "print" */是以行內註釋的方式爲動態生成的 chunk 賦一個名稱。此處還能夠寫更多的參數如:

  • webpackPrefetch 預取模塊
  • webpackPreload 預加載模塊

關於上面代碼中的 module.default,須要注意:

webpack v4 開始,在 import CommonJS 模塊時,不會再將導入模塊解析爲 module.exports 的值,而是爲 CommonJS 模塊建立一個 artificial namespace object(人工命名空間對象),關於其背後緣由的更多信息,請閱讀 webpack 4: import() 和 CommonJs

總結來講:

  • webpack4 與 webpack3 的 import(),對 CommonJS 模塊處理方式不太同樣;
  • webpack4 開始, import() 時,解析的都是一個命名空間對象了。

CND 加速

把靜態資源上傳到CDN服務上,能得到更快的響應速度。因爲 CDN 服務通常會爲資源開啓很長時間的緩存,因此HTML文件通常不放到 CND 上,並且關閉緩存。而對於靜態的JavaScript、CSS、圖片等文件傳到 CDN 上,同時爲文件名帶上由文件內容 計算出的 hash 指紋。

另外瀏覽器在同一時刻對同一域名的並行請求有限制,通常的解決方案是:把這些靜態資源分散到不一樣域名下的CDN服務上,如:JavaScript文件放到 js.cdn.xx.com 域名下,圖片放到 img.cdn.xx.com 域名下。

同時多個域名又會增長域名解析的成本,這個也須要權衡。固然也能夠經過也能夠在HTML的head 標籤中,增長 <link rel="dns-prefetch" href="//js.cdn.xx.com" /> 預解析域名,以減小域名解析帶來的延遲。

配置:

  • output.publicPath 設置 JavaScript 的基地址。
  • MiniCssExtractPlugin.loader 經過 publicPath 設置 CSS 的基地址。
  • file-loader/url-loader 經過 publicPath 設置圖片等文件的的基地址。

文件 hash 指紋在利用瀏覽器緩存中發揮了很重要的做用,在 webpack 中可用的 hash 的類型有 hash, chunkhash 和 contenthash,下面簡要介紹其主要區別。

hash, chunkhash 和 contenthash

  • hash,由編譯過程當中的 compilation 對象計算獲得的 hash,能夠理解是整個項目的 hash 值,項目中任何文件改變都會形成 hash 不一樣。
  • chunkhash 是根據 chunk 內容計算獲得的 hash,對於每一個 chunk 來講,若是該 chunk 代碼不變,那麼 hash 也將保持不變。
  • contenthash,非webpack提供,而是由 ExtractTextplugin 和 MiniCssExtractPlugin 這些動態建立 chunk 的插件提供。是由抽離出的文件的內容計算獲得的 hash。

tree-shaking

用來消除死碼。webpack 在生產環境,已經默認開啓了 tree-shaking,注意,爲了發揮tree-shaking 的做用,項目中必需使用 ES6 的模塊引用和導出規則。

CSS 的 「tree-shaking」 能夠經過 optimize-css-assets-webpack-plugin 實現。

Scope Hoisting

Scope Hoisting 可讓 Webpack 打包出來的代碼文件更小、運行的更快;webpack默認在生產模式已經開啓 Scope Hoisting。

大概的原理: 早期的 webpack 模塊打包,把模塊打包到一個個做用域隔離的函數中,而後把這些函數表達式放到一個數組裏,做爲參數傳遞給 webpack 運行時模塊解析的函數。其中運行這些函數建立了大量做用域,增長了內存開銷。 ModuleConcatenationPlugin (webpack內置實現Scope Hoisting的插件)經過分析出模塊之間的依賴關係,能夠將一個模塊之內聯函數的形式注入到另外一個模塊。

注意,此插件僅適用於由 webpack 直接處理的 ES6 模塊)。在使用轉譯器(transpiler)時,你須要禁用對模塊的處理(例如 Babel 中的 modules 選項)。

瞭解更多

happyPack

happyPack 能夠實現多進程併發處理 loader 解析。 隨着 webpack 性能的提高,happyPack 會被逐漸淘汰。

Is it necessary for Webpack 4?

Short answer: maybe not.

Long answer: there's now a competing add-on in the form of a loader for processing files in multiple threads, exactly what HappyPack does. The fact that it's a loader and not a plugin (or both, in case of H.P.) makes it much simpler to configure. Look at thread-loader and if it works for you - that's great, otherwise you can try HappyPack and see which fares better for you.

以上引自 happypack GitHub 的 FAQ。

如今能夠嘗試使用 webpack 提供的 thread-loader, 配置也很簡單。

最後

文中有不少都是我的的觀點,若是有啥不對,你們能夠評論區指出。

相關文章
相關標籤/搜索