從今天開始,學習Webpack,減少對腳手架的依賴(上)

汪圖南發表於2019-05-29

問:這篇文章適合哪些人?
答:適合沒接觸過Webpack或者瞭解不全面的人。

問:這篇文章的目錄怎麼安排的?
答:先介紹背景,由背景引入Webpack的概念,進一步介紹Webpack基礎、核心和一些常用配置案例、優化手段,Webpack的plugin和loader確實非常多,短短2w多字還只是覆蓋其中一小部分。

問:這篇文章的出處?
答:此篇文章知識來自付費視訊(連結在文章末尾),文章由自己獨立撰寫,已獲得講師授權並首發於掘金。

下一篇:從今天開始,學習Webpack,減少對腳手架的依賴(下)

如果你覺得寫的不錯,請給我點一個star,原部落格地址:原文地址

Webpack

注意,本篇部落格 Webpack 版本是4.0+,請確保你安裝了Node.js最新版本。

Webpack 的核心概念是一個 模組打包工具,它的主要目標是將js檔案打包在一起,打包後的檔案用於在瀏覽器中使用,但它也能勝任 轉換(transform)打包(bundle)包裹(package) 任何其他資源。

追本溯源

在學習 Webpack 之前,我們有必要來了解一下前端領域的開發歷程,只有明白了這些開發歷程,才能更加清楚 Webpack 是怎麼應運而生的,又能給我們解決什麼樣的問題。

程式導向開發

特徵: 一鍋亂燉
在早期 js 能力還非常有限的時候,我們通過程式導向的方式把程式碼寫在同一個.js檔案中,一個程式導向的開發模式可能如下所示:

<!-- index.html程式碼 -->
<p>這裡是我們網頁的內容</p>
<div id="root"></div>
<script src="./index.js"></script>
複製程式碼
// index.js程式碼
var root = document.getElementById('root');

// header模組
var header = document.createElement('div');
header.innerText = 'header';
root.appendChild(header);

// sidebar模組
var sidebar = document.createElement('div');
sidebar.innerText = 'sidebar';
root.appendChild(sidebar);

// content模組
var content = document.createElement('div');
content.innerText = 'content';
root.appendChild(content);
複製程式碼

物件導向開發

特徵: 物件導向開發模式便於程式碼維護,深入人心。
隨著 js 的不斷髮展,它所能解決的問題也越來越多,如果再像程式導向那樣把所有程式碼寫在同一個.js檔案中,那麼程式碼將變得非常難以理解和維護,此時物件導向開發模式便出現了,一個物件導向開發模式可能如下所示:

index.html中引入不同的模組:

<!-- index.html程式碼 -->
<p>這裡是我們網頁的內容</p>
<div id="root"></div>
<script src="./src/header.js"></script>
<script src="./src/sidebar.js"></script>
<script src="./src/content.js"></script>
<script src="./index.js"></script>
複製程式碼
// header.js程式碼
function Header() {
  var header = document.createElement('div');
  header.innerText = 'header';
  root.appendChild(header);
}
複製程式碼
// sidebar.js程式碼
function Sidebar() {
  var sidebar = document.createElement('div');
  sidebar.innerText = 'sidebar';
  root.appendChild(sidebar);
}
複製程式碼
// content.js程式碼
function Content() {
  var content = document.createElement('div');
  content.innerText = 'content';
  root.appendChild(content);
}

複製程式碼
// index.js程式碼
var root = document.getElementById('root');
new Header();
new Sidebar();
new Content();
複製程式碼

不足: 以上的程式碼示例中,雖然使用物件導向開發模式解決了程式導向開發模式中的一些問題,但似乎又引入了一些新的問題。

  1. 每一個模組都需要引入一個.js檔案,隨著模組的增多,這會影響頁面效能
  2. index.js檔案中,並不能直接看出模組的邏輯關係,必須去頁面才能找到
  3. index.html頁面中,檔案的引入順序必須嚴格按順序來引入,例如:index.js必須放在最後引入,如果把header.js檔案放在index.js檔案後引入,那麼程式碼會報錯

現代開發模式

特徵: 模組化載入方案讓前端開發進一步工程化
根據物件導向開發模式中的一系列問題,隨後各種模組化載入的方案如雨後春筍,例如:ES ModuleAMDCMD以及CommonJS等,一個ES Module模組化載入方案可能如下所示:

<!-- index.html程式碼 -->
<p>這裡是我們網頁的內容</p>
<div id="root"></div>
<script src="./index.js"></script>
複製程式碼
// header.js
export default function Header() {
  var root = document.getElementById('root');
  var header = document.createElement('div');
  header.innerText = 'header';
  root.appendChild(header);
}
複製程式碼
// sidebar.js
export default function Sidebar() {
  var root = document.getElementById('root');
  var sidebar = document.createElement('div');
  sidebar.innerText = 'sidebar';
  root.appendChild(sidebar);
}
複製程式碼
// content.js程式碼
export default function Content() {
  var root = document.getElementById('root');
  var content = document.createElement('div');
  content.innerText = 'content';
  root.appendChild(content);
}
複製程式碼
// index.js程式碼
import Header from './src/header.js';
import Sidebar from './src/sidebar.js';
import Content from './src/content.js';

new Header();
new Sidebar();
new Content();
複製程式碼

注意: 以上程式碼並不能直接在瀏覽器上執行,因為瀏覽器並不能直接識別ES Module程式碼,需要藉助其他工具來進行翻譯,此時 Webpack 就粉墨登場了。

Webpack初體驗

不建議跟隨此小結一起安裝,此次示例僅僅作為一個例子,詳細學習步驟請直接閱讀下一章節

生成package.json檔案

-y參數列示直接生成預設配置項的package.json檔案,不加此引數需要一步步按需進行配置。

$ npm init -y
複製程式碼

生成的package.json檔案:

{
  "name": "webpack-vuepress",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

複製程式碼

安裝Webpack

-D引數代表在本專案下安裝 Webpack ,它是--save-dev的簡寫

$ npm install webpack webpack-cli -D
複製程式碼

修改程式碼

Webpack預設打包路徑到dist資料夾,打包後的js檔名字叫main.js

其他程式碼不動,將index.html中的.js檔案改成如下引用方式(引用打包後的檔案):

<!-- index.html程式碼 -->
<p>這裡是我們網頁的內容</p>
<div id="root"></div>
<script src="./dist/main.js"></script>
複製程式碼

Webpack打包

引數說明

  1. npx webpack代表在本專案下尋找 Webpack 打包命令,它區別npm命令
  2. index.js引數代表本次打包的入口是index.js
$ npx webpack index.js
複製程式碼

打包結果:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

正如上面你所看到的那樣,網頁正確顯示了我們期待的結果,這也是 Webpack 能為我們解決問題的一小部分能力,下面將正式開始介紹 Webpack 。

安裝

全域性安裝

如果你只是想做一個 Webpack 的 Demo案例,那麼全域性安裝方法可能會比較適合你。如果你是在實際生產開發中使用,那麼推薦你使用本地安裝方法。

全域性安裝命令

Webpack4.0+的版本,必須安裝webpack-cli,-g命令代表全域性安裝的意思

$ npm install webpack webpack-cli -g
複製程式碼

解除安裝

通過npm install安裝的模組,對應的可通過npm uninstall進行解除安裝

$ npm uninstall webpack webpack-cli -g
複製程式碼

本地安裝(推薦)

本地安裝的 Webpack 意思是,只在你當前專案下有效。而通過全域性安裝的Webpack,如果兩個專案的 Webpack 主版本不一致,則可能會造成其中一個專案無法正常打包。本地安裝方式也是實際開發中推薦的一種 Webpack 安裝方式。

$ npm install webpack webpack-cli -D 或者 npm install webpack webpack-cli --save-dev
複製程式碼

版本號安裝

如果你對Webpack的具體版本有嚴格要求,那麼可以先去github的Webpack倉庫檢視歷史版本記錄或者使用npm view webpack versions檢視Webpack的npm歷史版本記錄

// 檢視webpack的歷史版本記錄
$ npm view webpack versions

// 按版本號安裝
$ npm install webpack@4.25.0 -D
複製程式碼

起步

建立專案結構

現在我們來建立基本的專案結構,它可能是下面這樣

|-- webpack-vuepress
|   |-- index.html
|   |-- index.js
|   |-- package.json
複製程式碼

其中package.json是利用下面的命令自動生成的配置檔案

$ npm init -y
複製程式碼

新增基礎程式碼

在建立了基本的專案結構以後,我們需要為我們建立的檔案新增一些程式碼

index.html頁面中的程式碼:

<p>這是最原始的網頁內容</p>
<div id="root"></div>
<!-- 引用打包後的js檔案 -->
<script src="./dist/main.js"></script>
複製程式碼

index.js檔案中的程式碼:

console.log('hello,world');
複製程式碼

安裝Webpack

執行如下命令安裝webpack4.0+webpack-cli

$ npm install webpack webpack-cli -D
複製程式碼

新增配置檔案

使用如下命令新增 Webpack 配置檔案:

$ touch webpack.config.js
複製程式碼

使用此命令,變更後的專案結構大概如下所示:

|-- webpack-vuepress
|   |-- index.html
|   |-- index.js
|   |-- webpack.config.js
|   |-- package.json
複製程式碼

至此我們的基礎目錄已建立完畢,接下來需要改寫webpack.config.js檔案,它的程式碼如下:

// path為Node的核心模組
const path = require('path');
module.exports = {
  entry: './index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製程式碼

配置引數說明:

  1. entry配置項說明了webpack打包的入口。
  2. output配置項說明了webpack輸出配置,其中filename配置了打包後的檔案叫main.js
  3. path配置了打包後的輸出目錄為dist資料夾下

改寫package.json檔案

改寫說明:

  1. 新增private屬性並設定為true,此屬效能讓我們的專案為私有的,防止意外發布程式碼
  2. 移除main屬性,我們的專案並不需要對外暴露一個入口檔案
  3. 新增scripts命令,即我們的打包命令

改寫後的package.json檔案如下所示:

{
  "name": "webpack-vuepress",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "bundle": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  }
}
複製程式碼

第一次打包

npm run代表執行一個指令碼命令,而bundle就是我們配置的打包命令,即npm run bundle就是我們配置的webpack打包命令。

執行如下命令進行專案打包:

$ npm run bundle
複製程式碼

打包後的效果如下所示:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

打包後的專案目錄如下所示,可以看到我們多出了一個叫dist的目錄,它裡面有一個main.js檔案

|-- dist
|   |-- main.js
|-- index.html
|-- index.js
|-- webpack.config.js
|-- package.json
複製程式碼

打包成功後,我們需要在瀏覽器中執行index.html,它的執行結果如下圖所示

從今天開始,學習Webpack,減少對腳手架的依賴(上)

理解webpack打包輸出

在上一節中,我們第一次執行了一個打包命令,它在控制檯上有一些輸出內容,這一節我們詳細來介紹這些輸出是什麼意思

從今天開始,學習Webpack,減少對腳手架的依賴(上)

  1. Hash: hash代表本次打包的唯一hash值,每一次打包此值都是不一樣的
  2. Version: 詳細展示了我們使用webpack的版本號
  3. Time: 代表我們本次打包的耗時
  4. Asset: 代表我們打包出的檔名稱
  5. Size: 代表我們打包出的檔案的大小
  6. Chunks: 代表打包後的.js檔案對應的idid0開始,依次往後+1
  7. Chunks Names: 代表我們打包後的.js檔案的名字,至於為何是main,而不是其他的內容,這是因為在我們的webpack.config.js中,entry:'./index.js'是對如下方式的簡寫形式:
// path為Node的核心模組
const path = require('path');
module.exports = {
  // entry: './index.js',
  entry: {
    main: './index.js'
  }
  // 其它配置
}
複製程式碼
  1. Entrypoint main = bundle.js: 代表我們打包的入口為main
  2. warning in configuration: 提示警告,意思是我們沒有給webpack.config.js設定mode屬性,mode屬性有三個值:development代表開發環境、production代表生產環境、none代表既不是開發環境也不是生產環境。如果不寫的話,預設是生產環境,可在配置檔案中配置此項,配置後再次打包將不會再出現此警告。
// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  mode: 'development'
}
複製程式碼

打包靜態資源

什麼是loader?

loader是一種打包規則,它告訴了 Webpack 在遇到非js檔案時,應該如何處理這些檔案

loader有如下幾種固定的運用規則:

  • 使用test正則來匹配相應的檔案
  • 使用use來新增檔案對應的loader
  • 對於多個loader而言,從 右到左 依次呼叫

使用loader打包圖片

打包圖片需要用到file-loader或者url-loader,需使用npm install進行安裝

$ npm install file-loader -D 或者 npm install url-loader -D
複製程式碼

一點小改動

在打包圖片之前,讓我們把index.html移動到上一節打包後的dist目錄下,index.html中相應的.js引入也需要修改一下,像下面這樣

// index.html的改動部分
<script src="./main.js"></script>
複製程式碼

新增打包圖片規則

對於打包圖片,我們需要在webpack.config.js中進行相應的配置,它可以像下面這樣:

// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: {
          loader: 'file-loader'
        }
      }
    ]
  }
}
複製程式碼

改寫index.js

import avatar from './avatar.jpg'

var root = document.getElementById('root');
var img = document.createElement('img');
img.src = avatar
root.appendChild(img)
複製程式碼

打包後的專案目錄

|-- dist
|   |-- bd7a45571e4b5ccb8e7c33b7ce27070a.jpg
|   |-- main.js
|   |-- index.html
|-- index.js
|-- avatar.jpg
|-- package.json
|-- webpack.config.js
複製程式碼

打包結果

從今天開始,學習Webpack,減少對腳手架的依賴(上)

運用佔位符

在以上打包圖片的過程中,我們發現打包生成的圖片好像名字是一串亂碼,如果我們要原樣輸出原圖片的名字的話,又該如何進行配置呢?這個問題,可以使用 佔位符 進行解決。

檔案佔位符它有一些固定的規則,像下面這樣:

  • [name]代表原本檔案的名字
  • [ext]代表原本檔案的字尾
  • [hash]代表一個唯一編碼

根據佔位符的規則再次改寫webpack.config.js檔案,

// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]'
          }
        }
      }
    ]
  }
}
複製程式碼

根據上面佔位符的運用,打包生成的圖片,它的名字如下

|-- dist
|   |-- avatar_bd7a45571e4b5ccb8e7c33b7ce27070a.jpg
複製程式碼

使用loader打包CSS

樣式檔案分為幾種情況,每一種都需要不同的loader來處理:

  1. 普通.css檔案,使用style-loadercss-loader來處理
  2. .less檔案,使用less-loader來處理
  3. .sass或者.scss檔案,需要使用sass-loader來處理
  4. .styl檔案,需要使用stylus-loader來處理

打包css檔案

首先安裝style-loadercss-loader

$ npm install style-loader css-loader -D
複製程式碼

改寫webpack配置檔案:

// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]'
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'] // 從右到左的順序呼叫,所以順序不能錯
      }
    ]
  }
}
複製程式碼

根目錄下建立index.css

.avatar{
  width: 150px;
  height: 150px;
}
複製程式碼

改寫index.js檔案

import avatar from './avatar.jpg';
import './index.css';

var root = document.getElementById('root');
var img = new Image();
img.src = avatar;
img.classList.add('avatar');
root.appendChild(img);
複製程式碼

打包結果

從今天開始,學習Webpack,減少對腳手架的依賴(上)

打包Sass檔案

需要安裝sass-loadernode-sass

$ npm install sass-loader node-sass -D
複製程式碼

改寫webpack.config.js檔案

// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]'
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(sass|scss)$/,
        use: ['style-loader','css-loader','sass-loader']
      }
    ]
  }
}
複製程式碼

根目錄下新增index-sass.sass檔案

body{
  .avatar-sass{
    width: 150px;
    height: 150px;
  }
}
複製程式碼

改寫index.js

import avatar from './avatar.jpg';
import './index.css';
import './index-sass.sass';

var img = new Image();
img.src = avatar;
img.classList.add('avatar-sass');

var root = document.getElementById('root');
root.appendChild(img);
複製程式碼

根據上面的配置和程式碼改寫後,再次打包,打包的結果會是下面這個樣子

從今天開始,學習Webpack,減少對腳手架的依賴(上)

自動新增CSS廠商字首

當我們在css檔案中寫一些需要處理相容性的樣式的時候,需要我們分別對於不同的瀏覽器書新增不同的廠商字首,使用postcss-loader可以幫我們在webpack打包的時候自動新增這些廠商字首。 自動新增廠商字首需要npm install安裝postcss-loaderautoprefixer

npm install postcss-loader autoprefixer -D
複製程式碼

修改index-sass.sass

.avatar-sass {
  width: 150px;
  height: 150px;
  transform: translate(50px,50px);
}
複製程式碼

在修改sass檔案程式碼後,我們需要對webpack.config.js

// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]'
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(sass|scss)$/,
        use: ['style-loader','css-loader','sass-loader','postcss-loader'] // 順序不能變
      }
    ]
  }
}
複製程式碼

根目錄下新增postcss.config.js,並新增程式碼

module.exports = {
  plugins: [require('autoprefixer')]
}
複製程式碼

根據上面的配置,我們再次打包執行,在瀏覽器中執行index.html,它的結果如下圖所示

從今天開始,學習Webpack,減少對腳手架的依賴(上)

模組化打包CSS檔案

CSS的模組化打包的理解是:除非我主動引用你的樣式,否則你打包的樣式不能影響到我。

根目錄下新增createAvatar.js檔案,並填寫下面這段程式碼

import avatar from './avatar.jpg';
export default function CreateAvatar() {
  var img = new Image();
  img.src = avatar;
  img.classList.add('avatar-sass');

  var root = document.getElementById('root');
  root.appendChild(img);
}
複製程式碼

改寫index.js,引入createAvatar.js並呼叫

import avatar from './avatar.jpg';
import createAvatar from './createAvatar';
import './index.css';
import './index-sass.sass';

createAvatar();

var img = new Image();
img.src = avatar;
img.classList.add('avatar-sass');

var root = document.getElementById('root');
root.appendChild(img);
複製程式碼

打包執行

從今天開始,學習Webpack,減少對腳手架的依賴(上)

我們可以看到,在createAvatar.js中,我們寫的img標籤的樣式,它受index-sass.sass樣式檔案的影響,如果要消除這種影響,需要我們開啟對css樣式檔案的模組化打包。

進一步改寫webpack.config.js

// path為Node的核心模組
const path = require('path');
module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]'
          }
        }
      },
      {
        test: /\.(sass|scss)$/,
        use: ['style-loader', {
          loader: 'css-loader',
          options: {
            modules: true
          }
        }, 'sass-loader', 'postcss-loader']
      }
    ]
  }
}
複製程式碼

開啟css模組化打包後,我們需要在index.js中做一點小小的改動,像下面這樣子

import avatar from './avatar.jpg';
import createAvatar from './createAvatar';
import './index.css';
import style from  './index-sass.sass';

createAvatar();

var img = new Image();
img.src = avatar;
img.classList.add(style['avatar-sass']);

var root = document.getElementById('root');
root.appendChild(img);
複製程式碼

打包執行後,我們發現使用createAvatar.js建立出來的img沒有受到樣式檔案的影響,證明我們的css模組化配置已經生效,下圖是css模組化打包的結果:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

Webpack核心

使用WebpackPlugin

plugin的理解是:當 Webpack 執行到某一個階段時,可以使用plugin來幫我們做一些事情。

在使用plugin之前,我們先來改造一下我們的程式碼,首先刪掉無用的檔案,隨後在根目錄下新建一個src資料夾,並把index.js移動到src資料夾下,移動後你的目錄看起來應該是下面這樣子的

|-- dist
|   |-- index.html
|-- src
|   |-- index.js
|-- postcss.config.js
|-- webpack.config.js
|-- package.json
複製程式碼

接下來再來處理一下index.js檔案的程式碼,寫成下面這樣

// src/index.js
var root = document.getElementById('root');
var dom = document.createElement('div');
dom.innerHTML = 'hello,world';
root.appendChild(dom);
複製程式碼

最後我們來處理一下我們的webpack.config.js檔案,它的改動有下面這些

  • 因為index.js檔案的位置變動了,我們需要改動一下entry
  • 刪除掉我們配置的所有loader規則 按照上面的改動後,webpack.config.js中的程式碼看起來是下面這樣的
const path = require('path');
module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname,'dist')
  }
}
複製程式碼

html-webpack-plugin

html-webpack-plugin可以讓我們使用固定的模板,在每次打包的時候 自動生成 一個.html檔案,並且它會 自動 幫我們引入我們打包後的.js檔案

使用如下命令安裝html-webpack-plugin

$ npm install html-webpack-plugin -D
複製程式碼

src目錄下建立index.html模板檔案,它的程式碼可以寫成下面這樣子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Html 模板</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
複製程式碼

因為我們要使用html-webpack-plugin外掛,所以我們需要再次改寫webpack.config.js檔案(具體改動部分見高亮部分掘金無高亮)

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [
    new htmlWebpackPlugin({
      template: 'src/index.html'
    })
  ],
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname,'dist')
  }
}
複製程式碼

在完成上面的配置後,我們使用npm run bundle命令來打包一下測試一下,在打包完畢後,我們能在dist目錄下面看到index.html中的程式碼變成下面這樣子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>HTML模板</title>
</head>
<body>
  <div id="root"></div>
  <script type="text/javascript" src="main.js"></script>
</body>
</html>
複製程式碼

我們發現,以上index.html的結構,正是我們在src目錄下index.html模板的結構,並且還能發現,在打包完成後,還自動幫我們引入了打包輸出的.js檔案,這正是html-webpack-plugin的基本功能,當然它還有其它更多的功能,我們將在後面進行詳細的說明。

clean-webpack-plugin

clean-webpack-plugin它能幫我們在打包之前 自動刪除dist打包目錄及其目錄下所有檔案,不用我們手動進行刪除。

我們使用如下命令來安裝clean-webpack-plugin

$ npm install clean-webpack-plugin -D
複製程式碼

安裝完畢以後,我們同樣需要在webpack.config.js中進行配置(改動部分參考高亮程式碼塊掘金無高亮)

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new cleanWebpackPlugin()
  ],
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname,'dist')
  }
}
複製程式碼

在完成以上配置後,我們使用npm run bundle打包命令進行打包,它的打包結果請自行在你的專案下觀看自動清理dist目錄的實時效果。

在使用WebpackPlugin小節,我們只介紹了兩種常用的plugin,更多plugin的用法我們將在後續進行講解,你也可以點選Webpack Plugins來學習更多官網推薦的plugin用法。

配置SourceMap

SourceMap的理解:它是一種對映關係,它對映了打包後的程式碼和原始碼之間的對應關係,一般通過devtool來配置。

以下是官方提供的devtool各個屬性的解釋以及打包速度對比圖:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

通過上圖我們可以看出,良好的source-map配置不僅能幫助我們提高打包速度,同時在程式碼維護和調錯方面也能有很大的幫助,一般來說,source-map的最佳實踐是下面這樣的:

  • 開發環境下(development):推薦將devtool設定成cheap-module-eval-source-map
  • 生產環境下(production):推薦將devtool設定成cheap-module-source-map

使用WebpackDevServer

webpack-dev-server的理解:它能幫助我們在原始碼更改的情況下,自動*幫我們打包我們的程式碼並啟動一個小型的伺服器。如果與熱更新一起使用,它能幫助我們高效的開發。

自動打包的方案,通常來說有如下幾種:

  • watch引數自動打包:它是在打包命令後面跟了一個--watch引數,它雖然能幫我們自動打包,但我們任然需要手動重新整理瀏覽器,同時它不能幫我們在本地啟動一個小型伺服器,一些http請求不能通過。
  • webpack-dev-server外掛打包(推薦):它是我們推薦的一種自動打包方案,在開發環境下使用尤其能幫我們高效的開發,它能解決watch引數打包中的問題,如果我們與熱更新(HMR)一起使用,我們將擁有非常良好的開發體驗。
  • webpack-dev-middleware自編碼啟動小型伺服器(不講述)

watch引數自動打包

使用watch引數進行打包,我們需要在package.json中新增一個watch打包命令,它的配置如下

{
  // 其它配置
  "scripts": {
    "bundle": "webpack",
    "watch": "webpack --watch"
  }
}
複製程式碼

在配置好上面的打包命令後,我們使用npm run watch命令進行打包,然後在瀏覽器中執行dist目錄下的index.html,執行後,我們嘗試修改src/index.js中的程式碼,例如把hello,world改成hello,dell-lee,改動完畢後,我們重新整理一下瀏覽器,會發現瀏覽器成功輸出hello,dell-lee,這也證明了watch引數確實能自動幫我們進行打包。

webpack-dev-server打包

要使用webpack-dev-server,我們需要使用如下命令進行安裝

$ npm install webpack-dev-server -D
複製程式碼

安裝完畢後,我們和watch引數配置打包命令一樣,也需要新增一個打包命令,在package.json中做如下改動:

// 其它配置
  "scripts": {
    "bundle": "webpack",
    "watch": "webpack --watch",
    "dev": "webpack-dev-server'
  }
複製程式碼

配置完打包命令後,我們最後需要對webpack.config.js做一下處理:

module.exports = {
  // 其它配置
  devServer: {
    // 以dist檔案為基礎啟動一個伺服器,伺服器執行在4200埠上,每次啟動時自動開啟瀏覽器
    contentBase: 'dist',
    open: true,
    port: 4200
  }
}
複製程式碼

在以上都配置完畢後,我們使用npm run dev命令進行打包,它會自動幫我們開啟瀏覽器,現在你可以在src/index.js修改程式碼,再在瀏覽器中檢視效果,它會有驚喜的哦,ღ( ´・ᴗ・` )比心

這一小節主要介紹瞭如何讓工具自動幫我們打包,下一節我們將講解模組熱更新(HMR)。

模組熱更新(HMR)

模組熱更新(HMR)的理解:它能夠讓我們在不重新整理瀏覽器(或自動重新整理)的前提下,在執行時幫我們更新最新的程式碼。

模組熱更新(HMR)已內建到 Webpack ,我們只需要在webpack.config.js中像下面這樣簡單的配置即可,無需安裝別的東西。

const webpack = require('webpack');
module.exports = {
  // 其它配置
  devServer: {
    contentBase: 'dist',
    open: true,
    port: 3000,
    hot: true, // 啟用模組熱更新
    hotOnly: true // 模組熱更新啟動失敗時,重新重新整理瀏覽器
  },
  plugins: [
    // 其它外掛
    new webpack.HotModuleReplacementPlugin()
  ]
}
複製程式碼

在模組熱更新(HMR)配置完畢後,我們現在來想一下,什麼樣的程式碼是我們希望能夠熱更新的,我們發現大多數情況下,我們似乎只需要關心兩部分內容:CSS檔案和.js檔案,根據這兩部分,我們將分別來進行介紹。

CSS中的模組熱更新

首先我們在src目錄下新建一個style.css樣式檔案,它的程式碼可以這樣下:

div:nth-of-type(odd) {
  background-color: yellow;
}
複製程式碼

隨後我們改寫一下src目錄下的index.js中的程式碼,像下面這樣子:

import './style.css';

var btn = document.createElement('button');
btn.innerHTML = '新增';
document.body.appendChild(btn);

btn.onclick = function() {
  var dom = document.createElement('div');
  dom.innerHTML = 'item';
  document.body.appendChild(dom);
}
複製程式碼

由於我們需要處理CSS檔案,所以我們需要保留處理CSS檔案的loader規則,像下面這樣

module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}
複製程式碼

在以上程式碼新增和配置完畢後,我們使用npm run dev進行打包,我們點選按鈕後,它會出現如下的情況

從今天開始,學習Webpack,減少對腳手架的依賴(上)

理解: 由於item是動態生成的,當我們要將yellow顏色改變成red時,模組熱更新能幫我們在不重新整理瀏覽器的情況下,替換掉樣式的內容。直白來說:自動生成的item依然存在,只是顏色變了。

在js中的模組熱更新

在介紹完CSS中的模組熱更新後,我們接下來介紹在js中的模組熱更新。

首先,我們在src目錄下建立兩個.js檔案,分別叫counter.jsnumber.js,它的程式碼可以寫成下面這樣:

// counter.js程式碼
export default function counter() {
  var dom = document.createElement('div');
  dom.setAttribute('id', 'counter');
  dom.innerHTML = 1;
  dom.onclick = function() {
    dom.innerHTML = parseInt(dom.innerHTML,10)+1;
  }
  document.body.appendChild(dom);
}
複製程式碼

number.js中的程式碼是下面這樣的:

// number.js程式碼
export default function number() {
  var dom = document.createElement('div');
  dom.setAttribute('id','number');
  dom.innerHTML = '1000';
  document.body.appendChild(dom);
}
複製程式碼

新增完以上兩個.js檔案後,我們再來對index.js檔案做一下小小的改動:

// index.js程式碼
import counter from './counter';
import number from './number';
counter();
number();
複製程式碼

在以上都改動完畢後,我們使用npm run dev進行打包,在頁面上點選數字1,讓它不斷的累計到你喜歡的一個數值(記住這個數值),這個時候我們再去修改number.js中的程式碼,將1000修改為3000,也就是下面這樣修改:

// number.js程式碼
export default function number() {
  var dom = document.createElement('div');
  dom.setAttribute('id','number');
  dom.innerHTML = '3000';
  document.body.appendChild(dom);
}
複製程式碼

我們發現,雖然1000成功變成了3000,但我們累計的數值卻重置到了1,這個時候你可能會問,我們不是配置了模組熱更新了嗎,為什麼不像CSS一樣,直接替換即可?

回答:這是因為CSS檔案,我們是使用了loader來進行處理,有些loader已經幫我們寫好了模組熱更新的程式碼,我們直接使用即可(類似的還有.vue檔案,vue-loader也幫我們處理好了模組熱更新)。而對於js程式碼,還需要我們寫一點點額外的程式碼,像下面這樣子:

import counter from './counter';
import number from './number';
counter();
number();

// 額外的模組HMR配置
if(module.hot) {
  module.hot.accept('./number.js', () => {
    document.body.removeChild(document.getElementById('number'));
    number();
  })
}
複製程式碼

寫完上面的額外程式碼後,我們再在瀏覽器中重複我們剛才的操作,即:

  • 累加數字1帶你喜歡的一個值
  • 修改number.js中的1000為你喜歡的一個值

以下截圖是我的測試結果,同時我們也可以在控制檯console上,看到模組熱更新第二次啟動時,已經成功幫我們把number.js中的程式碼輸出到了瀏覽器。

從今天開始,學習Webpack,減少對腳手架的依賴(上)

小結:在更改CSS樣式檔案時,我們不用書寫module.hot,這是因為各種CSSloader已經幫我們處理了,相同的道理還有.vue檔案的vue-loader,它也幫我們處理了模組熱更新,但在.js檔案中,我們還是需要根據實際的業務來書寫一點module.hot程式碼的。

處理ES6語法

我們在專案中書寫的ES6程式碼,由於考慮到低版本瀏覽器的相容性問題,需要把ES6程式碼轉換成低版本瀏覽器能夠識別的ES5程式碼。使用babel-loader@babel/core來進行ES6ES5之間的連結,使用@babel/preset-env來進行ES6ES5

在處理ES6程式碼之前,我們先來清理一下前面小節的中的程式碼,我們需要刪除counter.jsnumber.jsstyle.css這個三個檔案,刪除後的檔案目錄大概是下面這樣子的:

|-- dist
|   |-- index.html
|   |-- main.js
|-- src
|   |-- index.html
|   |-- index.js
|-- package.json
|-- webpack.config.js
複製程式碼

要處理ES6程式碼,需要我們安裝幾個npm包,可以使用如下的命令去安裝

// 安裝 babel-loader @babel/core
$ npm install babel-loader @babel/core --save-dev

// 安裝 @babel/preset-env
$ npm install @babel/preset-env --save-dev

// 安裝 @babel/polyfill進行ES5程式碼補丁
$ npm install @babel/polyfill --save-dev
複製程式碼

安裝完畢後,我們需要改寫src/index.js中的程式碼,可以是下面這個樣子:

import '@babel/polyfill';
const arr = [
  new Promise(() => {}),
  new Promise(() => {}),
  new Promise(() => {})
]

arr.map(item => {
  console.log(item);
})
複製程式碼

處理ES6程式碼,需要我們使用loader,所以需要在webpack.config.js中新增如下的程式碼:

module.exports = {
  // 其它配置
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  }
}
複製程式碼

@babel/preset-env需要在根目錄下有一個.babelrc檔案,所以我們新建一個.babelrc檔案,它的程式碼如下:

{
  "presets": ["@babel/preset-env"]
}
複製程式碼

為了讓我們的打包變得更加清晰,我們需要在webpack.config.js中把source-map配置成none,像下面這樣:

module.exports = {
  // 其他配置
  mode: 'development',
  devtool: 'none'
}
複製程式碼

本次打包,我們需要使用npx webpack,打包的結果如下圖所示:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

在以上的打包中,我們可以發現:

  • 箭頭函式被轉成了普通的函式形式
  • 如果你仔細觀察這次打包輸出的話,你會發現打包體積會非常大,有幾百K,這是因為我們將@babel/polyfill中的程式碼全部都打包進了我們的程式碼中

針對以上最後一個問題,我們希望,我們使用了哪些ES6程式碼,就引入它對應的polyfill包,達到一種按需引入的目的,要實現這樣一個效果,我們需要在.babelrc檔案中做一下小小的改動,像下面這樣:

{
  "presets": [["@babel/preset-env", {
    "corejs": 2,
    "useBuiltIns": "usage"
  }]]
}
複製程式碼

同時需要注意的時,我們使用了useBuiltIns:"usage"後,在index.js中就不用使用import '@babel/polyfill'這樣的寫法了,因為它已經幫我們自動這樣做了。

在以上配置完畢後,我們再次使用npx webpack進行打包,如下圖,可以看到此次打包後,main.js的大小明顯變小了。

從今天開始,學習Webpack,減少對腳手架的依賴(上)

Webpack進階

Tree Shaking

Tree Shaking是一個術語,通常用於描述移除專案中未使用的程式碼,Tree Shaking 只適用於ES Module語法(既通過export匯出,import引入),因為它依賴於ES Module的靜態結構特性。

在正式介紹Tree Shaking之前,我們需要現在src目錄下新建一個math.js檔案,它的程式碼如下:

export function add(a, b) {
  console.log(a + b);
}
export function minus(a, b) {
  console.log(a - b);
}
複製程式碼

接下來我們對index.js做一下處理,它的程式碼像下面這樣,從math.js中引用add方法並呼叫:

import { add } from './math'
add(1, 4);
複製程式碼

在上面的.js改動完畢後,我們最後需要對webpack.config.js做一下配置,讓它支援Tree Shaking,它的改動如下:

const path = require('path');
module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: {
    main: './src/index.js'
  },
  optimization: {
    usedExports: true
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname,'dist')
  }
}
複製程式碼

在以上webpack.config.js配置完畢後,我們需要使用npx webpack進行打包,它的打包結果如下:

// dist/main.js
"use strict";
/* harmony export (binding) */ 
__webpack_require__.d(__webpack_exports__, "a", function() { return add; });
/* unused harmony export minus */
function add(a, b) {
  console.log(a + b);
}
function minus(a, b) {
  console.log(a - b);
}
複製程式碼

打包結果分析:雖然我們配置了 Tree Shaking,但在開發環境下,我們依然能夠看到未使用過的minus方法,以上註釋也清晰了說明了這一點,這個時候你可能會問:為什麼我們配置了Tree Shakingminus方法也沒有被使用,但依然還是被打包進了main.js中?

其實這個原因很簡單,這是因為我們處於開發環境下打包,當我們處於開發環境下時,由於source-map等相關因素的影響,如果我們不把沒有使用的程式碼一起打包進來的話,source-map就不是很準確,這會影響我們本地開發的效率。

看完以上本地開發Tree Shaking的結果,我們也知道了本地開發Tree Shaking相對來說是不起作用的,那麼在生產環境下打包時,Tree Shaking的表現又如何呢?

在生產環境下打包,需要我們對webpack.config.js中的mode屬性,需要由development改為production,它的改動如下:

const path = require('path');
module.exports = {
  mode: 'production',
  devtool: 'source-map',
  entry: {
    main: './src/index.js'
  },
  optimization: {
    usedExports: true
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname,'dist')
  }
}
複製程式碼

配置完畢後,我們依然使用npx webpack進行打包,可以看到,它的打包結果如下所示:

// dist/main.js
([function(e,n,r){
  "use strict";
  var t,o;
  r.r(n),
  t=1,
  o=4,
  console.log(t+o)
}]);
複製程式碼

打包程式碼分析:以上程式碼是一段被壓縮過後的程式碼,我們可以看到,上面只有add方法,未使用的minus方法並沒有被打包進來,這說明在生產環境下我們的Tree Shaking才能真正起作用。

SideEffects

由於Tree Shaking作用於所有通過import引入的檔案,如果我們引入第三方庫,例如:import _ from 'lodash'或者.css檔案,例如import './style.css' 時,如果我們不 做限制的話,Tree Shaking將起副作用,SideEffects屬效能幫我們解決這個問題:它告訴webpack,我們可以對哪些檔案不做 Tree Shaking

// 修改package.json
// 如果不希望對任何檔案進行此配置,可以設定sideEffects屬性值為false
// *.css 表示 對所有css檔案不做 Tree Shaking
// @babael/polyfill 表示 對@babel/polyfill不做 Tree Shaking
"sideEffects": [
  "*.css",
  "@babel/polyfill"
],
複製程式碼

小結:對於Tree Shaking的爭議比較多,推薦看你的Tree Shaking並沒有什麼卵用,看完你會發現我們對Tree Shaking的瞭解真是太淺薄了。

區分開發模式和生產模式

像上一節那樣,如果我們要區分Tree Shaking的開發環境和生產環境,那麼我們每次打包的都要去更改webpack.config.js檔案,有沒有什麼辦法能讓我們少改一點程式碼呢? 答案是有的!

區分開發環境和生產環境,最好的辦法是把公用配置提取到一個配置檔案,生產環境和開發環境只寫自己需要的配置,在打包的時候再進行合併即可,webpack-merge 可以幫我們做到這個事情。

首先,我們效仿各大框架的腳手架的形式,把 Webpack 相關的配置都放在根目錄下的build資料夾下,所以我們需要新建一個build資料夾,隨後我們要在此資料夾下新建三個.js檔案和刪除webpack.config.js,它們分別是:

  • webpack.common.js:Webpack 公用配置檔案
  • webpack.dev.js:開發環境下的 Webpack 配置檔案
  • webpack.prod.js:生產環境下的 Webpack 配置檔案
  • webpack.config.js刪除根目錄下的此檔案

新建完webpack.common.js檔案後,我們需要把公用配置提取出來,它的程式碼看起來應該是下面這樣子的:

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader','css-loader']
      },
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: "babel-loader" 
      }
    ]
  },
  plugins: [
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new cleanWebpackPlugin()
  ],
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname,'dist')
  }
}
複製程式碼

提取完 Webpack 公用配置檔案後,我們開發環境下的配置,也就是webpack.dev.js中的程式碼,將剩下下面這些:

const webpack = require('webpack');
module.exports = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: 'dist',
    open: true,
    port: 3000,
    hot: true,
    hotOnly: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}
複製程式碼

而生產環境下的配置,也就是webpack.prod.js中的程式碼,可能是下面這樣子的:

module.exports = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
  optimization: {
    usedExports: true
  }
}
複製程式碼

在處理完以上三個.js檔案後,我們需要做一件事情:

  • 當處於開發環境下時,把webpack.common.js中的配置和webpack.dev.js中的配置合併在一起
  • 當處於開發環境下時,把webpack.common.js中的配置和webpack.prod.js中的配置合併在一起

針對以上問題,我們可以使用webpack-merge進行合併,在使用之前,我們需要使用如下命令進行安裝:

$ npm install webpack-merge -D
複製程式碼

安裝完畢後,我們需要對webpack.dev.jswebpack.prod.js做一下手腳,其中webpack.dev.js中的改動如下(程式碼高亮部分掘金無高亮):

const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const devConfig = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: 'dist',
    open: true,
    port: 3000,
    hot: true,
    hotOnly: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}
module.exports = merge(commonConfig, devConfig);
複製程式碼

相同的程式碼,webpack.prod.js中的改動部分如下:

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const prodConfig = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
  optimization: {
    usedExports: true
  }
}
module.exports = merge(commonConfig, prodConfig);
複製程式碼

聰明的你一定想到了,因為上面我們已經刪除了webpack.config.js檔案,所以我們需要重新在package.json中配置一下我們的打包命令,它們是這樣子寫的:

"scripts": {
  "dev": "webpack-dev-server --config ./build/webpack.dev.js",
  "build": "webpack --config ./build/webpack.prod.js"
},
複製程式碼

配置完打包命令,心急的你可能會馬上開始嘗試進行打包,你的打包目錄可能長成下面這個樣子:

|-- build
|   |-- dist
|   |   |-- index.html
|   |   |-- main.js
|   |   |-- main.js.map
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.prod.js
|-- src
|   |-- index.html
|   |-- index.js
|   |-- math.js
|-- .babelrc
|-- postcss.config.js
|-- package.json
複製程式碼

問題分析:當我們執行npm run build時,dist目錄打包到了build資料夾下了,這是因為我們把Webpack 相關的配置放到了build資料夾下後,並沒有做其他配置,Webpack 會認為build資料夾會是根目錄,要解決這個問題,需要我們在webpack.common.js中修改output屬性,具體改動的部分如下所示:

output: {
  filename: '[name].js',
  path: path.resolve(__dirname,'../dist')
}
複製程式碼

那麼解決完上面這個問題,趕緊使用你的打包命令測試一下吧,我的打包目錄是下面這樣子,如果你按上面的配置後,你的應該跟此目錄類似

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.prod.js
|-- dist
|   |-- index.html
|   |-- main.js
|   |-- main.js.map
|-- src
|   |-- index.html
|   |-- index.js
|   |-- math.js
|-- .babelrc
|-- postcss.config.js
|-- package.json
複製程式碼

程式碼分離(CodeSplitting)

Code Splitting 的核心是把很大的檔案,分離成更小的塊,讓瀏覽器進行並行載入。

常見的程式碼分割有三種形式:

  • 手動進行分割:例如專案如果用到lodash,則把lodash單獨打包成一個檔案。
  • 同步匯入的程式碼:使用 Webpack 配置進行程式碼分割。
  • 非同步匯入的程式碼:通過模組中的行內函數呼叫來分割程式碼。

手動進行分割

手動進行分割的意思是在entry上配置多個入口,例如像下面這樣:

module.exports = {
  entry: {
    main: './src/index.js',
    lodash: 'lodash'
  }
}
複製程式碼

這樣配置後,我們使用npm run build打包命令,它的打包輸出結果為:

        Asset       Size  Chunks             Chunk Names
  index.html  462 bytes          [emitted]
    lodash.js   1.46 KiB       1  [emitted]  lodash
lodash.js.map   5.31 KiB       1  [emitted]  lodash
      main.js   1.56 KiB       2  [emitted]  main
  main.js.map   5.31 KiB       2  [emitted]  main
複製程式碼

它輸出了兩個模組,也能在一定程度上進行程式碼分割,不過這種分割是十分脆弱的,如果兩個模組共同引用了第三個模組,那麼第三個模組會被同時打包進這兩個入口檔案中,而不是分離出來。

所以我們常見的做法是關心最後兩種程式碼分割方法,無論是同步程式碼還是非同步程式碼,都需要在webpack.common.js中配置splitChunks屬性,像下面這樣子:

module.exports = {
  // 其它配置
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}
複製程式碼

你可能已經看到了其中有一個chunks屬性,它告訴 Webpack 應該對哪些模式進行打包,它的引數有三種:

  • async:此值為預設值,只有非同步匯入的程式碼才會進行程式碼分割。
  • initial:與async相對,只有同步引入的程式碼才會進行程式碼分割。
  • all:表示無論是同步程式碼還是非同步程式碼都會進行程式碼分割。

同步程式碼分割

在完成上面的配置後,讓我們來安裝一個相對大一點的包,例如:lodash,然後對index.js中的程式碼做一些手腳,像下面這樣:

import _ from 'lodash'
console.log(_.join(['Dell','Lee'], ' '));
複製程式碼

就像上面提到的那樣,同步程式碼分割,我們只需要在webpack.common.js配置chunks屬性值為initial即可:

module.exports = {
  // 其它配置
  optimization: {
    splitChunks: {
      chunks: 'initial'
    }
  }
}
複製程式碼

webpack.common.js配置完畢後,我們使用npm run build來進行打包, 你的打包dist目錄看起來應該像下面這樣子:

|-- dist
|   |-- index.html
|   |-- main.js
|   |-- main.js.map
|   |-- vendors~main.js
|   |-- vendors~main.js.map
複製程式碼

打包分析main.js使我們的業務程式碼,vendors~main.js是第三方模組的程式碼,在此案例中也就是lodash中的程式碼。

非同步程式碼分割

由於chunks屬性的預設值為async,如果我們只需要針對非同步程式碼進行程式碼分割的話,我們只需要進行非同步匯入,Webpack會自動幫我們進行程式碼分割,非同步程式碼分割它的配置如下:

module.exports = {
  // 其它配置
  optimization: {
    splitChunks: {
      chunks: 'async'
    }
  }
}
複製程式碼

注意:由於非同步匯入語法目前並沒有得到全面支援,需要通過 npm 安裝 @babel/plugin-syntax-dynamic-import 外掛來進行轉譯

$ npm install @babel/plugin-syntax-dynamic-import -D
複製程式碼

安裝完畢後,我們需要在根目錄下的.babelrc檔案做一下改動,像下面這樣子:

{
  "presets": [["@babel/preset-env", {
    "corejs": 2,
    "useBuiltIns": "usage"
  }]],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
複製程式碼

配置完畢後,我們需要對index.js做一下程式碼改動,讓它使用非同步匯入程式碼塊:

// 點選頁面,非同步匯入lodash模組
document.addEventListener('click', () => {
  getComponent().then((element) => {
    document.getElementById('root').appendChild(element)
  })
})

function getComponent () {
  return import(/* webpackChunkName: 'lodash' */'lodash').then(({ default: _ }) => {
    var element = document.createElement('div');
    element.innerHTML = _.join(['Dell', 'lee'], ' ')
    return element;
  })
}
複製程式碼

上面import裡面的註釋內容是plugin-syntax-dynamic-import外掛支援的註釋內容,俗稱為"魔法註釋",它的含義是告訴 Webpack 我們的非同步模組的名字叫lodash,在後續preloading和prefetch也使用了相同的"魔法註釋"方法。

寫好以上程式碼後,我們同樣使用npm run build進行打包,dist打包目錄的輸出結果如下:

|-- dist
|   |-- 1.js
|   |-- 1.js.map
|   |-- index.html
|   |-- main.js
|   |-- main.js.map
複製程式碼

我們在瀏覽器中執行dist目錄下的index.html,切換到network皮膚時,我們可以發現只載入了main.js,如下圖:

從今天開始,學習Webpack,減少對腳手架的依賴(上)


當我們點選頁面時,才 真正開始載入 第三方模組,如下圖(1.js):

從今天開始,學習Webpack,減少對腳手架的依賴(上)

SplitChunksPlugin配置引數詳解

在上一節中,我們配置了splitChunks屬性,它能讓我們進行程式碼分割,其實這是因為 Webpack 底層使用了 splitChunksPlugin 外掛。這個外掛有很多可以配置的屬性,它也有一些預設的配置引數,它的預設配置引數如下所示,我們將在下面為一些常用的配置項做一些說明。

module.exports = {
  // 其它配置項
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};
複製程式碼

chunks引數

此引數的含義在上一節中已詳細說明,同時也配置了相應的案例,就不再次累述

minSize 和 maxSize

minSize預設值是30000,也就是30kb,當程式碼超過30kb時,才開始進行程式碼分割,小於30kb的則不會進行程式碼分割;與minSize相對的,maxSize預設值為0,為0表示不限制打包後檔案的大小,一般這個屬性不推薦設定,一定要設定的話,它的意思是:打包後的檔案最大不能超過設定的值,超過的話就會進行程式碼分割。

為了測試以上兩個屬性,我們來寫一個小小的例子,在src目錄下新建一個math.js檔案,它的程式碼如下:

export function add(a, b) {
  return a + b;
}
複製程式碼

新建完畢後,在index.js中引入math.js:

import { add } from './math.js'
console.log(add(1, 2));
複製程式碼

打包分析:因為我們寫的math.js檔案的大小非常小,如果應用預設值,它是不會進行程式碼分割的,如果你要進一步測試minSizemaxSize,請自行修改後打包測試。

minChunks

預設值為1,表示某個模組複用的次數大於或等於一次,就進行程式碼分割。

如果將其設定大於1,例如:minChunks:2,在不考慮其他模組的情況下,以下程式碼不會進行程式碼分割:

// 配置了minChunks: 2,以下lodash不會進行程式碼分割,因為只使用了一次 
import _ from 'lodash';
console.log(_.join(['Dell', 'Lee'], '-'));
複製程式碼

maxAsyncRequests 和 maxInitialRequests

  • maxAsyncRequests:它的預設值是5,代表在進行非同步程式碼分割時,前五個會進行程式碼分割,超過五個的不再進行程式碼分割。
  • maxInitialRequests:它的預設值是3,代表在進行同步程式碼分割時,前三個會進行程式碼分割,超過三個的不再進行程式碼分割。

automaticNameDelimiter

這是一個連線符,左邊是程式碼分割的快取組,右邊是打包的入口檔案的項,例如vendors~main.js

cacheGroups

在進行程式碼分割時,會把符合條件的放在一組,然後把一組中的所有檔案打包在一起,預設配置項中有兩個分組,一個是vendors和default

vendors組: 以下程式碼的含義是,將所有通過引用node_modules資料夾下的都放在vendors組中

vendors: {
  test: /[\\/]node_modules[\\/]/,
  priority: -10
}
複製程式碼

default組: 預設組,意思是,不符合vendors的分組都將分配在default組中,如果一個檔案即滿足vendors分組,又滿足default分組,那麼通過priority的值進行取捨,值最大優先順序越高。

default: {
  minChunks: 2,
  priority: -20,
  reuseExistingChunk: true
}
複製程式碼

reuseExistingChunk: 中文解釋是複用已存在的檔案。意思是,如果有一個a.js檔案,它裡面引用了b.js,但我們其他模組又有引用b.js的地方。開啟這個配置項後,在打包時會分析b.js已經打包過了,直接可以複用不用再次打包。

// a.js
import b from 'b.js';
console.log('a.js');

// c.js
import b from 'b.js';
console.log('c.js');
複製程式碼

Lazy Loading懶載入

Lazy Loading懶載入的理解是:通過非同步引入程式碼,它說的非同步,並不是在頁面一開始就載入,而是在合適的時機進行載入。

Lazy Loading懶載入的實際案例我們已經在上一小節書寫了一個例子,不過我們依然可以做一下小小的改動,讓它使用async/await進行非同步載入,它的程式碼如下:

// 頁面點選的時候才載入lodash模組
document.addEventListener('click', () => {
  getComponet().then(element => {
    document.body.appendChild(element);
  })
})
async function getComponet() {
  const { default: _ }  = await import(/* webpackChunkName: 'lodash' */ 'lodash');
  var element = document.createElement('div');
  element.innerHTML = _.join(['1', '2', '3'], '**')
  return element;
}
複製程式碼

以上懶載入的結果與上一小節的結果類似,就不在此展示,你可以在你本地的專案中打包後自行測試和檢視。

PreLoading 和Prefetching

在以上Lazy Loading的例子中,只有當我們在頁面點選時才會載入lodash,也有一些模組雖然是非同步匯入的,但我們希望能提前進行載入,PreLoadingPrefetching可以幫助我們實現這一點,它們的用法類似,但它們還是有區別的:Prefetching不會跟隨主程式一些下載,而是等到主程式載入完畢,頻寬釋放後才進行載入,PreLoading會隨主程式一起載入。

實現PreLoading或者Prefetching非常簡單,我們只需要在上一節的例子中加一點點程式碼即可:

// 頁面點選的時候才載入lodash模組
document.addEventListener('click', () => {
  getComponet().then(element => {
    document.body.appendChild(element);
  })
})
async function getComponet() {
  const { default: _ }  = await import(/* webpackPrefetch: true */ 'lodash');
  var element = document.createElement('div');
  element.innerHTML = _.join(['1', '2', '3'], '**')
  return element;
}
複製程式碼

改寫完畢後,我們使用npm run dev或者npm run build進行打包,在瀏覽器中點選頁面,我們將在network皮膚看到如下圖所示:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

相信聰明的你一定看到了0.js,它是from disk cache,那為什麼?原因在於,Prefetching的程式碼它會在head頭部,新增像這樣的一段內容:

<link rel="prefetch" as="script" href="0.js">
複製程式碼

這樣一段內容追加到head頭部後,指示瀏覽器在空閒時間裡去載入0.js,這正是Prefetching它所能幫我們做到的事情,而PreLoading的用法於此類似,請自行測試。

CSS程式碼分割

當我們在使用style-loadercss-loader打包.css檔案時會直接把CSS檔案打包進.js檔案中,然後直接把樣式通過<style></style>的方式寫在頁面,如果我們要把CSS單獨打包在一起,然後通過link標籤引入,那麼可以使用mini-css-extract-plugin外掛進行打包。

截止到寫此文件時,此外掛還未支援HMR,意味著我們要使用這個外掛進行打包CSS時,為了開發效率,我們需要配置在生產環境下,開發環境依然還是使用style-loader進行打包
此外掛的最新版已支援HMR

在配置之前,我們需要使用npm install進行安裝此外掛:

$ npm install mini-css-extract-plugin -D
複製程式碼

安裝完畢後,由於此外掛已支援HMR,那我們可以把配置寫在webpack.common.js中(以下配置為完整配置,改動參考高亮程式碼塊掘金無高亮):

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { 
            loader: miniCssExtractPlugin.loader,
            options: {
              hmr: true,
              reloadAll: true
            }
          },
          'css-loader'
        ]
      },
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: "babel-loader" 
      }
    ]
  },
  plugins: [
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new cleanWebpackPlugin(),
    new miniCssExtractPlugin({
      filename: '[name].css'
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname,'../dist')
  }
}
複製程式碼

配置完畢以後,我們來在src目錄下新建一個style.css檔案,它的程式碼如下:

body {
  color: green;
}
複製程式碼

接下來,我們改動一下index.js檔案,讓它引入style.css,它的程式碼可以這樣寫:

import './style.css';
var root = document.getElementById('root');
root.innerHTML = 'Hello,world'
複製程式碼

使用npm run build進行打包,dist打包目錄如下所示:

|-- dist
|   |-- index.html
|   |-- main.css
|   |-- main.css.map
|   |-- main.js
|   |-- main.js.map
複製程式碼

如果發現並沒有打包生成main.css檔案,可能是Tree Shaking的副作用,應該在package.json中新增屬性sideEffects:['*.css']

CSS壓縮

CSS壓縮的理解是:當我們有兩個相同的樣式分開寫的時候,我們可以把它們合併在一起;為了減`CSS檔案的體積,我們需要像壓縮JS檔案一樣,壓縮一下CSS檔案。

我們再在src目錄下新建style1.css檔案,內容如下:

body{
  line-height: 100px;
}
複製程式碼

index.js檔案中引入此CSS檔案

import './style.css';
import './style1.css';
var root = document.getElementById('root');
root.innerHTML = 'Hello,world'
複製程式碼

使用打包npm run build打包命令,我們發現雖然外掛幫我們把CSS打包在了一個檔案,但並沒有合併壓縮。

body {
  color: green;
}
body{
  line-height: 100px;
}
複製程式碼

要實現CSS的壓縮,我們需要再安裝一個外掛:

$ npm install optimize-css-assets-webpack-plugin -D
複製程式碼

安裝完畢後我們需要再一次改寫webpack.common.js的配置,如下:

const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  // 其它配置
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
    minimizer: [
      new optimizaCssAssetsWebpackPlugin()
    ]
  }
}
複製程式碼

配置完畢以後,我們再次使用npm run build進行打包,打包結果如下所示,可以看見,兩個CSS檔案的程式碼已經壓縮合並了。

body{color:red;line-height:100px}
複製程式碼

Webpack和瀏覽器快取(Caching)

在講這一小節之前,讓我們清理下專案目錄,改寫下我們的index.js,刪除掉一些沒用的檔案:

import _ from 'lodash';

var dom = document.createElement('div');
dom.innerHTML = _.join(['Dell', 'Lee'], '---');
document.body.append(dom);
複製程式碼

清理後的專案目錄可能是這樣的:

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.prod.js
|-- src
    |-- index.html
    |-- index.js
|-- postcss.config.js
|-- package.json
複製程式碼

我們使用npm run build打包命令,打包我們的程式碼,可能會生成如下的檔案:

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.prod.js
|-- dist
|   |-- index.html
|   |-- main.js
|   |-- main.js.map
|   |-- vendors~main.js
|   |-- vendors~main.js.map
|-- src
    |-- index.html
    |-- index.js
|-- package.json
|-- postcss.config.js
複製程式碼

我們可以看到,打包生成的dist目錄下,檔名是main.jsvendors~main.js,如果我們把dist目錄放在伺服器部署的話,當使用者第一次訪問頁面時,瀏覽器會自動把這兩個.js檔案快取起來,下一次非強制性重新整理頁面時,會直接使用快取起來的檔案。

假如,我們在使用者第一次重新整理頁面和第二次重新整理頁面之間,我們修改了我們的程式碼,並再一次部署,這個時候由於瀏覽器快取了這兩個.js檔案,所以使用者介面無法獲取最新的程式碼。

那麼,我們有辦法能解決這個問題呢,答案是[contenthash]佔位符,它能根據檔案的內容,在每一次打包時生成一個唯一的hash值,只要我們檔案發生了變動,就重新生成一個hash值,沒有改動的話,[contenthash]則不會發生變動,可以在output中進行配置,如下所示:

// 開發環境下的output配置還是原來的那樣,也就是webpack.common.js中的output配置
// 因為開發環境下,我們不用考慮快取問題
// webpack.prod.js中新增output配置
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js'
}
複製程式碼

使用npm run build進行打包,dist打包目錄的結果如下所示,可以看到每一個.js檔案都有一個唯一的hash值,這樣配置後就能有效解決瀏覽器快取的問題。

|-- dist
|   |-- index.html
|   |-- main.8bef05e11ca1dc804836.js
|   |-- main.8bef05e11ca1dc804836.js.map
|   |-- vendors~main.4b711ce6ccdc861de436.js
|   |-- vendors~main.4b711ce6ccdc861de436.js.map
複製程式碼

Shimming

有時候我們在引入第三方庫的時候,不得不處理一些全域性變數的問題,例如jQuery的$,lodash的_,但由於一些老的第三方庫不能直接修改它的程式碼,這時我們能不能定義一個全域性變數,當檔案中存在$或者_的時候自動的幫他們引入對應的包。

這個問題,可以使用ProvidePlugin外掛來解決,這個外掛已經被 Webpack 內建,無需安裝,直接使用即可。

src目錄下新建jquery.ui.js檔案,程式碼如下所示,它使用了jQuery$符號,建立這個檔案目的是為了來模仿第三方庫。

export function UI() {
  $('body').css('background','green');
}
複製程式碼

建立完畢後,我們修改一下index.js檔案, 讓它使用剛才我們建立的檔案:

import _ from 'lodash';
import $ from 'jquery';
import { UI } from './jquery.ui';

UI();

var dom = $(`<div>${_.join(['Dell', 'Lee'], '---')}</div>`);
$('#root').append(dom);
複製程式碼

接下來我們使用npm run dev進行打包,它的結果如下:

從今天開始,學習Webpack,減少對腳手架的依賴(上)

問題: 我們發現,根本執行不起來,報錯$ is not defined
解答: 這是因為雖然我們在index.js中引入的jquery檔案,但$符號只能在index.js才有效,在jquery.ui.js無效,報錯是因為jquery.ui.js$符號找不到引起的。

以上場景完美再現了我們最開始提到的問題,那麼我們接下來就通過配置解決,首先在webpack.common.js檔案中使用ProvidePlugin外掛:

配置$:'jquery',只要我們檔案中使用了$符號,它就會自動幫我們引入jquery,相當於import $ from 'jquery'

const webpack = require('webpack');
module.exports = {
  // 其它配置
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      _: 'lodash'
    })
  ]
}
複製程式碼

打包結果: 使用npm run dev進行打包,打包結果如下,可以發現,專案已經可以正確執行了。

從今天開始,學習Webpack,減少對腳手架的依賴(上)

處理全域性this指向問題

我們現在來思考一個問題,一個模組中的this到底指向什麼,是模組自身還是全域性的window物件

// index.js程式碼,在瀏覽器中輸出:false
console.log(this===window);
複製程式碼

如上所示,如果我們使用npm run dev執行專案,執行index.html時,會在瀏覽器的console皮膚輸出false,證明在模組中this指向模組自身,而不是全域性的window物件,那麼我們有什麼辦法來解決這個問題呢?可以安裝使用imports-loader來解決這個問題!

$ npm install imports-loader -D
複製程式碼

安裝完畢後,我們在webpack.common.js加一點配置,在.js的loader處理中,新增imports-loader

module.exports = {
  // ... 其它配置
  module: {
    rules: [
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        use: [
          {
            loader: 'babel-loader'
          },
          {
            loader: 'imports-loader?this=>window'
          }
        ]
      }
    ]
  }
}
複製程式碼

配置完畢後使用npm run dev來進行打包,檢視console控制檯輸出true,證明this這個時候已經指向了全域性window物件,問題解決。

從今天開始,學習Webpack,減少對腳手架的依賴(上)

本篇部落格由慕課網視訊從基礎到實戰手把手帶你掌握新版Webpack4.0閱讀整理而來,觀看視訊請支援正版。

相關文章