webpack從入門到精通

2021-05-27 08:00:14

開言

webpack作為現代前端開發最火的模組打包工具,已經成為了前端工程師必備的技能之一。

是:
前端資源構建工具;
靜態模組打包器;
webpack從入口檔案開始,根據模組的依賴關係進行分析,然後生成加工後的靜態資源;
將高階語法轉換成相容性高的通用語法;
引入chunk塊概念–打包–>bundles;
基於nodejs平臺的工具,遵循commonjs模組化規範

本章重點講解

  • webpack設定引數,
  • 開發環境設定,
  • 生產環境設定,
  • 企業級的優化環境設定
  • 等等

預備技能

  • 基本Nodejs知識和Npm指令;
  • 熟悉ES6語法

環境引數

  • Nodejs10版本以上;
  • webpack 4.26版本以上

知識點

  1. loader
  2. plugin
  3. HMR
  4. devtool
  5. resolve
  6. optimization
  7. code split
  8. caching
  9. lazy loading
  10. shimming
  11. library
  12. dll
  13. mode
  14. eslint
  15. babel
  16. pwa
  17. webpack4 /webpack5

webpack的五個核心概念

  1. entry:webpack從該入口開始分解構建內部依賴圖,並打包。
  2. output:打包後的bundles輸出到哪裡,以及檔名。
  3. loaders:webpack本身只認識JS,需要藉助loaders將來處理各種非JS資源
  4. plugins: 打包優化,壓縮,開發輔助,伺服器啟動等等
  5. mode:不同環境的預設。預設是‘production’。會將值賦值給一個全域性變數:process.env.NODE_ENV。webpack根據這個值,啟用不同的plugin和loader等等

安裝

npm i -D webpack webpack-cli

webpack-cli是做什麼的?
如果你使用 webpack v4+ 版本,並且想要在命令列中呼叫 webpack,你還需要安裝 CLI。

通過檢視webpack指令碼,得出webpack命令,本質就是呼叫了webpack-cli來實現的。webpack4+將這個功能單獨拆解出來了。

const runCli = cli => {
	const path = require("path");
	const pkgPath = require.resolve(`${cli.package}/package.json`);
	// eslint-disable-next-line node/no-missing-require
	const pkg = require(pkgPath);
	// eslint-disable-next-line node/no-missing-require
	require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};

/** @type {CliOption} */
const cli = {
	name: "webpack-cli",
	package: "webpack-cli",
	binName: "webpack-cli",
	installed: isInstalled("webpack-cli"),
	url: "https://github.com/webpack/webpack-cli"
};
if (!cli.installed) {
	const path = require("path");
	const fs = require("graceful-fs");
	const readLine = require("readline");

	const notify =
		"CLI for webpack must be installed.\n" + `  ${cli.name} (${cli.url})\n`;

	console.error(notify);
	// ...
} else {
	runCli(cli);
}

執行

在cli中執行入口檔案。

# 因為沒有全域性安裝webpack,所以需要制定本地webpack
./ node_modules/.bin/webpack ./src/index.js 
# 如果global安裝了
webpack ./src/index.js

通過npm指令碼執行-package.json:

scripts: {
	// npm 會主動去查詢環境中的webpack
	"webpack": "webpack ./src/index.js"
}

上面全部使用webpack的預設設定。

自定義設定

在目錄下新增:webapck.config.js,webpack預設會在專案根目錄下查詢這個檔案,當作自定義設定項。

module.exports = {
  mode: 'development'
}

如果放在其他的地址,需要在指令碼中顯式指定。如webpack/dev.js

scripts: {
	"webpack": "webpack --config ./webpack/dev.js ./src/index.js"
}

其他的設定項,可以放到組態檔,也可以當作指令碼引數傳入。【推薦:複雜的放到組態檔】

測試打包後資源是否可用

原始程式碼:

// demo1.js
const greeting = 'Hello World!';
console.log(greeting);

// index.js
import './demo1'

執行指令碼webpack後:

npm run webpack

預設情況下,輸出檔案為:dist/main.js

node dist/main.js
# 成功輸出
# Hello World!

結論:打包js成功了!

webpack預設支援js/json檔案,可以省略檔案字尾。

如果想要打包css檔案呢?

// style1.less
body {color: red}

// index.js
import './style'

報錯如下:

[no extension] src/style1 doesn't exist
[.js] src/style1.js doesn't exist
[.json] src/style1.json doesn't exist
[.wasm] src/style1.wasm doesn't exist
[as directory] src/style1 doesn't exist

用所有預設支援的格式去匹配,都找不到該檔案。
然後,指明檔案字尾

import './style1/css'

報錯如下:

Module parse failed: Unexpected token.
You may need an appropriate loader to handle this file type...

結論:需要安裝loader

打包其他資源

css資源

需要藉助loader,設定webpack.config.js

module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }

當webpack匹配到css結尾的模組時:

  1. 先使用css-loader將css檔案處理成commonjs模組,整合到js中;
  2. 再用style-loader,建立一個style標籤,將上面處理後的css程式碼插入標籤中,當有html檔案使用到這個打包檔案時,將style標籤,插入到head尾部

html資源

使用html-webpack-plugin外掛:

npm i -D html-webpack-plugin

使用:

const htmlWebpackPlugin = require('html-webpack-plugin')
// plugin: 下載 引入 使用
 plugins: [
   // html-webpack-plugin
   new htmlWebpackPlugin()
 ]

執行:npm run webpack
結果:在output指定目錄下,生成了一個index.html檔案。並且將bundle檔案–main.js插入到了script標籤中,預設將script放在head中。

圖片資源

如果在css中引入了圖片資源:

background-image: url(./images/image.png);

那麼需要用到url-loader(用來處理css檔案中url相關圖片檔案的)和file-loader【看情況安裝】:

{
  test: /\.png$/,
  loader: 'url-loader',
  options: {
    // 預設情況limit沒有限制;下面限制為:當圖片小於1kb,才會用url-loader進行處理,將圖片轉化為base64編碼字串【優點:可以減少http請求;缺點:加大js檔案體積】
    // 當圖片大雨1kb時,url-loader就需要依賴file-loader包,將圖片直接copy到dist目錄下,預設資源名字會變成hash名字
    limit: 1 * 1024
  }
}

如果在html中引入image標籤,上面的處理就會出問題了,因為,例如file-loader會修改圖片的名字,此時,html會找不到圖片資源,我們需要用到html-loader

 {
   test: /\.html$/,
  // 處理html中的img標籤資源,負責將img資源引入,然後才能被url-loader處理。
  // html-loader引入圖片是es6規範的,解析時會報錯
  // 解決辦法:將html-loader的esModule設定為false
  loader: 'html-loader',
  options: {
    esModule: false
  }
}

其他資源(字型等)

不需要做壓縮等處理,只需要,複製並輸出。
比如字型檔案:iconfont.ttf iconfont.svg

方案:用排除法匹配其他資源,然後用file-loader處理。匹配到的資源,將會用hash重新命名後,輸出到dist目錄

rules: [
{
  exclude: /\.(html|css|js)/,
  loader: 'file-loader'
}
]

devServer

開發伺服器:用來自動構建、開啟瀏覽器,並自動重新整理等功能。大大提升了開發效率。

 // 開發伺服器只在記憶體中編譯打包,沒有任何輸出
  // 用npx webpack serve
  devServer: {
    contentBase: './dist',
    // 啟動gzip
    compress: true,
    port: 3000
  }

需要安裝webpack-dev-server。

npm i -D webpack-dev-server

當你改動原始碼的時候,webpack就會自動打包,並更新頁面

不同環境的設定(開發/生產/測試)

一般情況下,webpack已經針對不同環境進行了預設的設定設定,只需要用mode去開啟就行了。

所以指令碼上,我們需要傳入不同的引數,或者是建立不同mode的相關config檔案。

// 如果只有一個webpack.config.js檔案,那麼我們可以通過給cli傳入引數
"dist": "webpack --env=dist"
"dev": "webpack --env=dev"
// 使用
const {env} = require('minimist')(process.argv.slice(2), {
  string: ['env'],
});
// 然後根據env給webpack的設定項傳入不同的值。

// 或者可以將組態檔指定為:dev.config.js/dist.config.js等,然後在指令碼中使用不同的檔案
"dist": "webpack --config=webpack/dist.config.js"
"dev": "webpack --config=webpack/dev.config.js"

針對不同環境,我們可以藉助一些分析工具,如:

  1. webpack-bundle-analyzer 開發環境分析工具
  2. webpack-dev-server 開發伺服器
  3. webpack-manifest-plugin 生產環境,可能還需要藉助這個工具
  4. optimize-css-assets-webpack-plugin 生產環境css檔案壓縮
  5. mini-css-extract-plugin 拆分css
  6. 等等

此外我們還可以考慮:

  1. css/js 相容不同瀏覽器的處理工具,如css字首新增工具【postcss-loader,它會去package.json中查詢browserList設定,然後生成不同的相容性字首,需要手動設定process.env_NODE_ENV】等
  2. 將css/js生成不同的檔案【那就不能用style-loader來建立style標籤,而要用mini-css-extract-plugin.loader提取出css,然後生成main.css,用link引入】;或者將檔案css+js整合成一個檔案【css-loader–將css整合到js檔案中】
  3. splitChunk: 將依賴的檔案,如jquery,antd等大檔案,排除不打包到main.js
  4. 或者用externals屬性,去排除一些包,然後用script的方式手動引入CDN中的程式碼

JS的語法檢查:

常用的語法檢查工具是eslint。我們可將eslint嵌到webpack中,用eslint-loader【以來esling】

rules: [
	{
	    // 注意:只檢查原始碼,需要排除node_modules中的程式碼
		test: /\.js$/,
		exclude: /node_modules/,
		loader: 'eslint-loader',
		// 手動設定不同的規則,或者使用第三方的規則,如:airbnb
		// npm i -D eslint-config-airbnb-base eslint-config-import
		// 將airbnb設定到package.json中:eslintConfig: {extends: 'airbnb-base'}
		options: {
			// 自動修復
			fix: false
		}
	}
]

然後執行webpack,如果js程式碼中有些語法不符合airbnb的規範,那麼webpack操作就會失敗,並輸出:錯誤

如果將fix設定為true,那麼錯誤會自動處理,webpack大部分情況不會被打斷,除非eslint處理不了。

JS相容性處理,如相容IE10:

正常情況下,原始碼如果使用es6及以上語法,webpack並不會進行轉換,此時若將bundle直接放到低版本瀏覽器(IE)中執行,指令碼會報錯,如:不認識const等等。

那麼我們需要用到babel:

npm i -D babel-loader @babel/core @babel/preset-env @babel/polyfill core-js
rules: [
{
	test: /\.js$/,
	excludes: /node_modules/,
	loader: 'babel-loader',
	options: {
		// 預設:指示babel做什麼樣的相容性處理,preset-env就是基本處理,將語法轉化為es5及以下相容性強的語法【基本語法】
		// presets: '@babel/preset-env'
		
		// 如果用到promise等語法,那上面的preset-env並不能處理,就需要其他的包:@babel/polyfill 在原始碼入口引入就可以了。
		// 問題:我只需要做一部分相容性處理,但引入@babel/polyfill體積太大了。。。。那麼就需要做按需載入---core-js。然後不需要引入:@babel/polyfill
		presets: [
		'@babel/preset-env',
		{
			// 按需載入
			useBuiltIns: 'usage',
			// 指定版本
			corejs: {version: 3},
			// 指定相容到哪個版本的瀏覽器
			targets: {chrome: '60', firefox: '60', ie: '9'}
		}
		]
	}
}
]

所以最終方案為:@babel/preset-env + core-js

JS壓縮

webpack開啟:

mode: 'prodcution'

HTML壓縮

設定外掛:

plugins: [
  // html-webpack-plugin
  new htmlWebpackPlugin({
    template: './index.html',
    title: 'Demo',
    inject: 'body',
    minify: {
    	// 如:移除空格等等
		collapseWhitespace: true,
	}
  })
]

優化

需要考慮哪些點呢?需要考慮如下兩類:

1. 開發環境優化

  • 優化打包構建速度
    • devServer也是一種優化
    • HMR(只打包修改了的模組–模組熱更新) ,在devServer中設定:hot: true來開啟。
      • css檔案可以使用HMR功能;
      • JS和html不能使用,並且導致html檔案不能熱更新了,
      • 那麼需要修改entry,將html檔案引入。
      • 如果修改html,那麼專案就會全部重新整理。。
      • 那麼html只有一個:需要進行熱更新嗎???? 不需要
      • JS需要HMR嗎?需要,那就在入口js檔案中加入熱更新程式碼
      • 只能處理非入口js檔案,入口檔案修改,肯定會全部打包
    • 開啟快取
      • babel-loader設定快取 cacheDirectory: true 【第二次構建時才會讀取快取】
  • 優化程式碼偵錯功能借用source-map技術
    • sourcemap是:一種原始碼到構建後程式碼的對映技術 ,能提供準確的錯誤資訊,及錯誤在原始碼發生的位置
    • inline-source-map
      • 只生成一個內聯sourcemap檔案【速度更快】
    • hidden-source-map
      • 外部【速度相對較慢】
    • eval-source-map
      • 為每個檔案都生成一個內聯的sourcemap檔案
    • 直接使用webpack的devtools
    • 速度:eval>inline>cheap>…
    • 偵錯最友好:source-map>cheap-module-source-map
    • 中和速度和友好度,推薦選擇:eval-source-map
// html不需要HMR 
entry: ['./src/index.js', './index.html']
devtool: 'inline-source-map'

// index.js
if(module.hot) {
 // 如果開啟了HMR,則監聽下面模組,如果變了,就只打包下面模組
 module.hot.accept('./...js', function(){})
}

2. 生產環境優化

  • 優化打包構建速度
    • 開啟快取
      • babel快取 cacheDirectory: true 【第二次構建時才會讀取快取】
      • 多程序打包:一般是給babel-loader用,使用tread-loader
  • 優化程式碼執行的效能【程序開啟600ms、程序通訊都需要開銷只有工作消耗時間長,才用
    • 檔案資源快取 啟動一個server。或者設定 static靜態資源 的maxAge: 10000強制快取時間為10000ms。同時配合hash值來替換進行資源比較【記得區分css和js的hash生成規則
    • tree-shaking:這個概念的意思就是:刪除未使用到的程式碼,從而達到減少bundle體積的目的。【必須:es模組化+production】【自動開啟
      • 如何package.json中設定了sideEffect: false ,表示原始碼中沒有副作用,那麼所有相關程式碼都會被shaking掉,從而,引發問題,可能會把import ‘xxx’這樣方法引入的檔案刪掉。需要將設定改為:sideEffects: [x.css] ,面對不同版本的webpack,tree-shaking的功能可能會有差異,如果需要開啟該功能,建議將sideEffects指明
    • code-split:優點:程式碼並行載入;減小單個檔案的大小;是按需載入的基礎。實現方式:
      • 多入口。輸出多個bundles。【一般用於多頁面應用的場景】
      • 設定optimization.splitChunk
rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    use: [
      // 開啟多程序打包
      'tread-loader',
      {loader: 'babel-loader', options: {presets: []}}
    ]
  }
]

optmization : {
  splitChunk: {
    // all的意思:將node_modules中程式碼打包成一個單獨檔案:vendor.js
    // 多入口檔案,會共用vendor.js
    // 我們還可以將node_modules中包,拆分成單獨的包,例如xlsx太大,可以單獨引入
    chunks: 'all'
  }
}
  • lazy-loading:使用es6的import()方法實現。需要設定webpackChunkName將其打包成單獨的檔案才能支援懶載入。
  • prefetch: 設定 webpackPrefetch: true實現延遲載入的包預載入。當再使用程式碼import()時,讀取的就是預載入進來的快取【code: 200】【prefetch:等其他資源載入結束後,瀏覽器空閒了才載入,不會阻塞正常資源的載入;行動端、IE相容性差—不推薦】
  • PWA:service work + cache,相容性差;
    • 檢測一個站點有麼有使用pwa技術:f12–network-offline,然後重新整理頁面。
    • offline時,資源請求成功的狀態為:200 (from service worker)
    • 怎麼實現?使用workbox-webpack-plugin
plugins: [
  new workboxWebpackPlugin.GenerateSW({
    // serviceWorker快速啟動
    // 刪除舊的serviceWorker
    // 生成一個serviceWorker組態檔
    clientClaim: true,
    skipWaiting: true
  })
]

// 入口檔案 index.js中註冊serviceWorker
// 1. eslint預設不認識navigator/window全域性變數,需要改package.json中eslintConfig.env: {browser: true},表示支援瀏覽器的變數
// 2. ==SW的程式碼必須執行在伺服器上==,而不能直接通過file://協定存取。
if('serviceWorker' in navigator) {
	window.addEventListener('load', ()=> {
	// service-worker.js這個檔案就是上面webpack生成的組態檔
	  navigator.serviceworker.register('/service-worker.js')
	    .then((success) => {
	      console.log(success)
	    })
	    .catch((error) => {
	      console.log(error)
	    })
	})
}
  • externals:排除掉一些包不進行打包,而用cdn的方式等引入
externals: {
  // 庫名: npm包名
  jquery: 'jQuery'
}
  • dll技術:動態連結庫。指定某些庫不進行打包。並將某個包打包成多個檔案,比splitChunk: all更優化。webpack.DllPlugin()【沒研究】
  • 生產環境原始碼要不要隱藏呢??
    • 肯定不能用 內聯 的sourcemap,會讓程式碼體積過大