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

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

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

問:這篇文章的出處?
答:此篇文章知識來自付費視頻(連接在文章末尾),文章由本身獨立撰寫,已得到講師受權並首發於掘金。html

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

若是你以爲寫的不錯,請給我點一個star,原博客地址:原文地址vue

Webpack

注意,本篇博客 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();
複製代碼

不足: 以上的代碼示例中,雖然使用面向對象開發模式解決了面向過程開發模式中的一些問題,但彷佛又引入了一些新的問題。

  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 的 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
複製代碼

打包後的效果以下所示:

打包後的項目目錄以下所示,能夠看到咱們多出了一個叫dist的目錄,它裏面有一個main.js文件

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

打包成功後,咱們須要在瀏覽器中運行index.html,它的運行結果以下圖所示

理解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
複製代碼

打包結果

運用佔位符

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

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

  • [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);
複製代碼

打包結果

打包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);
複製代碼

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

自動添加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,它的結果以下圖所示

模塊化打包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.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中的代碼輸出到了瀏覽器。

小結:在更改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,打包的結果以下圖所示:

在以上的打包中,咱們能夠發現:

  • 箭頭函數被轉成了普通的函數形式
  • 若是你仔細觀察此次打包輸出的話,你會發現打包體積會很是大,有幾百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 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,以下圖:



當咱們點擊頁面時,才 真正開始加載 第三方模塊,以下圖(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文件的大小很是小,若是應用默認值,它是不會進行代碼分割的,若是你要進一步測試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面板看到以下圖所示:

相信聰明的你必定看到了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進行打包,它的結果以下:

問題: 咱們發現,根本運行不起來,報錯$ 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閱讀整理而來,觀看視頻請支持正版。

相關文章
相關標籤/搜索