問:這篇文章適合哪些人?
答:適合沒接觸過Webpack或者瞭解不全面的人。
javascript
問:這篇文章的目錄怎麼安排的?
答:先介紹背景,由背景引入Webpack的概念,進一步介紹Webpack基礎、核心和一些經常使用配置案例、優化手段,Webpack的plugin和loader確實很是多,短短2w多字還只是覆蓋其中一小部分。
css
問:這篇文章的出處?
答:此篇文章知識來自付費視頻(連接在文章末尾),文章由本身獨立撰寫,已得到講師受權並首發於掘金。html
下一篇:從今天開始,學習Webpack,減小對腳手架的依賴(下)前端
若是你以爲寫的不錯,請給我點一個star,原博客地址:原文地址vue
注意,本篇博客 Webpack 版本是4.0+,請確保你安裝了Node.js最新版本。 java
Webpack 的核心概念是一個 模塊打包工具,它的主要目標是將js
文件打包在一塊兒,打包後的文件用於在瀏覽器中使用,但它也能勝任 轉換(transform
) 、打包(bundle
) 或 包裹(package
) 任何其餘資源。node
在學習 Webpack 以前,咱們有必要來了解一下前端領域的開發歷程,只有明白了這些開發歷程,才能更加清楚 Webpack 是怎麼應運而生的,又能給咱們解決什麼樣的問題。jquery
特徵: 一鍋亂燉
在早期 js
能力還很是有限的時候,咱們經過面向過程的方式把代碼寫在同一個.js
文件中,一個面向過程的開發模式可能以下所示:webpack
<!-- 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
文件中,那麼代碼將變得很是難以理解和維護,此時面向對象開發模式便出現了,一個面向對象開發模式可能以下所示:git
在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 就粉墨登場了。
不建議跟隨此小結一塊兒安裝,這次示例僅僅做爲一個例子,詳細學習步驟請直接閱讀下一章節
-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"
}
複製代碼
-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>
複製代碼
參數說明
$ 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');
複製代碼
運行以下命令安裝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
文件夾下改寫說明:
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
,它的運行結果以下圖所示
在上一節中,咱們第一次運行了一個打包命令,它在控制檯上有一些輸出內容,這一節咱們詳細來介紹這些輸出是什麼意思
hash
表明本次打包的惟一hash
值,每一次打包此值都是不同的webpack
的版本號.js
文件對應的id
,id
從0
開始,依次日後+1
.js
文件的名字,至於爲什麼是main
,而不是其餘的內容,這是由於在咱們的webpack.config.js
中,entry:'./index.js'
是對以下方式的簡寫形式:// path爲Node的核心模塊
const path = require('path');
module.exports = {
// entry: './index.js',
entry: {
main: './index.js'
}
// 其它配置
}
複製代碼
main
webpack.config.js
設置mode
屬性,mode
屬性有三個值:development
表明開發環境、production
表明生產環境、none
表明既不是開發環境也不是生產環境。若是不寫的話,默認是生產環境,可在配置文件中配置此項,配置後再次打包將不會再出現此警告。// path爲Node的核心模塊
const path = require('path');
module.exports = {
// 其它配置
mode: 'development'
}
複製代碼
loader是一種打包規則,它告訴了 Webpack 在遇到非js文件時,應該如何處理這些文件
loader
有以下幾種固定的運用規則:
test
正則來匹配相應的文件use
來添加文件對應的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'
}
}
]
}
}
複製代碼
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
文件,使用style-loader
和css-loader
來處理.less
文件,使用less-loader
來處理.sass或者.scss
文件,須要使用sass-loader
來處理.styl
文件,須要使用stylus-loader
來處理首先安裝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-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
文件中寫一些須要處理兼容性的樣式的時候,須要咱們分別對於不一樣的瀏覽器書添加不一樣的廠商前綴,使用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的模塊化打包的理解是:除非我主動引用你的樣式,不然你打包的樣式不能影響到我。
根目錄下添加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
模塊化打包的結果:
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
文件,而且它會 自動 幫咱們引入咱們打包後的.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
它能幫咱們在打包以前 自動刪除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的理解:它是一種映射關係,它映射了打包後的代碼和源代碼之間的對應關係,通常經過devtool來配置。
如下是官方提供的devtool
各個屬性的解釋以及打包速度對比圖:
經過上圖咱們能夠看出,良好的source-map
配置不只能幫助咱們提升打包速度,同時在代碼維護和調錯方面也能有很大的幫助,通常來講,source-map
的最佳實踐是下面這樣的:
development
):推薦將devtool
設置成cheap-module-eval-source-map
production
):推薦將devtool
設置成cheap-module-source-map
webpack-dev-server的理解:它能幫助咱們在源代碼更改的狀況下,自動*幫咱們打包咱們的代碼並啓動一個小型的服務器。若是與熱更新一塊兒使用,它能幫助咱們高效的開發。
自動打包的方案,一般來講有以下幾種:
watch
參數自動打包:它是在打包命令後面跟了一個--watch
參數,它雖然能幫咱們自動打包,但咱們任然須要手動刷新瀏覽器,同時它不能幫咱們在本地啓動一個小型服務器,一些http
請求不能經過。webpack-dev-server
插件打包(推薦):它是咱們推薦的一種自動打包方案,在開發環境下使用尤爲能幫咱們高效的開發,它能解決watch
參數打包中的問題,若是咱們與熱更新(HMR
)一塊兒使用,咱們將擁有很是良好的開發體驗。webpack-dev-middleware
自編碼啓動小型服務器(不講述)使用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
,咱們須要使用以下命令進行安裝
$ 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)已內置到 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
文件,根據這兩部分,咱們將分別來進行介紹。
首先咱們在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
依然存在,只是顏色變了。
在介紹完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
代碼轉換成低版本瀏覽器可以識別的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
,打包的結果以下圖所示:
在以上的打包中,咱們能夠發現:
@babel/polyfill
中的代碼所有都打包進了咱們的代碼中針對以上最後一個問題,咱們但願,咱們使用了哪些ES6
代碼,就引入它對應的polyfill
包,達到一種按需引入的目的,要實現這樣一個效果,咱們須要在.babelrc
文件中作一下小小的改動,像下面這樣:
{
"presets": [["@babel/preset-env", {
"corejs": 2,
"useBuiltIns": "usage"
}]]
}
複製代碼
同時須要注意的時,咱們使用了useBuiltIns:"usage"
後,在index.js
中就不用使用import '@babel/polyfill'
這樣的寫法了,由於它已經幫咱們自動這樣作了。
在以上配置完畢後,咱們再次使用npx webpack
進行打包,以下圖,能夠看到這次打包後,main.js
的大小明顯變小了。
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
才能真正起做用。
因爲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
複製代碼
Code Splitting 的核心是把很大的文件,分離成更小的塊,讓瀏覽器進行並行加載。
常見的代碼分割有三種形式:
lodash
,則把lodash
單獨打包成一個文件。手動進行分割的意思是在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
):
在上一節中,咱們配置了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
}
}
}
}
};
複製代碼
此參數的含義在上一節中已詳細說明,同時也配置了相應的案例,就再也不次累述。
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
,請自行修改後打包測試。
默認值爲1,表示某個模塊複用的次數大於或等於一次,就進行代碼分割。
若是將其設置大於1,例如:minChunks:2
,在不考慮其餘模塊的狀況下,如下代碼不會進行代碼分割:
// 配置了minChunks: 2,如下lodash不會進行代碼分割,由於只使用了一次
import _ from 'lodash';
console.log(_.join(['Dell', 'Lee'], '-'));
複製代碼
maxAsyncRequests
:它的默認值是5,表明在進行異步代碼分割時,前五個會進行代碼分割,超過五個的再也不進行代碼分割。maxInitialRequests
:它的默認值是3,表明在進行同步代碼分割時,前三個會進行代碼分割,超過三個的再也不進行代碼分割。這是一個鏈接符,左邊是代碼分割的緩存組,右邊是打包的入口文件的項,例如vendors~main.js
在進行代碼分割時,會把符合條件的放在一組,而後把一組中的全部文件打包在一塊兒,默認配置項中有兩個分組,一個是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
懶加載的實際案例咱們已經在上一小節書寫了一個例子,不過咱們依然能夠作一下小小的改動,讓它使用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;
}
複製代碼
以上懶加載的結果與上一小節的結果相似,就不在此展現,你能夠在你本地的項目中打包後自行測試和查看。
在以上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
的用法於此相似,請自行測試。
當咱們在使用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文件的體積,咱們須要像壓縮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}
複製代碼
在講這一小節以前,讓咱們清理下項目目錄,改寫下咱們的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
複製代碼
有時候咱們在引入第三方庫的時候,不得不處理一些全局變量的問題,例如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
到底指向什麼,是模塊自身仍是全局的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閱讀整理而來,觀看視頻請支持正版。