問:這篇文章適合哪些人?
答:適合沒接觸過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();
複製程式碼
不足: 以上的程式碼示例中,雖然使用物件導向開發模式解決了程式導向開發模式中的一些問題,但似乎又引入了一些新的問題。
- 每一個模組都需要引入一個
.js
檔案,隨著模組的增多,這會影響頁面效能 - 在
index.js
檔案中,並不能直接看出模組的邏輯關係,必須去頁面才能找到 - 在
index.html
頁面中,檔案的引入順序必須嚴格按順序來引入,例如:index.js
必須放在最後引入,如果把header.js
檔案放在index.js
檔案後引入,那麼程式碼會報錯
現代開發模式
特徵: 模組化載入方案讓前端開發進一步工程化
根據物件導向開發模式中的一系列問題,隨後各種模組化載入的方案如雨後春筍,例如:ES Module
、AMD
、CMD
以及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打包
引數說明
- npx webpack代表在本專案下尋找 Webpack 打包命令,它區別npm命令
- index.js引數代表本次打包的入口是index.js
$ npx webpack index.js
複製程式碼
打包結果:
正如上面你所看到的那樣,網頁正確顯示了我們期待的結果,這也是 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')
}
}
複製程式碼
配置引數說明:
entry
配置項說明了webpack
打包的入口。output
配置項說明了webpack
輸出配置,其中filename
配置了打包後的檔案叫main.js
path
配置了打包後的輸出目錄為dist
資料夾下
改寫package.json檔案
改寫說明:
- 新增
private
屬性並設定為true
,此屬效能讓我們的專案為私有的,防止意外發布程式碼 - 移除
main
屬性,我們的專案並不需要對外暴露一個入口檔案 - 新增
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
複製程式碼
打包後的效果如下所示:
打包後的專案目錄如下所示,可以看到我們多出了一個叫dist
的目錄,它裡面有一個main.js
檔案
|-- dist
| |-- main.js
|-- index.html
|-- index.js
|-- webpack.config.js
|-- package.json
複製程式碼
打包成功後,我們需要在瀏覽器中執行index.html
,它的執行結果如下圖所示
理解webpack打包輸出
在上一節中,我們第一次執行了一個打包命令,它在控制檯上有一些輸出內容,這一節我們詳細來介紹這些輸出是什麼意思
- Hash:
hash
代表本次打包的唯一hash
值,每一次打包此值都是不一樣的 - Version: 詳細展示了我們使用
webpack
的版本號 - Time: 代表我們本次打包的耗時
- Asset: 代表我們打包出的檔名稱
- Size: 代表我們打包出的檔案的大小
- Chunks: 代表打包後的
.js
檔案對應的id
,id
從0
開始,依次往後+1
- 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'
}
// 其它配置
}
複製程式碼
- Entrypoint main = bundle.js: 代表我們打包的入口為
main
- 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
複製程式碼
打包結果
運用佔位符
在以上打包圖片的過程中,我們發現打包生成的圖片好像名字是一串亂碼,如果我們要原樣輸出原圖片的名字的話,又該如何進行配置呢?這個問題,可以使用 佔位符 進行解決。
檔案佔位符它有一些固定的規則,像下面這樣:
[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
來處理:
- 普通
.css
檔案,使用style-loader
和css-loader
來處理 .less
檔案,使用less-loader
來處理.sass或者.scss
檔案,需要使用sass-loader
來處理.styl
檔案,需要使用stylus-loader
來處理
打包css檔案
首先安裝style-loader
和css-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);
複製程式碼
打包結果
打包Sass檔案
需要安裝sass-loader
和node-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);
複製程式碼
根據上面的配置和程式碼改寫後,再次打包,打包的結果會是下面這個樣子
自動新增CSS廠商字首
當我們在css
檔案中寫一些需要處理相容性的樣式的時候,需要我們分別對於不同的瀏覽器書新增不同的廠商字首,使用postcss-loader
可以幫我們在webpack
打包的時候自動新增這些廠商字首。
自動新增廠商字首需要npm install
安裝postcss-loader
和autoprefixer
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
,它的結果如下圖所示
模組化打包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);
複製程式碼
打包執行
我們可以看到,在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核心
使用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
各個屬性的解釋以及打包速度對比圖:
通過上圖我們可以看出,良好的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
進行打包,我們點選按鈕後,它會出現如下的情況
理解: 由於item
是動態生成的,當我們要將yellow
顏色改變成red
時,模組熱更新能幫我們在不重新整理瀏覽器的情況下,替換掉樣式的內容。直白來說:自動生成的item
依然存在,只是顏色變了。
在js中的模組熱更新
在介紹完CSS
中的模組熱更新後,我們接下來介紹在js
中的模組熱更新。
首先,我們在src
目錄下建立兩個.js
檔案,分別叫counter.js
和number.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
中的程式碼輸出到了瀏覽器。
小結:在更改CSS
樣式檔案時,我們不用書寫module.hot
,這是因為各種CSS
的loader
已經幫我們處理了,相同的道理還有.vue
檔案的vue-loader
,它也幫我們處理了模組熱更新,但在.js
檔案中,我們還是需要根據實際的業務來書寫一點module.hot
程式碼的。
處理ES6語法
我們在專案中書寫的ES6
程式碼,由於考慮到低版本瀏覽器的相容性問題,需要把ES6
程式碼轉換成低版本瀏覽器能夠識別的ES5
程式碼。使用babel-loader
和@babel/core
來進行ES6
和ES5
之間的連結,使用@babel/preset-env
來進行ES6
轉ES5
在處理ES6
程式碼之前,我們先來清理一下前面小節的中的程式碼,我們需要刪除counter.js
、number.js
和style.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
,打包的結果如下圖所示:
在以上的打包中,我們可以發現:
- 箭頭函式被轉成了普通的函式形式
- 如果你仔細觀察這次打包輸出的話,你會發現打包體積會非常大,有幾百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進階
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 Shaking
,minus
方法也沒有被使用,但依然還是被打包進了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.js
和webpack.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
,如下圖:
當我們點選頁面時,才 真正開始載入 第三方模組,如下圖(1.js
):
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
檔案的大小非常小,如果應用預設值,它是不會進行程式碼分割的,如果你要進一步測試minSize
和maxSize
,請自行修改後打包測試。
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
,也有一些模組雖然是非同步匯入的,但我們希望能提前進行載入,PreLoading
和Prefetching
可以幫助我們實現這一點,它們的用法類似,但它們還是有區別的: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
皮膚看到如下圖所示:
相信聰明的你一定看到了0.js
,它是from disk cache
,那為什麼?原因在於,Prefetching
的程式碼它會在head
頭部,新增像這樣的一段內容:
<link rel="prefetch" as="script" href="0.js">
複製程式碼
這樣一段內容追加到head
頭部後,指示瀏覽器在空閒時間裡去載入0.js
,這正是Prefetching
它所能幫我們做到的事情,而PreLoading
的用法於此類似,請自行測試。
CSS程式碼分割
當我們在使用style-loader
和css-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.js
和vendors~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
進行打包,它的結果如下:
問題: 我們發現,根本執行不起來,報錯$ 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
進行打包,打包結果如下,可以發現,專案已經可以正確執行了。
處理全域性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
物件,問題解決。
本篇部落格由慕課網視訊從基礎到實戰手把手帶你掌握新版Webpack4.0閱讀整理而來,觀看視訊請支援正版。