Webpack4 進階與實踐

Entry 與 Output 的基礎配置

經過 《Webpack4 基礎入門與實踐》 的基礎學習,已經瞭解到應用程序經過 webpack 執行打包的入口 entry 及文件輸出 output。它們的默認值分別是 './src'main.jsjavascript

但若是咱們的項目是一個可能會有多個入口文件的多頁面的應用。css

entry: {
    home: './home.js',
    about: './about.js',
    contact: './contact.js'
  }
複製代碼

那麼此時 output 下的配置也要進行相應的更改。最簡單的方式就是經過佔位符去設置打包輸出的文件名。html

output: {
  publicPath: 'http://cdn.com' // 或指定目錄 '/assets/',
  filename: '[name].js' // name 佔位符
}
// 打包輸出 home.js、about.js、contact.js
複製代碼

不少時候咱們可能會將打包生成的文件給後端或者將資源託管到 CDN 時。那麼咱們在打包生成的 html 文件內去引用這些文件就須要一個指定的 URL 前綴。打包後的文件在 html 內的引用方式會以下所示:前端

<script src="http://cdn.com/about.js"></script>
複製代碼

查閱文檔,學習更多 entryoutput 的配置參數以及 Output Management 的知識。java

Source Map 的配置

當 webpack 打包源代碼時,可能會很難追蹤到 error(錯誤) 和 warning(警告) 在源代碼中的原始位置。node

例如,若是將三個源文件(a.js, b.jsc.js)打包到一個 bundle(bundle.js)中,而其中一個源文件包含一個錯誤,那麼堆棧跟蹤就會直接指向到 bundle.js。你可能須要準確地知道錯誤來自於哪一個源文件,因此這種提示這一般不會提供太多幫助。webpack

爲了更容易地追蹤 error 和 warning,JavaScript 提供了 source map 功能(一個映射關係),能夠將編譯後的代碼映射回原始源代碼。git

經過設置 webpack 的 devtool 屬性就能夠配置 Scource Map。github

// ...
mode: 'development', // 開發環境
devtool: 'source-map', // 默認爲 none
複製代碼

從新打包後,若是有報錯或者警告信息那麼就能夠經過控制檯提示定位到對應位置的代碼。web

同時 dist 目錄下會增長一個 bundle.js.map 文件。打包後的文件代碼與源代碼就是經過這個文件進行映射的。

查看文檔,webpack 提供了十幾種 source map 的配置格式。其中:

  1. inline-source-map

    效果與 source map 一致,區別在於這種方式不會生成 map 文件,而是將其映射關係的內容經過 dataUrl 的方式直接寫入到打包後的 bundle.js 文件底部。

  2. inline-cheap-source-map

    上面提到的配置格式,在提示錯誤和警告信息的時候,會幫咱們精確到項目文件代碼中的具體某行某列。

    而加了 cheap 以後,它只會精確到行,而不會精確到哪一列。同時,它會只關心咱們寫的業務代碼,再也不關心其餘部分的代碼,因此這種方式的構建過程就會變得比較快。

  3. inline-cheap-module-source-map

    若是咱們在 cheap 的基礎上還想讓 webpack 爲咱們提供 loader、第三方模塊等部分代碼的錯誤信息,那麼能夠加上 module 關鍵字。

  4. eval

    效果與 source map 的方式是同樣的,可是 dist 目錄下不會生成 map 文件,同時 bundle.js 文件底部也沒有 Base64 的 dataUrl 字段。可是有一個 eval() 方法,因此它是經過 eval 的 js 執行形式來生成 source map 的對應關係的。

    這種方式是執行效率最快,性能最好的方式,但缺點是針對於比較複雜的代碼狀況下,不能正確的顯示行數。

最佳實踐:

  • 開發環境 development: cheap-module-eval-source-map
  • 生成環境 production: nonecheap-module-source-map
  • 或者使用 SourceMapDevToolPlugin 進行更細粒度的配置。(切勿和 devtool 選項同時使用 )

模塊熱更新 Hot Module Replace

在咱們使用 webpack-dev-server 實現代碼的熱加載以後,每次源代碼有改動,這個本地服務器就會自動幫咱們打包並刷新瀏覽器。這確實很方便,但其實有時候咱們其實卻並不但願它去刷新頁面。

好比,咱們在頁面上經過不少的點擊交互操做,最終在頁面顯示出一個特定列表。而後爲了修改列表項樣式,咱們對源代碼作了更改。若是此時 webpack-dev-server 幫咱們自動刷新瀏覽器頁面了,那咱們就須要再從新進行一遍點擊操做才能看到更改樣式後的列表… 😟

此時咱們就可使用 webpack-dev-server 的模塊熱更新功能 (HMR)。

  1. 修改 devServer 配置項
devServer: {
  port: 8086,
  hot: true, // 開啓 hmr 功能
  hotOnly: true // 可選,意思是即便 hrm 不生效,瀏覽器也不刷新
},
複製代碼
  1. 引入 HotModuleReplacementPlugin 插件 (webpack 自帶)
// 先引入 webpack
const webpack = require('webpack');
//...
plugins:[
	new HtmlWebpackPlugin({
		template: './src/index.html'
	}),
  // 使用插件
	new webpack.HotModuleReplacementPlugin()
]
複製代碼
  1. 重啓一下npm start 使修改後的配置文件生效

關於熱模塊替換的更多用法指南、及實現原理、API 用法能夠翻閱如下文檔:

指南概念API

Tree Shaking

開發過程當中咱們常常會須要 import 一些外部的公共方法來實現方法複用,但咱們大多數時候都是隻須要這個公共方法模塊裏的幾個方法,而不是所有。藉助 Tree Shaking,咱們就能夠將模塊中沒有用到的方法搖晃掉。

Tree Shaking 只支持 ES module 這種靜態的 import 的模塊引入方式,而不支持 common js 動態的 require 引入方式。

配置: 默認的開發模mode: 'development' 是沒有 tree shaking 功能的,要想加上 tree shaking 首先在配置文件中加入 optimization 配置項。

{
  plugins: [
    //...
  ],
  optimization: {
    usedExports: true // 只將使用到的導出模塊進行打包
  },
}
複製代碼

可是這樣會可能遺漏掉那些不導出任何內容的模塊。實際上,只要 import 引入一個模塊,Tree Shaking 就會檢查這個模塊導出了什麼,代碼引用了什麼,若是沒有導出或者沒有引用,就會忽略這個模塊引入。

好比@babel/poly-fill這種只是單純地在 window 對象上綁定了一些全局變量而不導出內容的模塊,或者是代碼裏引入的一些 CSS、SCSS 樣式文件。

此時要在 package.json 中加入sideEffects配置,將這些須要特殊處理的模塊放進一個數組裏。

{
  "name": 'webpack-demo',
  "sideEffects": [
    "@babel/poly-fill",
    "*.css",
    "*.scss"
  ]
  // 若是業務邏輯裏沒有要特殊處理的模塊就直接將 sideEffects 設爲 false
  // "sideEffects:false"
}
複製代碼

其實 Development 模式下,即便咱們配置了 tree shaking ,它也不會將你不用的代碼從打包後的 main.js 中剔除掉,而只是在註釋中提醒你一下。🌚

這是由於咱們在開發環境生成的代碼通常都須要作一些調試,若是 tree shaking 把一些代碼刪除掉的話,sourceMap 代碼對應的一些行數就會錯誤,因此開發環境下的 tree shaking 還會保留這些代碼。可是若是咱們真正的要將項目打包上線,將 mode 改成 production,那麼它就會生效了。但同時要注意這時咱們的 devtool 屬性在生成環境通常都使用cheap-module-source-map而不是帶 eval 的配置。

另外在生產環境下,咱們甚至都不用寫上面的 optimization 配置,它會默認按這個配置去執行。可是 sideEffects 仍是要本身配置的。🤪

開發與生產模式的配置

由上可見,開發環境與線上生產環境的配置在不少地方是有區別的。爲了方便起見,咱們也能夠編寫兩份不一樣的配置文件,來實現兩種環境的切換。

// package.json 文件
"scripts": {
  // "start": "webpack-dev-server --open" 開發環境
  "dev": "webpack-dev-server --config webpack.dev.js",
  // "build": "webpack --mode development" 生產環境
  "build": "webpack --config webpack.prod.js"
}
複製代碼

而後遵循不重複原則 (Don't repeat yourself - DRY),建立一個 webpack.common.js 文件來報存兩種環境下的通用配置。

而後再安裝使用cnpm i webpack-merge -D將這些配置合併在一塊兒。此工具會引用 "common" 配置,所以咱們沒必要再在環境特定的配置中編寫重複代碼。

// webpack.prod.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const prodConfig = {
    mode: 'production',
    plugins:[
        new HtmlWebpackPlugin({
            template: './src/index.html'
        }),
    ]
}
module.exports = merge(commonConfig, prodConfig)
複製代碼

也能夠將這些新加的配置文件統一放入一個 build 文件夾內,但同時要注意修改各個配置文件及 package.json 裏 script 字段的文件路徑。

代碼分離 Code Splitting

webpack 默認會根據配置將咱們項目的代碼都打包到 output 的文件中,但其實咱們的項目代碼不全是業務代碼,確定會引用一些第三方類庫或者像 lodash 這樣的工具函數庫。若是都各類代碼全都打包到一個 JS 文件輸出,不只頁面加載這個文件時不只會很慢,並且一旦業務代碼有任何改動,下次訪問就須要從新獲取。

因此咱們須要將代碼進行分離,而後將不一樣的代碼打包到多個文件輸出,這樣下次訪問時由於瀏覽器的緩存機制,沒有變更的代碼文件便不用去從新獲取。

代碼分離是 webpack 中最引人注目的特性之一。此特性可以把代碼分離到不一樣的 bundle 中,而後能夠按需加載或並行加載這些文件。代碼分離能夠用於獲取更小的 bundle,以及控制資源加載優先級,若是使用合理,會極大影響加載時間。

入口起點

代碼分離最簡單的方法就是經過手動配置 webpack 的入口起點來實現。

// 在 src 下新建一個 lodash.js 並將 lodash 掛載到全局
import _ from 'lodash';
window._ = _;

// 配置入口起點
entry: {
    index: './src/index.js',
    lodash: './src/lodash.js' // 打包 lodash.js
}
複製代碼

而後從新運行打包命令,會發現 dist 下多出一個單獨打包 lodash 工具函數庫代碼的 lodash.js,且打包後的 index.html 裏也能自動引入。

但這種方式存在一些隱患:

  • 若是入口 chunk 之間包含一些重複的模塊,那些重複模塊都會被引入到各個 bundle 中。
  • 這種方法不夠靈活,而且不能動態地將核心應用程序邏輯中的代碼拆分出來。

這兩點中的第一點,對咱們的示例來講毫無疑問是個嚴重問題,由於咱們若是在 ./src/index.js 中也引入 lodash,這樣就形成在兩個 bundle 中重複引用。咱們能夠經過使用 SplitChunksPlugin 插件來移除重複模塊。

去除重複

其實,本質上 Code Splitting 只是一個分割代碼的概念,與 webpack 沒有直接關係。但之因此說它是 webpack 的特性是由於 webpack4 裏面直接捆綁了SplitChunks這樣的插件,咱們不用再手動配置或是安裝其餘插件就能夠很方便的實現代碼分割。

optimization: {
	splitChunks: {
    chunks: 'all'
  }
}
複製代碼

該插件能夠將公共的依賴模塊提取到已有的 entry chunk 中,或者提取到一個新生成的 chunk。好比經過設置配置文件的optimization.splitChunks選項,此插件將 lodash 這個沉重負擔從主 bundle 中移除,而後分離到一個單獨的 chunk 中,同時將項目中重複的依賴項刪除掉。

另一些由社區提供,對於代碼分離頗有幫助的 plugin 和 loader:

動態引入

當涉及到對動態引入的代碼進行拆分時,webpack 推薦選擇的方案是:使用符合 ECMAScript 提案import() 語法 來實現動態導入。👉🏻dynamic imports

// 動態引入 lodash 的 demo 
function getComponent() {
  // Lodash, now imported by this script
	return import('lodash').then(({ default: _ }) => {
		var element = document.createElement('div');
		element.innerHTML = _.join(['Hello', 'webpack'], '🎉');
		return element;
	}).catch(
    error => 'An error occurred while loading the component');
}

getComponent().then(component => {
	document.body.appendChild(component);
})
複製代碼

因爲 import() 會返回一個 promise,所以它能夠和async一塊兒使用。可是,須要使用像 Babel 這樣的預處理器和 Syntax Dynamic Import Babel Plugin

async function getComponent() {
  var element = document.createElement('div');
  
  // Notice the default
  const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  return element;
}
複製代碼

/*webpackChunkName: "lodash"*/ :import() 語法的魔法註釋,爲動態引入的模板設置打包後的文件名。

Lazy Loading 懶加載

懶加載或者按需加載,並非 webpack 裏的概念,而是一種很好的優化網頁或應用的方式。藉助 EcmaScript 的 import() 實驗性質語法的支持,從而實現先把代碼在一些邏輯斷點處分離開,而後在代碼塊中完成某些操做後,再引用另一些新的代碼塊。

經過懶加載,頁面能夠在執行的時候須要哪一個模塊再去請求對應模塊的源代碼(由於某些代碼塊可能永遠不會被加載),而不是一次性地把全部代碼都加載到頁面上,減少了整體體積,因此能夠加快應用的初始加載速度。

// 利用懶加載,只有頁面被點擊後纔會加載 lodash
document.addEventListener('click', () => {
  getComponent().then(component => {
    document.body.appendChild(component);
  })
})
複製代碼

SplitChunksPlugin 配置

上面👆咱們經過設置 SplitChunksPlugin 的splitChunks.chunks配置就實現了去除重複依賴項以及同步與異步動態引入代碼的打包分離。

這是該插件的默認配置:

optimization: {
	splitChunks: {
  	chunks: 'async',
    minSize: 30000,
    maxSize: 0,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
    	vendors: {
      	test: /[\\/]node_modules[\\/]/,
        priority: -10,
        filename: 'vendors.js'
      },
      default: {
      	minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}
複製代碼

chunks: async 只對異步引入的模塊進行打包分離,initial 同步,all 二者均可。

minSize/maxSize: 模塊的文件大小範圍。

minChunks: 打包生成的 chunks 至少有幾個引用了該模塊,符合條件的模塊纔會被分離。

maxAsyncRequests: 同時加載的模塊數,後續超出部分的模塊不會被分離。

maxInitialRequests: 入口文件引用模塊代碼分離的上限數。

automaticNameDelimiter: 生成的文件名默認鏈接符。

name: 使下面設置的 filename 生效,從而能夠爲生成的文件重命名。

cacheGroups: 緩存組 打包同步引入的代碼時必須配合這個配置項一塊兒使用才能生效,它決定分離出來的代碼到底要放到哪一個文件裏面。vendors 爲默認的分組名,test 爲模塊來源,priority 當前組的優先級,先放入優先級高的分組下的文件裏。reuseExistingChunk 忽略已打包過的模塊,直接複用。

想要更好的控制代碼分離的流程,請查閱 SplitChunksPlugin

Prefetch 和 Preload

既然咱們經過將splitChunks.chunks字段配置成 all 以後,能夠同時對同步與異步代碼進行分割,將 loadash、jQuery 等代碼單獨分割打包生成一個文件。在第一次加載後,再次訪問就可使用瀏覽器緩存來提升訪問速度。

那爲何 webpack 還要將其默認值設爲async,只對異步代碼進行分割打包呢?

這是由於按照咱們的上述配置,只經過將 jQuery、loadash 打包成單獨的文件,加載後使用緩存只能提升第二次及之後再訪問這些文件時的速度,而不能真正對頁面訪問性能作優化。webpack 所但願達到的效果是第一次訪問頁面時,它的速度就是最快的。

document.addEventListener('click', () => {
	const element = document.createElement('div');
	element.innerHTML = 'Bingo!!!';
	document.body.appendChild(element);
})
複製代碼

好比這樣一段代碼,在頁面加載以後,咱們能夠在 Chrome 控制檯經過查看到 Sources 源文件裏代碼的執行狀況以及 Coverage 的 Unused Bytes 覆蓋率。 其中點擊回調方法裏的代碼是須要點擊以後纔會被覆蓋執行到的。

若是想提升頁面核心代碼的利用率,咱們能夠將那些交互以後纔用到的代碼方法封裝到另一個文件中,再在須要執行的時候將其加載進來。

// 將點擊的回調方法封裝進 click.js
function handleClick() { 
  const element = document.createElement('div');
	element.innerHTML = 'Bingo!!!';
	document.body.appendChild(element);
}
export default handleClick;
// index.js
document.addEventListener('click', () => {
	// 這裏經過 default 來拿到導出的方法後重命名爲 func
	import('./click.js').then(({default: func}) => {
		func();
	})
})
複製代碼

因而可知,webpack 認爲只有這種異步的組件才能真正的提高網頁的打包性能。而同步的代碼模塊只能增長一個緩存,而對性能的提高是有限的。即咱們在作前端代碼性能優化的時候,最重要的點其實不是緩存,而是 Code Coverage 代碼覆蓋率。即緩存帶來的代碼性能提高是很是有限的,而應該經過提升頁面核心代碼的覆蓋和利用率,從而提高代碼性能與頁面加載速度。

一些網站的登陸模態框功能就是使用這種方式去實現的,可是若是咱們只在點擊後纔去加載登陸相關的代碼,加載速度有可能會比較慢,影響用戶體驗。那麼此時就須要用到 webpack 預取 prefetching 和預加載 preloading 模塊) 的功能。從而既能提升首頁核心代碼的加載速度,同時也能夠在頁面展現完成後將登錄功能的代碼加載進來,保證用戶點擊登陸後的快速響應。

import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
		func();
	})
複製代碼

經過加入/* webpackPrefetch: true */後咱們就能夠等待頁面核心代碼加載完成以後,瀏覽器帶寬閒置時再去懶加載 prefetch 對應的文件。

至於 webpackPreload,與 webpackPrefetch 不一樣的一點就在於它是和業務代碼主線程一塊兒去加載的。

這裏也並不適用 webpackPreload,關於二者細節的區別請查看文檔。另外不正確地使用 webpackPreload 也會有損性能。

打包分析

當咱們使用 webpack 對各類模塊代碼進行了分離打包以後,理所應當應該利用一些打包分析的工具來對輸出的結果進行檢查,分析是否合理。

使用 webpack 官方打包分析工具 生成一個打包分析的說明文件 stats.json,而後能夠上傳到 這裏 上查看結果。

// package.json
"scripts": {
  // 能夠在 build 字段中加入 --profile --json > stats.json 
  "build": "webpack --profile --json > stats.json --config webpack.prod.js",
  // 也能夠專門添加一條生成分析文件的指令
  "analyse": "webpack --profile --json > stats.json"
},
複製代碼

其餘一些打包分析的可視化工具:

  • webpack-chart:webpack stats 可交互餅圖。
  • webpack-visualizer:可視化並分析你的 bundle,檢查哪些模塊佔用空間,哪些多是重複使用的。
  • webpack-bundle-analyzer:一個 plugin 和 CLI 工具,它將 bundle 內容展現爲便捷的、交互式、可縮放的樹狀圖形式。
  • webpack bundle optimize helper:此工具會分析你的 bundle,併爲你提供可操做的改進措施建議,以減小 bundle 體積大小。

CSS 代碼分離

上面咱們已經實現了對項目引用的一些 JS 代碼進行分離打包,可是 CSS 代碼依然仍是被打包進了 JS 文件裏 (css-in-js) 。

要想對 CSS 文件也進行分離打包,可使用 MiniCssExtractPlugin

但目前這個插件還不支持 HMR,因此咱們只是在線上環境使用。

使用yarn add mini-css-extract-plugin -D完成安裝後,配置 webpack 線上環境的文件。

// webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // 可選參數
      filename: "[name].css",
      chunkFilename: "[name].chunk.css"
    })
  ],
  module: {
    rules: [
    	{
      	test: /\.(css|scss)$/,
        use: [
          // 與開發環境使用 style-loader 不一樣,這裏要使用 MiniCssExtractPlugin 的 loader
        	MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      }
    ]
  }
}
複製代碼

若是想對打包後的 css 文件代碼進壓縮可使用官方推薦的 optimize-css-assets-webpack-plugin(Webpack5 已內置)

其餘關於多入口的時候打包對應 CSS 的需求,配置相關的optimization.splitChunks.cacheGroups便可實現。

Caching 緩存

目前咱們每次打包後生成的文件都是按如下格式來配置的:

output:{
	// 使用佔位符來命名即項目直接引用的 app.js
	filename:"[name].js",
	// 代碼在 node-modules 下的模塊打包生成的 chunks 會按照這個格式命名
 	chunkFilename: "[name].chunk.js"
}
複製代碼

這樣有一個問題,就是若是咱們更改了源代碼,將從新打包生成的文件放到服務器以後。在瀏覽器端去請求時,會由於本地有緩存的同名文件,而不會去使用最新上傳服務器的文件,致使最新代碼沒法生效。

開發環境下咱們不須要關心緩存的問題,由於 HMR 會爲咱們解決這個問題,因此咱們只須要使用[contenthash]佔位符修改一下生成環境的打包配置。

output:{
	filename:"[name].[contenthash].js", 
	// app.6df1cf350155facd60f5.js
	chunkFilename: "[name].[contenthash].js"
	// lodash.96223b700cd12e8a3a4d.js
}
複製代碼

使用一串哈希值爲文件內容添加上一個標識,這樣只要咱們的源代碼不發生改變,打包後的文件名就不會變。相應的,若是源代碼發生變更,文件名的 hash 值也會發生改變。從而解決本地緩存的問題。

一些由於舊版本的 boilerplate(引導模板),特別是 runtime 和 manifest 引發的,不改動源代碼,文件名卻發生變化的問題,查看官網具體 最佳方案 解決。

Shim 預置依賴

在 webpack 打包過程當中,當咱們要利用 polyfill 來作代碼方面的兼容擴展瀏覽器能力,或者是處理打包過程的兼容時,可使用 Shim 預置依賴的功能。

常見的一些方案與工具查看官方 shim 預置依賴文檔。

相關文章
相關標籤/搜索