新版webpack4.0指南

此項目總共24節,主要參考資料以下:
視頻:https://coding.imooc.com/lear...
博客:https://itxiaohao.github.io/b...
文章:
https://webpack.js.org/
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://www.cnblogs.com/kwzm/...css

1、webpack簡介

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle

2、WebPack和Grunt以及Gulp相比有什麼特性

其實Webpack和另外兩個並無太多的可比性,Gulp/Grunt是一種可以優化前端的開發流程的工具,而WebPack是一種模塊化的解決方案,不過Webpack的優勢使得Webpack在不少場景下能夠替代Gulp/Grunt類的工具html

Grunt和Gulp的工做方式是:在一個配置文件中,指明對某些文件進行相似編譯,組合,壓縮等任務的具體步驟,工具以後能夠自動替你完成這些任務
Webpack的工做方式是:把你的項目當作一個總體,經過一個給定的主文件(如:index.js),Webpack將從這個文件開始找到你的項目的全部依賴文件,使用loaders處理它們,最後打包爲一個(或多個)瀏覽器可識別的JavaScript文件
若是實在要把兩者進行比較,Webpack的處理速度更快更直接,能打包更多不一樣類型的文件前端

3、webpack 核心概念:

  • Entry :入口
  • Module:模塊,webpack中一切皆是模塊
  • Chunk: 代碼庫,一個chunk由十多個模塊組合而成,用於代碼合併與分割
  • Loader: 模塊轉換器,用於把模塊原內容按照需求轉換成新內容
  • Plugin: 擴展插件,在webpack構建流程中的特定時機注入擴展邏輯來改變構建結果或作你想要作的事情
  • Output: 輸出結果

4、webpack打包流程:

webpack啓動後會從 Entry 裏配置的 Module 開始遞歸解析 Entry 依賴的全部Module.每找到一個Module,就會根據配置的Loader去找出對應的轉換規則,對Module進行轉換後,再解析出當前的Module依賴的Module.這些模塊會以Entry爲單位進行分組,一個Entry和其全部依賴的Module被分到一個組也就是一個Chunk。最好Webpack會把全部Chunk轉換成文件輸出。在整個流程中Webpack會在恰當的時機執行Plugin裏定義的邏輯vue

5、搭建webpack環境

1.webpack是基於node環境的,因此使用webpack以前須要先安裝node.js文件
2.安裝完node.js以後能夠在cmd命令行經過node -v 查看node是否安裝成功,出現版本號即安裝成功;而後經過npm -v 查看node中的包管理器是否安裝成功,若是出現版本號,也說明安裝成功
3.新建webpack-demo文件夾,而後cd進入這個文件目錄,執行以下命令初始化npmnode

npm init -y

執行完以後,咱們的文件夾中會多出一個package.json文件
圖片描述
而後咱們稍加修改react

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,                     
  "scripts": {
    
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

"private"設置爲 true, 表示私有的,不會被髮布到npm的線上倉庫中去
刪除"main":"index.js"這行,意思是咱們這個項目不會被外部引用,只是本身來用,不必暴露一個js文件,這能夠防止意外發布你的代碼
4.package.json文件已經就緒,接下來安裝webpack依賴jquery

npm install --save-dev webpack webpack-cli

咱們不是全局安裝而是安裝在項目內,此時在命令行輸入webpack -v 查看版本號會顯示出錯webpack

PS E:\Code\webpack4.0\webpack-demo> webpack -v
webpack : 沒法將「webpack」項識別爲 cmdlet、函數、腳本文件或可運行程序的名稱。請檢查名稱的拼寫,若是包括路徑,請確保路徑正確,而後再試一次。
所在位置 行:1 字符: 1
+ webpack -v
+ ~~~~~~~
    + CategoryInfo          : ObjectNotFound: (webpack:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

可是不要緊,node提供了一個npx命令,經過命令npx webpack -v就能夠查看版本號git

PS E:\Code\webpack4.0\webpack-demo> npx webpack -v
4.29.6

此時說明咱們webpack安裝成功
要想查看webpack之前的各類版本,能夠經過以下命令github

npm view webpack versions

6、webpack的配置文件

如今咱們將建立如下目錄結構、文件和內容:

webpack-demo
  |- package-lock.json
  |- package.json
+ |- index.html
+ |- /src
+   |- index.js

src/index.js

function component() {
    var element = document.createElement('div');
    element.innerHTML = 'hello webapck';
  
    return element;
  }
  
  document.body.appendChild(component());

index.html

<!doctype html>
<html>
  <head>
    <title>起步</title>
  </head>
  <body>
    <script src="./src/index.js"></script>
  </body>
</html>

而後,咱們稍微調整下目錄結構,將「源」代碼(/src)從咱們的「分發」代碼(/dist)中分離出來。「源」代碼是用於書寫和編輯的代碼。「分發」代碼是構建過程產生的代碼最小化和優化後的「輸出」目錄,最終將在瀏覽器中加載:

webpack-demo
  |- package-lock.json
  |- package.json
+ |- /dist
+    |- index.html
- |- index.html
+ |- /src
+   |- index.js

dist/index.html

<!doctype html>
  <html>
   <head>
     <title>起步</title>
   </head>
   <body>
-    <script src="./src/index.js"></script>
+    <script src="main.js"></script>
   </body>
  </html>

執行 npx webpack,會將咱們的腳本做爲入口起點,而後 輸出 爲 main.js。Node 8.2+ 版本提供的 npx 命令,能夠運行在初始安裝的 webpack 包(package)的 webpack 二進制文件(./node_modules/.bin/webpack):

PS E:\Code\webpack4.0\webpack-demo> npx webpack
Hash: 12bb1db463f0190f063f
Version: webpack 4.29.6
Time: 409ms
Built at: 2019-03-27 11:46:08
  Asset   Size  Chunks             Chunk Names
main.js  1 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 191 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

在瀏覽器中打開 index.html,若是一切訪問都正常,你應該能看到如下文本:'Hello webpack'

從上面能夠看到,咱們並無在文件中配置webpack的配置文件,爲什麼也能打包成功呢?這是由於webpack內部提供了一套默認配置,因此咱們打包的時候用的是它的默認配置文件,若是咱們想自定義這個配置文件裏面的內容,該怎麼作呢?

咱們增長一個webpack.config.js配置文件

webpack-demo
  |- package-lock.json
  |- package.json
+ |- webpack.config.js
  |- /dist
    |- index.html
    |- main.js
  |- /src
    |- index.js

webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',                         // 入口文件
    output: {
        filename: 'bundle.js',                       // 打包好以後的名字,以前默認是叫main.js 這裏咱們改成bundle.js
        path: path.resolve(__dirname, 'dist')        // 打包好的文件應該放到哪一個文件夾下
    }
}

如今,讓咱們經過新配置文件再次執行構建

PS E:\Code\webpack4.0\webpack-demo> npx webpack
Hash: ececbdb7c981b95af3a3
Version: webpack 4.29.6
Time: 130ms
Built at: 2019-03-27 14:20:10
    Asset   Size  Chunks             Chunk Names
bundle.js  1 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 191 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

此時項目結構應該是

webpack-demo
  |- package-lock.json
  |- package.json
  |- webpack.config.js
  |- /dist
    |- index.html
    |- bundle.js
  |- /src
    |- index.js

當咱們運行npx webapck時,webpack並不知道如何去打包,因而它就是會找默認的配置文件,找到webpack.config.js這個文件,而後根據這個文件中配置的入口和出口打包了,假設咱們這個配置文件的名字不是這個默認的名字,而是叫webpack.aaa.js,如今咱們從新運行npx webpack,這個時候它就不會執行這個webpack.aaa.js這個文件了,而是會去走它內部的一套流程,打包出來的仍是main.js而不是bundle.js,若是咱們任然想輸出bundle.js,這時咱們能夠執行以下命令

PS E:\Code\webpack4.0\webpack-demo> npx webpack --config webpack.aaa.js
Hash: ececbdb7c981b95af3a3
Version: webpack 4.29.6
Time: 116ms
Built at: 2019-03-27 14:45:53
    Asset   Size  Chunks             Chunk Names
bundle.js  1 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 191 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
若是 webpack.config.js 存在,則 webpack 命令將默認選擇使用它。咱們在這裏使用 --config 選項只是向你代表,能夠傳遞任何名稱的配置文件。這對於須要拆分紅多個文件的複雜配置是很是有用

測試完以後,咱們把webpack.aaa.js文件還原成webpack.config.js

考慮到用 npx這種方式來運行本地的 webpack 不是特別方便,咱們能夠設置一個快捷方式。在 package.json 添加一個 npm 腳本(npm script):
package.json

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
+    "bundle": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0"
  }
}

意思是當咱們運行bundle這個命令,它就會自動幫咱們執行webpack這個命令,如今,能夠使用 npm run bundle命令,來替代咱們以前使用的 npx 命令

PS E:\Code\webpack4.0\webpack-demo> npm run bundle

> webpack-demo@1.0.0 bundle E:\Code\webpack4.0\webpack-demo
> webpack

Hash: 12bb1db463f0190f063f
Version: webpack 4.29.6
Time: 241ms
Built at: 2019-03-27 14:54:39
  Asset   Size  Chunks             Chunk Names
main.js  1 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 191 bytes {0} [built]

如今,咱們已經實現了一個基本的構建過程,此刻你的項目應該和以下相似:

webpack-demo
|- /dist
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- index.js
|- package.json
|- package-lock.json
|- webpack.config.js

細節補充:
咱們在以前打包的時候會發現命令行會出現以下警告

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concep...

是由於咱們沒有給打包設置模式,如今咱們在webpack.config.js中設置mode
webpack.config.js

const path = require('path');

module.exports = {
    mode: 'production',                              // 不寫的mode,默認就是生產模式
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                       
        path: path.resolve(__dirname, 'dist')       
}

從新打包,發現警告消失了,其實,這裏mode除了能夠設置production外還能夠設置成development,設置development模式打包以後代碼是不會被壓縮的

7、webpack中loader

loader能夠說是webpack最核心的部分,loader簡單來講就是一個導出爲函數的JavaScript模塊,webpack會配置文件申明的倒序調用loader,傳入資源文件,經loader處理後傳給下一loader或者webpack處理, 通俗點理解就是,webpack自身只理解JavaScript,loader可讓webpack可以去處理那些非JavaScript文件

(一)、使用loader打包圖片

安裝file-loader

npm install file-loader --save-dev

webpack.config.js

const path = require('path');

    module.exports = {
        mode: 'development',                             
        entry: './src/index.js',                         
        output: {
            filename: 'bundle.js',                      
            path: path.resolve(__dirname, 'dist')     
        },
+       module: {
+            rules: [                      // module.rules 容許你在 webpack 配置中指定多個 loader
+                {
+                    test: /\.(png|svg|jpg|gif)$/,
+                    use: [
+                        'file-loader'        // 這裏實際上是  {loader: 'file-loader'}的簡寫
+                    ]
+                }
+            ]
+        }
    }

往src目錄下添加一張圖片(如:04.jpg),而後刪除index.js裏面的內容,添加以下內容:

import avatar from './04.jpg';

var img = new Image();
img.src = avatar;

var root = document.getElementById('root');
root.append(img);

/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>起步</title>
</head>
<body>
 +  <div id="root"></div>
    <script src="./bundle.js"></script>
</body>
</html>

最後執行npm run bundle打包,會發現dist目錄下多出了一張圖片,如今目錄結構以下

webpack-demo
|- /dist
  |- bundle.js
  |- c613962b1e741b4139150622b2371cd9.jpg
  |- index.html
|- /node_modules
|- /src
  |- index.js
|- package.json
|- package-lock.json
|- webpack.config.js

打開index.html文件,圖片顯示正常,說明咱們已經打包成功

若是咱們想自定義打包後圖片的名字該如何處理呢?
webpack.config.js

module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        // [name]: 資源的基本名稱    [ext]: 資源擴展名
     +                  options: {
     +                      name: '[name].[ext]'  
     +                  }
                    }
                ]
            }
        ]
    }

刪除掉dist目錄下的bundle.js和c613962b1e741b4139150622b2371cd9.jpg,而後從新執行npm run bundle,打開index.html文件仍然正常顯示,如今dist目錄下以下

|- /dist
  |- bundle.js
  |- 04.jpg
  |- index.html

如今咱們圖片是打包到dist目錄下,若是咱們想圖片打包到別的目錄下,能夠經過outputPath這個屬性來配置

module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
           +                outputPath: 'images/'              
                        }
                    }
                ]
            }
        ]
    }

刪除掉dist目錄下的bundle.js和04.jpg,而後從新執行npm run bundle,打開index.html文件仍然正常顯示,如今dist目錄下以下

|- /dist
  |-images
    |- 04.jpg
  |- bundle.js
  |- index.html
其實file-loader還有許多其它的參數,具體能夠參見 file-loader文檔

接下來,咱們介紹一個和file-loader很相似的url-loader ,url-loader除了能夠作file-loader的 工做以外 ,它還能作一個額外的事情
安裝url-loader

npm install --save-dev url-loader

而後咱們把dist目錄下的images文件和bundle.js文件刪掉,用url-loader替換掉file-loader
webpack.config.js

{
 -     loader: 'file-loader',
 +     loader: 'url-loader'
       options: {
                 name: '[name].[ext]',
                 outputPath: 'images/'
               }
      }

而後從新執行npm run bundle,打包正常,可是咱們發現圖片並無打包進dist目錄下

|- /dist
  |- bundle.js
  |- index.html

打開index.html,發現圖片仍是能正常顯示,是否是很奇怪,這究竟是怎麼回事呢?
咱們打開控制檯,發現圖片地址是以base64的形式被引進來的
圖片描述

這是由於當你去打包一個jpg格式的圖片的時候,用了url-loader,它會把你圖片轉換成一個base64的字符串,而後直接放到bundle.js文件裏面,而不是生成一個圖片文件
可是若是這個loader這麼用,實際上是不合理的,雖然圖片被打包進js裏面,加載好js 圖片天然就出來,它不用再去額外請求一個圖片的地址了,省了一次http請求,可是帶來的問題是什麼呢?若是這個文件特別大,打包生成的js文件也就會特別的大,那麼你加載這個js的時間就會很長,那麼url-loader的最佳使用方式是什麼?若是圖片很是小隻有1-2kb,那麼圖片被打包進js文件是個很是好的選擇,若是圖片很大,那就應該像file-loader同樣,把圖片打包到dist目錄下,不要打包到bundle.js裏,這樣更合適

其實咱們在options裏再配置個參數limit就能夠實現這個功能

{
   loader: 'url-loader',
   options: {
       name: '[name].[ext]',
       outputPath: 'images/',
 +     limit: 2048
     }
 }

意思是,若是你的圖片大小超過了2048個字節的話,那麼就會像file-loader同樣,打包到dist目錄下生成一個圖片;可是若是圖片小於2048個字節也就是小於2kb的時候,url-loader會直接把這個圖片變成一個base64的字符串放到bundle.js中

接下來驗證下,咱們04.jpg圖片是1.58M確定大於20kb,執行npm run bundle打包,果真在dist目錄下生成了圖片

|- /dist
  |-images
    |- 04.jpg
  |- bundle.js
  |- index.html

而後咱們刪除掉images文件和bundle.js文件,再把limit值改成900000000,1.58M確定小於這個值,再從新執行打包,發現圖片被打包進bundle裏面了

|- /dist
  |- bundle.js
  |- index.html
其實url-loader還有許多其它的參數,具體能夠參見 url-loader文檔

(二)、使用loader打包樣式

安裝style-loader和css-loader

npm install --save-dev style-loader css-loader

webpack.config.js

const path = require('path');

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
  +         {
  +           test: /\.css$/,
  +           use: ['style-loader', 'css-loader']
  +         }
        ]
    }
}

而後在src中新建一個index.css文件
src/index.css

.avatar {
    width: 150px;
    height: 150px;
}

src/index.js

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

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

var root = document.getElementById('root');
root.append(img);

從新運行npm run bundle,再次在瀏覽器中打開 index.html,你應該看到圖片大小已經變成150*150了,檢查頁面,並查看頁面的 head 標籤。它應該包含咱們在 index.js 中導入的 style 塊元素 ,那麼問題來了,爲何須要兩個loader來處理呢?這是由於它們兩個分工不一樣,css-loader會幫咱們分析出全部css文件之間的關係, 最終把這些css文件合併成一段css,style-loader在獲得css-loader生成的內容以後,style-loader會把這段內容掛載到頁面的head部分

若是咱們項目中用的是sass或者less該如何處理呢?
如今咱們把src中的index.css改成index.scss文件
src/index.scss

body{
    .avatar {
        width: 150px;
        height: 150px;
    }
}

index.js

import avatar from './04.jpg';
- import './index.css';
+ import 'index.scss'

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

var root = document.getElementById('root');
root.append(img);

webpack.config.js

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

最後咱們執行npm run bundle,打包成功刷新頁面,發現圖片又變回原來的大小,咱們打開控制檯head部分,發現style中的語法並非css語法,而是原始的scss語法,因此瀏覽器固然是不能識別了,因此咱們在打包scss文件時還須要藉助其餘額外的loader,幫助咱們把scss語法翻譯成css語法
圖片描述

安裝sass-loader和node-sass, node-sass是sass-loader的依賴,因此也須要一併安裝

npm install sass-loader node-sass --save-dev

安裝完成以後,再在webpack.config.js中配置sass-loader

{
      test: /\.scss$/,
      use: [
          'style-loader',       // 將 JS 字符串生成爲 style 節點
          'css-loader',         // 將 CSS 轉化成 CommonJS 模塊
+          'sass-loader'         // 將 Sass 編譯成 CSS
      ]
 }

執行npm run bundle,刷新頁面發現圖片又變回150*150了,檢查head,能夠看到sass語法已經被編譯成css語法
圖片描述

注意: 在webpack的配置裏面loader是有順序的,執行順序是 從下到上,從右到左,因此當咱們去打包一個sass文件的時候,首先會執行sass-loader,對sass代碼進行翻譯,翻譯成css代碼以後給到css-loader,而後css-loader把全部的css合併成一個css模塊,最後被style-loader掛載到頁面的head中去

其實css-loader和sass-loader還有許多其它的參數,具體能夠參見css-loader文檔sass-loader文檔

有時候咱們寫C3的新特性的時候,每每須要在這樣寫,目的是爲了兼容不一樣版本瀏覽器

div {
    transform: translate(150px,150px);
    -ms-transform: translate(150px,150px);
    -moz-transform: translate(150px,150px);
    -webkit-transform: translate(150px,150px);
}

可是這樣寫起來會很麻煩,咱們可不能夠經過loader來自動爲屬性添加廠商前綴呢?答案確定是能夠的,接下來爲你們介紹一個postcss-loader
安裝postcss-loader

npm i -D postcss-loader

index.scss

body{
    .avatar {
        width: 150px;
        height: 150px;
 +      transform: translate(150px,150px)
    }
}

而後再在webpack-demo目錄下建立一個postcss.config.js文件
postcss.config.js

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

這裏咱們還須要安裝下autoprefixer

npm install autoprefixer -D

安裝完成以後,咱們在webpack.config.js中配置postcss-loader
webpack.config.js

{
      test: /\.scss$/,
      use: [
          'style-loader',       // 將 JS 字符串生成爲 style 節點
          'css-loader',         // 將 CSS 轉化成 CommonJS 模塊
+          'postcss-loader',
          'sass-loader',        // 將 Sass 編譯成 CSS
      ]
 }

從新npm run bundle,打包成功以後刷新頁面,顯示正常,而且圖片樣式上會自動添加上了廠商前綴
圖片描述

postcss-loader其餘的參數使用具體見 postcss-loader文檔

補充知識:

一、importLoader參數

若是咱們在scss文件中又去引入了一個額外的scss文件,這種狀況webpack該如何去處理呢?
首先咱們在src中新建一個avatar.scss文件
src/avatar.scss

body {
    .abc {
        border: 5px solid red;
    }
}

index.scss

+ @import './avatar.scss';

   body{
        .avatar {
            width: 150px;
            height: 150px;
            transform: translate(150px,150px)
        }
    }

webpack打包的時候對於index.js中引入的index.scss文件,它會依次調用postcss-loader,sass-loader, css-loader,style-loader,可是它在打包index.scss文件的時候,它裏面又經過import語法額外引入了一個avatar.scss文件,那麼有可能這塊的引入在打包的時候,就不會去走sass-loader和postcss-loader了,而是直接去走css-loader和style-loader了,若是咱們但願在index.scss裏面引入的avatar.scss文件也能夠走sass-loader和postcss-loader,那該怎麼辦呢?這時咱們須要在css-loader裏面配置一個importLoaders參數
webpack.config.js

{
                test: /\.scss$/,
                use: [
                    'style-loader', 
   -                'css-loader'
   +                {
   +                    loader: 'css-loader',
   +                    options: {
       // 查詢參數 importLoaders,用於配置「css-loader 做用於 @import 的資源以前」有多少個 loader
   +                         importLoaders: 2     // 0 => 無 loader(默認); 1 => postcss-loader; 2 => postcss-loader, sass-loader
   +                     }
   +                  },
                    'postcss-loader',
                    'sass-loader'
                ]
            }

意思就是你經過@import引入的scss文件在打包以前也要去走兩個loader,也就是postcss-loader和sass-loader;這種語法就能保證不管你是在js裏面直接去引入scss文件,仍是在scss文件裏再去引用別的scss文件,都會從下到上執行全部的loader,這樣就不會出現任何的問題了

二、css模塊化打包

在src下建立一個createAvatar.js文件
createAvatar.js

import avatar from './04.jpg';

function createAvatar() {
    var img = new Image();
    img.src = avatar;
    img.classList.add('avatar')

    var root = document.getElementById('root');
    root.append(img);
}

export default createAvatar;

index.js

import avatar from './04.jpg';
import './index.scss';
+ import createAvatar from './createAvatar';

+ createAvatar()

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

var root = document.getElementById('root');
root.append(img);

從新執行npm run bundle,打包成功以後刷新頁面,頁面會正常顯示兩張圖片,而且這兩張圖片都有avatar樣式
圖片描述

這說明咱們經過import './index.scss'這種形式引入的css文件,至關因而全局的,若是咱們一不當心改了這個文件裏面的樣式,極可能會影響到另外一個文件裏面的樣式,很容易出現樣式衝突的問題,這樣就引出了css 模塊化的概念,讓css文件只做用於當前這個模塊

咱們在webpack.config.js中的css-loader中引入modules參數
webpack.config.js

{
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
     +                      modules: true               //  意思是開啓css的模塊化打包
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            }

而後咱們在index.js中修改scss的引入
index.js

import avatar from './04.jpg';
- import './index.scss';
+ import style from './index.scss';
+ import createAvatar from './createAvatar';

+ createAvatar()

  var img = new Image();
  img.src = avatar;
- img.classList.add('avatar')
+ img.classList.add(style.avatar)
  var root = document.getElementById('root');
  root.append(img);

而後從新打包,刷新頁面,你會發現只有當前文件中的這個圖片有樣式,而經過createAvatar引入的這個圖片是沒有樣式的

此時目錄結構以下

webpack-demo
|- /dist
  |- images
    |- 04.jpg
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- 04.jpg
  |- avatar.scss
  |- createAvatar.js
  |- index.scss
  |- index.js
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

(三)、使用loader打包字體文件

首先刪除index.js和index.scss裏面的內容,而後刪除dist目錄下的imags文件夾和bundle.js
而後刪除04.jpg和createAvatar.js,avatar.scss文件
如今目錄結構以下:

webpack-demo
|- /dist
  |- index.html
|- /node_modules
|- /src
  |- index.scss
  |- index.js
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

首先咱們在src中新建一個font文件夾
而後咱們從IconFont中下載兩個圖標到本地,而後解壓到文件夾,把文件夾中的.eot,.svg,.ttf,.woff,.woff2字體文件複製到font文件夾下
最後把解壓文件夾中的iconfont.css文件裏面的內容複製到index.scss文件中
接着咱們 把index.scss中的iconfont字體文件的路徑改對
圖片描述
而後咱們在index.js中添加以下代碼

var root = document.getElementById('root');
import './index.scss'
root.innerHTML = '<div class="iconfont iconyanxianbi"></div>'

在webpack.config.js中去掉css模塊化配置而且在webpack中添加打包字體文件的loader

const path = require('path');

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
-                           modules: true
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
+           {
+              test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
+              use: ['file-loader']
+           }
        ]
    }
}

執行npm run bundle,打包成功以後刷新頁面,字體圖標已經生效
此時目錄結構:

webpack-demo
|- /dist
  |- 4bba583098563e64f4b12ab1d27cd516.eot
  |- 7db708ac7335b8e8596a04a93c5501cd.ttf
  |- 0052329c35318bbe484b99b3d3e5aa47.woff
  |- 54718bd06e7ee6c87b9e2f41c96851ea.svg
  |- bundle.js
  |- index.html
|- /node_modules
|- /src
  |- font
    |- iconfont.eot
    |- iconfont.svg
    |- iconfont.ttf
    |- iconfont.woff
    |- iconfont.woff2
  |- index.scss
  |- index.js
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

8、webpack中plugins

插件是 webpack 生態系統的重要組成部分,爲社區用戶提供了一種強大方式來直接觸及 webpack 的編譯過程(compilation process)。插件可以 鉤入(hook) 到在每一個編譯(compilation)中觸發的全部關鍵事件。在編譯的每一步,插件都具有徹底訪問 compiler 對象的能力,若是狀況合適,還能夠訪問當前 compilation 對象。

(一)、html-webpack-plugin

在以前的項目中咱們dist目錄中的index.html文件是咱們手動建立的,若是咱們每次打包都本身手動建立那就太麻煩了,因此咱們須要藉助html-webpack-plugin這個插件,該插件會在打包結束後,自動生成一個html文件,並把打包生成的js自動引入到這個html文件中。這對於在文件名中包含每次會隨着編譯而發生變化哈希的 webpack bundle 尤爲有用。 你可讓插件爲你生成一個HTML文件,使用lodash模板提供你本身的模板,或使用你本身的loader

安裝

npm install --save-dev html-webpack-plugin

首先咱們刪除整個dist文件夾,而後再在webpack.config.js中配置這個插件

const path = require('path');
+  const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'bundle.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
+   plugins: [new HtmlWebpackPlugin()]
}

最後執行npm run bundle,打包完成後會看到dist目錄下webpack自動幫咱們生成了一個index.html文件
可是咱們會發現咱們直接打開這個index.html文件字體圖標並無顯示出來,這是由於咱們在src/index.js中獲取過root這個dom節點,可是咱們打包生成的index.html中沒有給咱們自動生成一個這樣的dom元素
圖片描述

接下來,咱們在src中建立一個index.html模板
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>
+   <title><%= htmlWebpackPlugin.options.title %></title> 
</head>
<body>
    <div id="root"></div>
</body>
</html>

而後在webpack.config.js中對HtmlWebpackPlugin從新配置下
webpack.confiig.js

plugins: [new HtmlWebpackPlugin(
+       {
+          template: 'src/index.html',           //意思是打包的時候以哪一個html文件爲模板
+          filename: 'index.html',               // 默認狀況下生成的html文件叫index.html,能夠自定義
+          title: 'test App',   // 爲打包後的index.html配置title,這裏配置後,在src中的index.html模板中就不能寫死了,須要<%= htmlWebpackPlugin.options.title %>這樣寫才能生效
+          minify: {
+                collapseWhitespace: true        // 把生成的index.html文件的內容的沒用空格去掉
+            }
+       }
    )]

從新刪除dist目錄,避免干擾,而後再去打包,打包完成以後打開dist目錄中的index.html文件,能夠看到字體圖標能正常顯示了

其餘參數配置請見 html-webpack-plugin官方文檔

(二)、clean-webpack-plugin

假如咱們想改變打包生成以後的js文件名,好比咱們不想叫bundle.js了而是想叫dist.js
webpack.config.js

output: {
-       filename: 'bundle.js',  
+       filename: 'dist.js',                    
        path: path.resolve(__dirname, 'dist')        
    },

從新npm run bundle,能夠看到dist目錄下會出多一個新打包出來的dist.js文件,可是上一次打包的bundle.js仍是依然存在,咱們但願的是,每次打包的時候,能幫咱們先把dist目錄先刪除,而後從新生成,要實現這個功能咱們就須要藉助clean-webpack-plugin這個插件,這個插件不是官方推薦的,而是一個第三方插件

安裝Webpack

npm install clean-webpack-plugin -D

webpack.confiig.js

const path = require('path');
   const HtmlWebpackPlugin = require('html-webpack-plugin');
+  const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
    mode: 'development',                             
    entry: './src/index.js',                        
    output: {
        filename: 'dist.js',                      
        path: path.resolve(__dirname, 'dist')        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin(
        {
            template: 'src/index.html',
            filename: 'index.html',
            minify: {
                collapseWhitespace: true
            }
        }
+    ),  new CleanWebpackPlugin()]
}

配置完了以後從新打包,發現以前打包生成的bundle.js就看不到了
圖片描述

詳情見 clean-webpack-plugin官方文檔

此時目錄結構以下

webpack-demo
|- /dist
  |- 4bba583098563e64f4b12ab1d27cd516.eot
  |- 7db708ac7335b8e8596a04a93c5501cd.ttf
  |- 0052329c35318bbe484b99b3d3e5aa47.woff
  |- 54718bd06e7ee6c87b9e2f41c96851ea.svg
  |- dist.js
  |- index.html
|- /node_modules
|- /src
  |- font
    |- iconfont.eot
    |- iconfont.svg
    |- iconfont.ttf
    |- iconfont.woff
    |- iconfont.woff2
  |- index.scss
  |- index.js
  |- index.html
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

9、Entry與Output的基礎配置

entry顧名思義就是打包的入口文件
在webpack.config.js中entry對應的是一個字符串,其實它是下面這種方式的簡寫

entry: {
    main: './src/index.js'
}

默認打包輸出的文件是main.js
假如咱們有這樣一個需求,咱們須要將src/index.js文件打包兩次,第一次打包到一個main.js中,第二次打包到一個sub.js中

-  entry: './src/index.js'
+  entry: {
+        main: './src/index.js',
+        sub: './src/index.js'
+    }, 
+  output: {
        filename: 'dist.js',                      
        path: path.resolve(__dirname, 'dist')        
    },

執行npm run bundle,咱們會發現打包出錯了,這是由於咱們打包要生成兩個文件一個叫main一個叫sub,最終都會起名叫dist.js,這樣的話名字就衝突了,想要解決這個問題,咱們就須要把output中的filename替換成一個佔位符,而不是一個固定的名字

output: {
-         filename: 'dist.js',
+        filename: '[name].js',      //  這裏name指的就是前面entry中對應的main和sub                   
         path: path.resolve(__dirname, 'dist')        
    },
這裏佔位符還有不少具體能夠見 output參數

從新npm run bundle打包,打包完成以後咱們發現dist目錄中既有main.js也有sub.js文件,而且index.html中把main.js和sub.js同時都引入進來了

有的時候可能會有這樣一種場景,打包完成以後咱們會把這些打包好的js文件託管到CDN上,這時output.publicPath 是很重要的選項。若是指定了一個錯誤的值,則在加載這些資源時會收到 404 錯誤

output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
+       publicPath: 'http://cdn.com.cn'        
    },

從新打包,而後查看dist中的index.html,能夠看到注入進來的js文件中每一個文件前面都自動帶有cdn域名
圖片描述

10、SourceMap的配置

當 webpack 打包源代碼時,可能會很難追蹤到錯誤和警告在源代碼中的原始位置。例如,若是將三個源文件(a.js, b.js 和 c.js)打包到一個 bundle(bundle.js)中,而其中一個源文件包含一個錯誤,那麼堆棧跟蹤就會簡單地指向到 bundle.js。這並一般沒有太多幫助,由於你可能須要準確地知道錯誤來自於哪一個源文件。

爲了更容易地追蹤錯誤和警告,JavaScript 提供了 source map 功能,將編譯後的代碼映射回原始源代碼。若是一個錯誤來自於 b.js,source map 就會明確的告訴你

如今咱們作一些回退處理,將目錄中dist目錄刪掉,而後把src中的font文件夾和index.scss刪掉,而且清空index.js裏面的內容
此時目錄以下

webpack-demo
|- /node_modules
|- /src
  |- index.js
  |- index.html
|- package.json
|- package-lock.json
|- postcss.config.js
|- webpack.config.js

而後對webpack.config.js作稍許修改

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
    mode: 'development',          
+   devtool: 'none',           // 咱們如今是開發模式,這個模式下,默認sourcemap已經被配置進去了,因此須要關掉              
    entry: {
        main: './src/index.js',
-       sub: './src/index.js'
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
-       publicPath: 'http://cdn.com.cn'        
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin(
        {
            template: 'src/index.html',
            title: 'test App',
            filename: 'index.html',
            minify: {
                collapseWhitespace: true
            }
        }
    ), new CleanWebpackPlugin()]
}

而後再在src/index.js中生成一個錯誤

cosnole.error('I get error!');

從新打包,而後打開dist目錄中的index.html文件,而後再控制檯能夠看到錯誤,可是咱們只能看到這個錯誤來自於打包後的main.js裏面,並不知道這個錯誤來自於源文件的哪一行裏面,這對於咱們代碼調試很是不友好,咱們須要webpack明確告訴咱們是哪個文件的哪一行出錯,怎麼作呢?
圖片描述

如今咱們對webpack.config.js中的devtool從新改下

mode: 'development', 
-   devtool: 'none',
+   devtool: 'source-map',                            
    entry: {
        main: './src/index.js',
    },

而後npm run bundle,刷新頁面,能夠看到若是用source-map,在dist目錄下會多出一個main.js.map文件,這個map文件中是一些映射的對應關係,它能夠對咱們源代碼和打包後的代碼作一個映射,

注意: 在谷歌瀏覽器中source-map仍是沒法指向源文件 圖片描述
可是在火狐是能夠指向源文件的 圖片描述

官方文檔中也提到source map在Chrome中有一些問題,具體看這裏

此外咱們devtool還能夠配置inline-source-map,從新打包,刷新頁面,能夠看到在谷歌中它能夠指向源文件
圖片描述

可是咱們在dist目錄中發現,此時並無main.js.map文件了,其實當咱們用inline-source-map時,這個map文件會經過dataUrl的形式直接寫在main.js裏面
圖片描述

此外devtool還能夠配置inline-cheap-source-map,它相似於inline-source-map,惟一的區別就是inline-source-map會幫咱們把錯誤代碼精確到源文件的第幾行第幾個字符,可是咱們通常只須要知道在哪一行就能夠了,這樣的一種映射它比較耗費性能,而加個cheap以後意思就是隻須要映射哪一行出錯就能夠了,因此相對而言它的打包速度會快些

可是inline-cheap-source-map這個配置只會針對於咱們的業務代碼進行映射,好比這裏咱們的index.js文件和打包後的main.js作映射,它不會管引入的其餘第三方模塊之間的映射,若是咱們想讓webpack不只管業務代碼還管第三方模塊錯誤代碼之間的映射,那麼咱們能夠配置這個inline-cheap-module-source-map

除此以外,咱們還能夠配置devtool:eval, eval是打包速度最快的一種方式,性能最好的一種,可是針對比較複雜的代碼狀況下,用eval可能提示出來的內容並不全面

最佳實踐:在development模式,用cheap-module-eval-source-map; 在production模式下,用cheap-module-source-map

devtool還有許多其餘參數,具體能夠見devtool官方文檔

11、webpack-dev-server

每次咱們改變代碼以後,都會從新npm run bundle,而後手動打開dist目錄下的index.html查看,才能實現代碼的從新編譯運行,實際上這種方式會致使咱們的開發效率很是低下,咱們但願咱們改了src下的源代碼dist目錄自動從新打包

要想實現這種功能,有三種方法:

(一)、修改package.json配置

"scripts": {
     "bundle": "webpack"
 +   "watch": "webpack --watch"  // 意思是webpack會監聽打包的文件,只要打包的文件發生變化,就會自動從新打包
  },

從新執行npm run watch,而後咱們把src/index.html代碼改下

-   cosnole.error('I get error!');
+   console.log('哈哈哈')

不用從新打包,咱們刷新頁面就能夠看到控制檯已經打印出了‘哈哈哈’字樣

(二)、dev-server

有時候咱們須要命令不只能幫咱們實現自動打包還能第一次運行的時候幫咱們自動打開瀏覽器頁面同時還能模擬一些服務器的功能,這時咱們能夠藉助webpack-dev-server這個工具
webpack-dev-server 爲你提供了一個簡單的 web 服務器,而且可以實時從新加載(live reloading)。它並不真實打包文件,只是在內存中生成
安裝

npm install --save-dev webpack-dev-server

修改webpack.config.js配置

+   devServer: {
+     contentBase: './dist'
+   },

以上配置告知 webpack-dev-server,在 localhost:8080 下創建服務,將 dist 目錄下的文件,做爲可訪問文件。

讓咱們添加一個 script 腳本,能夠直接運行開發服務器(dev server):
package.json

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "bundle": "webpack",
    "watch": "webpack --watch",
+   "start": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^9.5.1",
    "clean-webpack-plugin": "^2.0.1",
    "css-loader": "^2.1.1",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-server": "^3.3.1"
  }
}

如今,咱們能夠在命令行中運行 npm run start,能夠看到它幫咱們生成了一個服務器地址
圖片描述
手動打開這個地址,在控制檯看到內容正常打印出來了,若是如今修改和保存任意源文件,web 服務器就會自動從新加載編譯後的代碼

devServer中咱們還能夠配置open參數:

devServer: {
     contentBase: './dist',
     open: true     // 執行npm run start時會自動打開頁面,而不須要咱們手動打開地址,它等同於咱們在package.json中"start": "webpack-dev-server --open" 這個命令  
     },

若是你有單獨的後端開發服務器 API,而且但願在同域名下發送 API 請求 ,那麼代理某些 URL 會頗有用
在 localhost:3000 上有後端服務的話,你能夠這樣啓用代理:

devServer: {
        contentBase: './dist',
        open: true,
+       proxy: {
+           '/api:': 'http://localhost:3000'
+       }
    },

請求到 /api/users 如今會被代理到請求 http://localhost:3000/api/users

還能夠設置端口號

devServer: {
        contentBase: './dist',
        open: true,
+       port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        }
    },

能夠看到咱們端口號已經變成了8080了
圖片描述

webpack-dev-server還有其餘不少參數,具體見 devServer官方文檔

(三)、使用 webpack-dev-middleware

若是不用webpack-dev-server,咱們能夠經過webpack-dev-middlewar結合express手動寫一個這樣的服務
webpack-dev-middleware 是一個容器(wrapper),它能夠把 webpack 處理後的文件傳遞給一個服務器(server)。 webpack-dev-server 在內部使用了它,同時,它也能夠做爲一個單獨的包來使用,以便進行更多自定義設置來實現更多的需求,接下來是一個 webpack-dev-middleware 配合 express server 的示例

首先,安裝 express 和 webpack-dev-middleware

npm install --save-dev express webpack-dev-middleware

接下來咱們須要對 webpack 的配置文件作一些調整,以確保中間件(middleware)功能可以正確啓用:

output: {
        filename: '[name].js',                           
        path: path.resolve(__dirname, 'dist'),
 +      publicPath: '/'         // 表示全部打包生成的文件之間的引用都加一個根路徑
    },

publicPath 也會在服務器腳本用到,以確保文件資源可以在 http://localhost:3000 下正確訪問

接下來,咱們新建一個server.js文件

webpack-demo
        |- dist
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- package.json
        |- package-lock.json
        |- postcss.config.js
+        |- server.js
        |- webpack.config.js

server.js

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const complier = webpack(config)   // 用webpack結合這個配置文件隨時進行代碼的編譯

const app = express();
app.use(webpackDevMiddleware(complier, {
    publicPath: config.output.publicPath
}))


app.listen(3000, () => {
    console.log('server is running')
})

如今,添加一個 npm script,以使咱們更方便地運行服務:
package.json

"scripts": {
    "bundle": "webpack",
    "watch": "webpack --watch",
    "start": "webpack-dev-server",
+   "server": "node server.js"
  },

執行npm run server,將會有相似以下信息輸出,說明node服務器已經運行,而且已經幫咱們打包好文件,而後咱們打開localhost:3000,能夠看到控制檯打印正常,可是這個服務沒有webpack-dev-server這樣智能,每次更改源文件以後都須要手動刷新頁面才能看到內容的變化
圖片描述

幾點區別:
output.publicPath: 是指打包後的html文件加載其餘css/js時,加上publicPath這個路徑。
devServer.contentBase: 是指以哪一個目錄爲靜態服務
devServer.publicPath: 此路徑下的打包文件可在瀏覽器中訪問,假設服務器運行在 http://localhost:8080 而且 output.filename 被設置爲 bundle.js。默認 publicPath 是 "/",因此你的包(bundle)能夠經過 http://localhost:8080/bundle.js 訪問,能夠修改 publicPath,將 bundle 放在一個目錄publicPath: "/assets/",你的包如今能夠經過 http://localhost:8080/assets/bundle.js 訪問

12、熱模塊更新

模塊熱替換(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它容許在運行時更新各類模塊,而無需進行徹底刷新

如今作一些回退處理
package.json

"scripts": {
-    "bundle": "webpack",
-    "watch": "webpack --watch",
     "start": "webpack-dev-server",
-    "server": "node server.js"
  },

刪除掉server.js文件,而且對webpack.config.js作一些修改

output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
  -     publicPath: '/'
    },
    module: {
        rules: [
           ...
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
+           {
+                test: /\.css$/,
+                use: [
+                    'style-loader',
+                    'css-loader', 
+                    'postcss-loader',
+               ]
+           },
            ...
        ]
    }
}

而後咱們去掉src/index.js中的內容,而後從新添加新的內容

import './style.css';

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

btn.onclick = function() {
    var div = document.createElement('div');
    div.innerHTML = 'item';
    document.body.appendChild(div)
}

同時在src目錄下新增一個style.css文件

div:nth-of-type(odd) {
    background-color: yellow;
}

從新npm run start,會看到頁面上多了一個新增按鈕,點擊新增按鈕,頁面會出現item,而且奇數的item背景色是黃色;
如今咱們把style.css中的背景色改成blue,保存,回到頁面,webpack-dev-server發現代碼改變了,它會幫咱們從新打包編譯而且從新刷新頁面,致使頁面上的這些item所有都沒有了,若是咱們想測試這些item背景色是否改變,就須要從新點擊按鈕,每次這樣的話就會很麻煩, 咱們但願當咱們改變樣式代碼的時候,不要幫咱們刷新頁面,只是把樣式代碼替換掉就能夠了,以前頁面渲染出來的這些東西不要動,這個時候就能夠藉助HMR的這個功能來幫咱們實現

打開webpack.config.js這個配置文件,進行修改

const path = require('path');
   const HtmlWebpackPlugin = require('html-webpack-plugin');
   const CleanWebpackPlugin = require('clean-webpack-plugin');
+  const webpack = require('webpack');

module.exports = {
    mode: 'development', 
    devtool: 'heap-module-eval-source-map',                            
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist')
    },
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
+       hot: true,               // 啓用 webpack 的模塊熱替換特性
+       hotOnly: true            // 即便HMR功能不生效,也不讓瀏覽器自動刷新
    },
    module: {
        ...
    },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
+       new webpack.HotModuleReplacementPlugin()        // webapck內置插件
    ]
}

從新運行npm run start,而後點擊新增,背景色變成藍色了,而後咱們到style.css中將blue變成red,回到頁面能夠看到背景爲藍色的地方已經所有替換成了紅色,而頁面並無所有刷新,只是有樣式改變的地方局部進行了刷新

那麼HMR在js中有什麼好處呢?接下來看下面這個例子
在src中新增一個counter.js和number.js文件
counter.js

function counter() {
    var div = document.createElement('div');
    div.setAttribute('id', 'counter');
    div.innerHTML = 1;
    div.onclick = function() {
        div.innerHTML = parseInt(div.innerHTML, 10) + 1;
    }
    document.body.appendChild(div)
}

export default counter;

number.js

function number() {
    var div = document.createElement('div');
    div.setAttribute('id', 'number');
    div.innerHTML = 1000;
    document.body.appendChild(div)
}
export default number;

index.js

// import './style.css';

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

// btn.onclick = function() {
//     var div = document.createElement('div');
//     div.innerHTML = 'item';
//     document.body.appendChild(div)
// }

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

而後咱們把webpack.config.js裏面的熱模塊更新的代碼先註釋掉

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const webpack = require('webpack');

module.exports = {
    mode: 'development', 
    devtool: 'heap-module-eval-source-map',                            
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist')
    },
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        // hot: true,               // 啓用 webpack 的模塊熱替換特性
        // hotOnly: true            // 即便HMR功能不生效,也不讓瀏覽器自動刷新
    },
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader', 
                    'postcss-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        // new webpack.HotModuleReplacementPlugin()        // webapck內置插件
    ]
}

從新npm run start,頁面上能夠看到一個1一個1000,咱們點擊1這個地方讓這個數字一直加到某個值如:8,而後咱們回到number.js中,把div.innerHTML = 1000 改成div.innerHTML = 2000,保存,回到頁面,咱們發現以前加到8的數字又從新變回1了,這是由於咱們改了代碼,webpack從新編譯從新刷新頁面了,咱們但願下面這個數字改變了不要影響我上面加好了的數字,如今藉助HMR就能夠實現咱們的目標

如今咱們把webpack.config.js中以前註釋的代碼所有放開,咱們從新npm run bundle;回到頁面,咱們把上面的這個1點成某個值如:10,而後咱們回到number.js中,把div.innerHTML = 2000 改成div.innerHTML = 3000,保存,回到頁面,發現頁面2000並無變成3000,這是由於代碼雖然從新編譯了,可是index.js中number()沒有被從新執行,此時咱們須要在index.js中增長點代碼:
src/index.js

// import './style.css';

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

// btn.onclick = function() {
//     var div = document.createElement('div');
//     div.innerHTML = 'item';
//     document.body.appendChild(div)
// }

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

+ if(module.hot) {
     // 若是number這個文件發生了變化,那麼就會執行後面這個函數,讓number()從新執行下
+    module.hot.accept('./number', () => {
         // 獲取以前的元素,刪除它
+        let abc= document.getElementById('number');
+        document.body.removeChild(abc);
+        number();
+    })         
+  }

作完這步從新npm run start,而後回到頁面,把1點成某個值如:10,而後咱們回到number.js中,把div.innerHTML = 3000 改成div.innerHTML = 4000,保存,回到頁面,此時能夠看到此時3000已經變成4000了,可是上面的10仍是10,沒有變成1,說明熱模塊更新已經成功
那爲何上面的樣式文件的改變,能夠不用寫if(module.hot){...}這樣的代碼,就能達到熱模塊更新的效果呢?這是由於style-loader已經內置了這樣的功能,當更新 CSS 依賴模塊時,此 loader 在後臺使用 module.hot.accept 來修補(patch) style標籤,像其餘loader也有這個功能,好比:vue-loader 此 loader 支持用於 vue 組件的 HMR,提供開箱即用體驗

關於熱模塊替換能夠參考 熱模塊替換官方文檔
module.hot的其餘參數能夠參考 這裏

十3、使用babel處理ES6語法

對以前的項目目錄進行簡化,刪除src下的counter.js, number.js, style.css, 而後把index.js中的內容所有清除
此時目錄結構

webpack-demo
        |- dist
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

而後再在index.js中寫一點ES6的語法
index.js

const arr = [
    new Promise(() => {}),
    new Promise(() => {})
];

arr.map(item => {
    console.log(item)
})

從新npm run start,編譯成功以後,打開console,能夠看到Promise被打印出來了,說明ES6語法運行是沒有任何問題的,這是由於谷歌瀏覽器對ES6語法是支持的,可是有不少低版本瀏覽器好比IE,對ES6是不支持的,咱們就須要把它轉換成ES5語法,要實現這種轉換咱們須要藉助babel

安裝

npm install --save-dev babel-loader @babel/core

安裝完成以後,在webpack.config.js中增長babel配置規則
webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
    module: {
        rules: [
+            { 
+                test: /\.js$/, 
+                exclude: /node_modules/,   // 若是js文件在node_modules裏面,就不使用這個babel-loader了,node_module裏面的js其實是一些第三方代碼,不必對這些代碼進行ES6轉ES5操做
+                loader: "babel-loader" 
+            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            ...
        ]
    },
    ...
}

當咱們使用babel-loader處理js文件的時候,實際上這個babel-loader只是webpack和babel作通訊的一個橋樑,咱們配置了Babel但它並不會幫你把ES6語法翻譯成ES5的語法,因此還須要藉助@babel/preset-env這個模塊
安裝@babel/preset-env

npm install @babel/preset-env --save-dev

而後再在webpack.config.js中從新進行配置

{ 
       test: /\.js$/, 
       exclude: /node_modules/, 
       loader: "babel-loader",
  +     options: {
  +              presets: ["@babel/preset-env"]
  +         } 
       },

而後咱們經過npx webpack進行打包,打包完成以後打開在dist目錄下打開main.js文件,在最下面能夠看到以前寫的ES6語法已經被翻譯成ES5語法了
圖片描述

(一)、@babel/polyfill : ES6 內置方法和函數轉化墊片

可是光作到這樣還不夠,由於像Promise,map這些新的語法變量和方法在低版本瀏覽器中仍是不存在的,因此咱們不只要使用@babel/preset-env作語法上的轉換,還須要把這些新的語法變量和方法補充到低版本瀏覽器裏,這裏咱們藉助@babel/polyfill

使用 @babel/polyfill 的緣由
Babel 默認只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,好比 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局對象,以及一些定義在全局對象上的方法(好比 Object.assign)都不會轉碼。必須使用 @babel/polyfill,爲當前環境提供一個墊片。
所謂墊片也就是墊平不一樣瀏覽器或者不一樣環境下的差別

安裝

npm install --save @babel/polyfill

index.js

+ import "@babel/polyfill";

 const arr = [
    new Promise(() => {}),
    new Promise(() => {})
 ];

 arr.map(item => {
    console.log(item)
 })

沒引進@babel/polyfill以前咱們打包,main.js的大小隻有29.5kb
圖片描述
引進了以後咱們從新npx webpack,打包以後看main.js一下就變成了1.04Mb了
圖片描述
這多的內容就是@babel/polyfill彌補的的一些低版本瀏覽器不存在的內容

咱們如今只用了Promise和map語法,其餘的ES6的語法咱們在這裏並無用到,實際上這樣引入@babel/polyfill,它會把其餘ES6的補充語法一併打包到main.js中了,咱們能夠繼續優化下
webpack.config.js

{ 
          test: /\.js$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader',
          options: {
-                 presets: ['@babel/preset-env']
+                 presets: [['@babel/preset-env', {
+                        useBuiltIns: 'usage'   // 根據業務代碼決定補充什麼內容
+                    }]]
          } 
      },

打包發現報錯了
圖片描述

這裏其實咱們還須要安裝一個core-js,具體緣由能夠參考這裏

npm install --save core-js@3.0.1

安裝完成以後,對presets從新配置

presets: [['@babel/preset-env', {
         useBuiltIns: 'usage',
 +       corejs: 3
        }]]

配置了useBuiltIns: 'usage'了以後,polyfill在須要的時候會自動導入,因此能夠把全局引入的這段代碼註釋掉了

// 全局引入
// import "@babel/polyfill";

從新npx webpack,發現打包出的main.js體積小了很多
圖片描述

presets 裏面還能夠配置targets參數

{ 
          test: /\.js$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader',
          options: {
                   presets: [
                        ['@babel/preset-env', {
       +                    targets: {
       +                         chrome: "67"
       +                    },
                            useBuiltIns: 'usage',
                            corejs: 3
                    }]
                ]
           }
     },

這段代碼意思是webpack打包的時候會判斷Chrome瀏覽器67以上的版本是否兼容ES6,若是兼容它打包的時候就不會作ES6轉ES5,若是不兼容就會對ES6轉ES5操做

如今驗證下,我用的谷歌版本是73.0.3683.103,是兼容ES6新的api的,因此它不會經過@babel/polyfill對這些新的api進行轉化了,從新npx webpack,能夠看到由於沒有用到@babel/polyfill,打包體積又變回了以前的29.6kb了
圖片描述
打開dist目錄下的main.js,到最下面能夠看到webapck確實沒有對Promise和map這些ES6語法進行轉化

@babel/polyfill的詳細介紹能夠參考 官網

(二)、@babel/plugin-transform-runtime : 避免 polyfill 污染全局變量,減少打包體積

可是這樣配置也不是全部的場景都適用的,好比你在開發一個類庫或者開發一個第三方模塊或者組件庫的時候,實際上用@babel/polyfill這種方案是有問題的,由於它在注入這些Promise和map方法的時候,它會經過全局變量的形式注入,會污染全局環境,因此咱們須要換一種配置方式,使用@babel/plugin-transform-runtime

使用 @babel/plugin-transform-runtime 的緣由
Babel 使用很是小的助手來完成常見功能。默認狀況下,這將添加到須要它的每一個文件中。這種重複有時是沒必要要的,尤爲是當你的應用程序分佈在多個文件上的時候。 transform-runtime 能夠重複使用 Babel 注入的程序代碼來節省代碼,減少體積。

index.js中咱們註釋掉import "@babel/polyfill"這段代碼

// import "@babel/polyfill";

const arr = [
    new Promise(() => {}),
    new Promise(() => {})
];

arr.map(item => {
    console.log(item)
})

安裝

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

安裝完成以後咱們在webpack.config.js從新進行配置

{ 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: 'babel-loader',
        options: {
               // presets: [
               //     ['@babel/preset-env', {
               //         targets: {
               //             chrome: "67"
               //         },
               //         useBuiltIns: 'usage',
               //         corejs: 3
               //     }]
               // ]
+              'plugins': [
+                     ['@babel/plugin-transform-runtime', {
+                         'absoluteRuntime': false,
+                         'corejs': 2,
+                         'helpers': true,
+                         'regenerator': true,
+                         'useESModules': false
+                    }]
+                ]
           } 
   },

打包npx webpack,發現報錯了,這是由於咱們配置了'corejs': 2,因此還須要額外安裝一個包

npm install --save @babel/runtime-corejs2

安裝完成以後,從新npx webpack,這樣打包就沒有任何問題了

注意: 若是你寫的只是業務代碼的時候,那你配置的時候只須要配置presets:[['@babel/preset-env',{...}]]這段代碼,而且在業務代碼前面引入import "@babel/polyfill"就能夠了;
若是你寫的是一個庫相關的代碼的時候,你須要使用@babel/plugin-transform-runtime這個插件,它的好處是不會污染全局環境,因此當你寫類庫的時候不去污染全局環境是一個更好的方案

@babel/plugin-transform-runtime的詳細介紹能夠參考官網

知識補充點:
咱們看到babel對應的配置項會很是多,也很是長,咱們能夠在根目錄下建立一個.babelrc文件,而後把options對應的這個對象剪切到.babelrc文件中
.babelrc

{
    "plugins": [
        ["@babel/plugin-transform-runtime", {
            "absoluteRuntime": false,
            "corejs": 2,
            "helpers": true,
            "regenerator": true,
            "useESModules": false
        }]
    ]
}

而後去掉webpack.config.js中的options

{ 
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: 'babel-loader',
   -            options: {
   -                 // presets: [
   -                 //     ['@babel/preset-env', {
   -                 //         targets: {
   -                 //             chrome: '67'
   -                 //         },
   -                 //         useBuiltIns: 'usage',
   -                 //         corejs: 3
   -                 //     }]
   -                 // ]
   -                 'plugins': [
   -                     ['@babel/plugin-transform-runtime', {
   -                         'absoluteRuntime': false,
   -                         'corejs': 2,
   -                         'helpers': true,
   -                         'regenerator': true,
   -                         'useESModules': false
   -                     }]
   -  
   -                 ]
   -             } 
            },

保存,從新打包npx webpack,能夠看到依然能夠正常打包
此時目錄結構爲

webpack-demo
        |- dist
          |- index.html
          |- main.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

十4、webpack實現對React框架代碼的打包

首先作一些回退處理,咱們如今是寫的業務代碼,因此在babelrc文件配置@babel/preset-env

{
-    "plugins": [
-        ["@babel/plugin-transform-runtime", {
-            "absoluteRuntime": false,
-            "corejs": 2,
-            "helpers": true,
-            "regenerator": true,
-            "useESModules": false
-        }]
-    ]

+    "presets": [
+        ["@babel/preset-env", {
+            "targets": {
+                "chrome": "67"
+            },
+            "useBuiltIns": "usage",
+            "corejs": 3
+        }]
+    ]
}

而後安裝React包

npm install react react-dom --save

index.js

import React, {Component} from 'react';
import ReactDom from 'react-dom';

class App extends Component {
    render() {
        return <div>Hello World</div>
    }
}

ReactDom.render(<App />, document.getElementById('root'))

執行npm run start,而後打開頁面控制檯,發現頁面報錯,實際上是瀏覽器不識別React這種jsx語法,咱們咱們須要藉助@babel/preset-react這個工具來實現對React的打包
安裝

npm install --save-dev @babel/preset-react

安裝完成以後,在babelrc中進行配置
.babelrc

{
    "presets": [
        ["@babel/preset-env", {
            "targets": {
                "chrome": "67"
                },
            "useBuiltIns": "usage",
            "corejs": 3
            }
        ],
+       "@babel/preset-react"
    ]
}

從新npm run start,此時頁面顯示正常了

@babel/preset-react的詳細介紹能夠參考 官網

十5、Tree Shaking

tree shaking 是一個術語,一般用於描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴於 ES2015 模塊系統中的靜態結構特性,例如 import 和 export。這個術語和概念其實是興起於 ES2015 模塊打包工具 rollup。

新的 webpack 4 正式版本,擴展了這個檢測能力,經過 package.json 的 "sideEffects" 屬性做爲標記,向 compiler 提供提示,代表項目中的哪些文件是 "pure(純的 ES2015 模塊)",由此能夠安全地刪除文件中未使用的部分。

(一)、JS Tree Shaking

在咱們的項目中添加一個新的通用模塊文件 src/math.js,此文件導出兩個函數:

webpack-demo
        |- dist
          |- index.html
          |- main.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
+          |- math.js
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

math.js

export const add = (a, b) => {
    console.log(a + b)
}

export const minus = (a, b) => {
    console.log(a - b)
}

index.js

import { add } from './math.js';

add(1,2)

而後打包npx webpack,在控制檯能夠看到輸出3,說明代碼已經正確運行了,實際上在index.js裏面咱們引入了add方法,可是咱們並無引入minus方法,可是在打包的時候能夠看到在main.js中webpack不只把add方法打包進來了,還把minus方法也打包進來
圖片描述
咱們的業務代碼中實際上只用到了add方法,若是把minus方法也打包進來是沒有必要的,會使咱們的main.js文件變的很大,最理想的打包方式是咱們引入什麼就幫咱們打包什麼,因此咱們須要藉助tree shaking功能

注意:tree shaking只支持ES Module這種模塊的引入,若是使用這種CommonJs的引入方式require('./math.js'),tree shaking是不支持的

在development模式下,默認是沒有tree shaking這個功能,要想加上須要這樣配置

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
    mode: 'development', 
    devtool: 'heap-module-eval-source-map',                            
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true,               // 啓用 webpack 的模塊熱替換特性
        hotOnly: true            // 即便HMR功能不生效,也不讓瀏覽器自動刷新
    },
    module: {
        rules: [
            { 
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: 'babel-loader'
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader', 
                    'postcss-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
+   optimization: {
+       usedExports: true
+   },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new webpack.HotModuleReplacementPlugin()        // webapck內置插件
    ]
}

接着在package.json裏面加上sideEffects屬性爲false,意思是tree shaking對全部模塊都作tree shaking,沒有要特殊處理的東西

{
  "name": "webpack-demo",
+ "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "start": "webpack
    -dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.4.3",
    "@babel/plugin-transform-runtime": "^7.4.3",
    "@babel/preset-env": "^7.4.3",
    "@babel/preset-react": "^7.0.0",
    "autoprefixer": "^9.5.1",
    "babel-loader": "^8.0.5",
    "clean-webpack-plugin": "^2.0.1",
    "css-loader": "^2.1.1",
    "express": "^4.16.4",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-middleware": "^3.6.2",
    "webpack-dev-server": "^3.3.1"
  },
  "dependencies": {
    "@babel/polyfill": "^7.4.3",
    "@babel/runtime": "^7.4.3",
    "@babel/runtime-corejs2": "^7.4.3",
    "core-js": "^3.0.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

可是假如咱們引入了import "@babel/polyfill"這樣的包,就須要特殊處理了。這個模塊實際上並無導出任何的內容,在它的內部其實是在window對象上綁定了一些全局變量,好比說Promise(window.promise)這些東西,因此它沒有直接導出模塊,若是用了tree shaking,發現這個模塊沒有導出任何內容,就會打包的時候直接把這個@babel/polyfill給忽略掉了,可是咱們又是須要這個模塊的,因此打包的時候就會出問題了,因此咱們須要對這樣的模塊作一個特殊的設置,若是不但願對@babel/polyfill這樣的模塊進行tree shaking,咱們能夠在package.json中這樣設置「sideEffects」: ["@babel/polyfill"]

除了@babel/polyfill這樣的文件須要特殊處理外,還有咱們引入的css文件(如:import './style.css'),實際上只要引入一個模塊,tree shaking就會去看這個模塊導出了什麼,你引入了什麼,若是沒有用到的打包的時候就會幫你忽略掉,style.css顯然沒有導出任何內容,若是這樣寫,tree shaking解析的時候就會把這個樣式忽略掉,樣式就不會生效了,因此咱們還須要這樣添加「sideEffects」: ["*.css"],意思是任何的css文件都不要tree shaking

對於上面這段話的實踐驗證:
development模式下,無論設置"sideEffects": false 仍是 "sideEffects": ["*.css"],style.css都不會被tree shaking,頁面樣式仍是會生效,結論就是,開發模式下,對於樣式文件tree shaking是不生效的
production模式下,"sideEffects": false頁面樣式不生效,說明樣式文件被tree shaking了;而後設置"sideEffects": ["*.css"]頁面樣式生效,說明樣式文件沒有被tree shaking,結論就是,生產模式下,對於樣式文件tree shaking是生效的

配置好了以後從新npx webpack,而後打開main.js能夠看到minus方法任然被打包進來,那是否是tree shaking沒有生效呢?其實它已經生效了,咱們往上面看,能夠看到這樣的一句話
圖片描述
它的意思是這個模塊提供了兩個方法,可是隻有一個add方法被使用了,使用了tree shaking的webpack打包的時候已經知道哪些方法被使用了,故做出這樣的提示,那爲何沒有幫咱們把沒有用到的代碼去除掉呢? 這是由於在development模式下,咱們可能須要作一些調試,若是刪除掉了,那咱們作調試的時候可能就找不到具體位置了,因此開發環境下,tree shaking還會保留這些無用代碼

若是是production環境下,咱們對webpack.json.js文件進行調整下

module.exports = {
    // mode: 'development', 
    // devtool: 'cheap-module-eval-source-map',  
+   mode: 'production', 
+   devtool: 'cheap-module-source-map',  
   ...
    // optimization: {   //  在production模式下,tree shaking一些配置自動就配置好了,因此這裏不須要寫了
    //     usedExports: true
    // },
    ...
}

從新npx webapck,打開main.js,由於是線上代碼webpack作了壓縮,咱們搜索console.log能夠看到只能搜到一個,說明webpack去掉了minus方法
圖片描述

如何處理第三方JS庫?
對於常用的第三方庫(例如 jQuery、lodash 等等),如何實現 Tree Shaking ?
以lodash.js爲例,進行介紹
安裝lodash.js

npm install lodash --save

index.js

import { add } from './math.js';


+  import { chunk } from 'lodash'
+  console.log(chunk([1, 2, 3], 2))


add(1, 2)

執行npx webpack,以下圖所示,打包後大小爲77.3kb,顯然只引用了一個函數,不該該這麼大。並無進行tree shaking
圖片描述
開頭講過,js tree shaking 利用的是 ES 的模塊系統。而 lodash.js 沒有使用 CommonJS 或者 ES6 的寫法。因此,安裝對應的模塊系統便可

安裝 lodash.js 的 ES 寫法的版本:

npm install lodash-es --save

修改下index.js

import { add } from './math.js';

- import { chunk } from 'lodash'
+ import { chunk } from 'lodash-es'
console.log(chunk([1, 2, 3], 2))

add(1, 2)

再次npx webpack,只有1.04kb了,顯然,tree shaking成功
圖片描述

友情提示:
在一些對加載速度敏感的項目中使用第三方庫,請注意庫的寫法是否符合 ES 模板系統規範,以方便 webpack 進行 tree shaking。

(二)、CSS Tree Shaking

在src中新增一個style.css文件
style.css

.box {
  height: 200px;
  width: 200px;
  border-radius: 3px;
  background: green;
}

.box--big {
  height: 300px;
  width: 300px;
  border-radius: 5px;
  background: red;
}

.box-small {
  height: 100px;
  width: 100px;
  border-radius: 2px;
  background: yellow;
}

index.js

import { add } from './math.js';
+  import './style.css';

-  import { chunk } from 'lodash-es'
-  console.log(chunk([1, 2, 3], 2))

+  var root = document.getElementById('root')
+  var div = document.createElement('div')
+  div.className = 'box'
+  root.appendChild(div)

   add(1, 2)

PurifyCSS 將幫助咱們進行 CSS Tree Shaking 操做。爲了能準確指明要進行 Tree Shaking 的 CSS 文件,還有 glob-all (另外一個第三方庫)。 glob-all 的做用就是幫助 PurifyCSS 進行路徑處理,定位要作 Tree Shaking 的路徑文件。
安裝依賴

npm install glob-all purify-css purifycss-webpack --save-dev

爲了配合PurifyCSS 這個插件,咱們還須要額外安裝一個mini-css-extract-plugin這個插件

npm install --save-dev mini-css-extract-plugin
mini-css-extract-plugin更多參數配置請參考 這裏

而後更改配置文件

+  const MiniCssExtractPlugin = require('mini-css-extract-plugin')       // 默認打包後只能插入<style>標籤內,這個插件能夠將css單獨打包成文件,以<link>形式引入
+  const PurifyCSS = require('purifycss-webpack');
+  const glob = require('glob-all');

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new webpack.HotModuleReplacementPlugin(),  
+       new MiniCssExtractPlugin({
+           filename: '[name].css'                         // 打包後的css文件名
+       }),     
+       new PurifyCSS({
+           paths: glob.sync([
               // 要作CSS TreeShaking的文件
+               path.resolve(__dirname, './src/*.js')
+           ])
+       })
    ]
}

打包完以後,檢查dist/main.css能夠看到沒有被使用的類樣式(box-big和box-small)就沒有被打包進去
圖片描述

警告
若是項目中有引入第三方 css 庫的話,謹慎使用

Tree Shaking部份內容引用這裏

此時項目結構爲:

webpack-demo
        |- dist
          |- index.html
          |- main.css
          |- main.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
          |- math.js
          |- style.css
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- webpack.config.js

十6、Development和Production模式的區分打包

開發環境(development)和生產環境(production)的構建目標差別很大。在開發環境中,咱們須要具備強大的、具備實時從新加載(live reloading)或熱模塊替換(hot module replacement)能力的 source map 和 localhost server。而在生產環境中,咱們的目標則轉向於關注更小的 bundle,更輕量的 source map,以及更優化的資源,以改善加載時間。因爲要遵循邏輯分離,咱們一般建議爲每一個環境編寫彼此獨立的 webpack 配置。

如今作一些回退處理,刪除src中的style.css,而且刪除index.js中的部份內容,而後對配置文件進行調整
index.js

import { add } from './math.js';
-  import './style.css';

-   var root = document.getElementById('root')
-   var div = document.createElement('div')
-   div.className = 'box'
-   root.appendChild(div)

    add(1, 2)

webpack.config.js

const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    const webpack = require('webpack');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')   // 這個插件能夠保留
-   const PurifyCSS = require('purifycss-webpack');
-   const glob = require('glob-all');

module.exports = {
    mode: 'development', 
    devtool: 'cheap-module-eval-source-map',  
    // mode: 'production', 
    // devtool: 'cheap-module-source-map',  
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        ...      
    },
    module: {
        ...
    },
    optimization: {
         usedExports: true
    },
    plugins: [
        ... 
        new MiniCssExtractPlugin({              // 這個插件保留
            filename: '[name].css'                       
        }),     
-       new PurifyCSS({
-           paths: glob.sync([
-               path.resolve(__dirname, './src/*.js')
-           ])
-       })
    ]
}

回退處理完成以後,咱們將webpack.config.js重命名爲webpack.dev.js,讓它做爲開發環境下的配置文件,而後再在同級目錄新建一個webpack.prod.js文件,讓其做爲生產環境下的配置文件,而後將webpack.dev.js文件中的所有內容拷貝一份webpack.prod.js中,並作一些刪減
webpack.prod.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')   

module.exports = {
 -   mode: 'development', 
 -   devtool: 'cheap-module-eval-source-map',  
     mode: 'production', 
     devtool: 'cheap-module-source-map',  
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, 'dist'),
    },
-   devServer: {
-       contentBase: './dist',
-       open: true,
-       port: 8080,
-       proxy: {
-           '/api:': 'http://localhost:3000'
-       },
-       hot: true,             
-       hotOnly: true        
-   },
    module: {
        rules: [
            { 
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: 'babel-loader'
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    // 'style-loader',                          
                    'css-loader', 
                    'postcss-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
-   optimization: {
-       usedExports: true
-   },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
-       new webpack.HotModuleReplacementPlugin(),  
        new MiniCssExtractPlugin({
            filename: '[name].css'                       
        })
    ]
}

接下來,咱們打開package.json文件,作一些調整

{
  "name": "webpack-demo",
  "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
-    "start": "webpack-dev-server"
+    "dev": "webpack-dev-server --config webpack.dev.js",
+    "build": "webpack --config webpack.prod.js" 
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}

意思是若是執行npm run dev命令,那麼就運行webpack.dev.js這個配置文件,若是執行npm run build命令,那就運行webpack.prod.js這個配置文件

如今驗證下,運行npm run dev,打包成功webpack幫咱們打開一個localhost:8080這個地址,查看控制檯輸出3,而後咱們把src/index.js中的add(1, 2)改成add(1, 4),保存,返回瀏覽器收到刷新頁面,而後查看控制檯,發現打印出了5,若是咱們不想手動刷新能夠在webpack.dev.js中將hotOnly:true刪掉,而後從新npm run dev下

devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true,             
-       hotOnly: true        
    }

若是咱們代碼須要打包上線了,咱們須要在命令行運行npm run build,打包完成以後,在dist目錄中咱們能夠看到main.js已是壓縮過的文件了,咱們把這個文件夾丟到線上給後端使用就能夠了

可是咱們發現,這兩個文件中還存在不少重複的代碼,咱們須要繼續優化下,新建一個通用配置,爲了將這些配置合併在一塊兒,咱們將使用一個名爲 webpack-merge 的工具。經過「通用」配置,咱們沒必要在環境特定(environment-specific)的配置中重複代碼。

安裝webpack-merge

npm install --save-dev webpack-merge

而後在同級目錄新建一個webpack.common.js的配置文件,而後把那兩個文件中公用的代碼都提取到這個文件中來
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',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, '../dist'),
    },
    module: {
        rules: [
            { 
                test: /\.js$/, 
                exclude: /node_modules/, 
                loader: 'babel-loader'
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'images/',
                            limit: 2048
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    // 'style-loader',                          
                    'css-loader', 
                    'postcss-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                use: ['file-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new MiniCssExtractPlugin({
            filename: '[name].css'                       
        })
    ]
}

而後再在這兩個文件中經過webpack-merge這個插件對通用配置進行合併
webpack.dev.js

const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
    mode: 'development', 
    devtool: 'cheap-module-eval-source-map',  
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true          
    },
    optimization: {
        usedExports: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
}

module.exports = merge(commonConfig, devConfig);

webpack.prod.js

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
    mode: 'production', 
    devtool: 'cheap-module-source-map'
}

module.exports = merge(commonConfig, prodConfig);

而後執行npm run dev,打包沒有問題,再去執行npm run build,打開dist目錄下的index.html文件,也沒有問題,說明咱們合併成功

爲了文件目錄簡潔,咱們在webpack-demo目錄下新建一個build文件夾,而後把這三個文件移到這個文件夾中,而後從新修改下packgae.json中的路徑

{
  "name": "webpack-demo",
  "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
-    "dev": "webpack-dev-server --config webpack.dev.js",
-    "build": "webpack --config webpack.prod.js"
+    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
+    "build": "webpack --config ./build/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}

從新驗證,都是沒問題的
此時項目結構以下:

webpack-demo
        |- build
          |- webpack.common.js
          |- webpack.dev.js
          |- webpack.prod.js
        |- dist
          |- index.html
          |- main.js
          |- main.js.map
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
          |- math.js
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js

十7、Code Splitting

代碼分離是 webpack 中最引人注目的特性之一。此特性可以把代碼分離到不一樣的 bundle 中,而後能夠按需加載或並行加載這些文件。代碼分離能夠用於獲取更小的 bundle,以及控制資源加載優先級,若是使用合理,會極大影響加載時間

(一)、同步代碼code splitting

爲了能檢查打包後的文件內容,咱們在package.json中新增一個命令

{
  "name": "webpack-demo",
  "sideEffects": false,
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
+   "dev-build": "webpack --config ./build/webpack.dev.js",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}

而後咱們作一些回退處理,如今刪除掉src目錄下的math.js文件而且去掉index.js文件中的內容,而後去下載一個第三方庫lodash

npm install lodash --save

在index.js中添加以下代碼
index.js

import _ from 'lodash';

console.log(_.join(['a', 'b', 'c']));

執行命令npm run dev-build,打包成功以後打開dist/index.html文件,檢查控制檯輸出正常
圖片描述

假設index.js中的業務邏輯很是長

import _ from 'lodash';

console.log(_.join(['a', 'b', 'c'], '***'));
// 此處省略幾千行業務邏輯...
console.log(_.join(['a', 'b', 'c'], '***'));

咱們引入了一個工具庫(假設有1MB),同時下面又有幾千行的業務邏輯(假設也有1MB), webpack打包的時候會統一打包到main.js(如今有2MB),這樣勢必形成打包後的文件很是大,加載時間會很長,這樣還會帶來另一個問題,若是咱們修改了咱們的業務邏輯,而後從新打包。打包出一個新的2MB的main.js,瀏覽器每次打開頁面,都要先加載 2M 的文件,才能顯示業務邏輯,這樣會使得加載時間變長,那有沒有辦法去解決這個問題呢?

在src中新建一個lodash.js

import _ from 'lodash';

// 把lodash掛載到全局window上面
window._ = _;

index.js

- import _ from 'lodash';

  console.log(_.join(['a', 'b', 'c'], '***'));
  // 此處省略幾千行業務邏輯...
  console.log(_.join(['a', 'b', 'c'], '***'));

webpack.common.js

module.exports = { 
    entry: {
 +       lodash: './src/lodash.js',
         main: './src/index.js'
    },                        
    ...
}

從新運行npm run dev-build,在dist目錄中能夠看到打包拆分出來的兩個js文件(分別是1MB),而後打開dist中的index.html,如今瀏覽器就能夠並行加載這兩個文件了,這樣比一次性加載2MB的main.js性能會好點,另外這樣打包還有一個好處,就是假如咱們的業務邏輯作了變動,如今只須要從新加載main.js就行了,而lodash.js基本不會變動,直接從瀏覽器緩存中取,這樣能夠提高加載速度
圖片描述

上面是咱們本身作的代碼分割,其實webpack經過splitChunksPlugins就能夠幫咱們作code splitting

在 webpack4 以前是使用 commonsChunkPlugin 來拆分公共代碼,v4 以後被廢棄,並使用 splitChunksPlugin,在使用 splitChunksPlugin 以前,首先要知道 splitChunksPlugin 是 webpack 主模塊中的一個細分模塊,無需 npm 引入

咱們刪除掉src/lodash.js文件,而後把index.js還原

import _ from 'lodash';

console.log(_.join(['a', 'b', 'c'], '***'));

而後點開webpack.common.js,加上以下代碼

module.exports = { 
    entry: {
 -      lodash: './src/lodash.js',
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js',                      
        path: path.resolve(__dirname, '../dist'),
    },
    ...
    plugins: [
        new HtmlWebpackPlugin(
            {
                template: 'src/index.html',
                title: 'test App',
                filename: 'index.html',
                minify: {
                    collapseWhitespace: true
                }
            }
        ), 
        new CleanWebpackPlugin(), 
        new MiniCssExtractPlugin({
            filename: '[name].css'                       
        })
    ],
+   optimization: {
+       splitChunks: {
+           chunks: 'all'   // 分割全部代碼包括同步代碼和異步代碼,默認chunks:'async'分割異步代碼
+       }
+   }
}

從新npm run dev-build,打包成功以後,能夠看到dist中生成了兩個js文件
圖片描述
點開這個vendors~main.js,在最上面能夠看到它把lodash這個工具庫單獨提取出來了,之前咱們是本身手動提取,如今咱們經過webapck一個簡單的配置,它會自動幫咱們去作代碼分割

(一)、異步代碼code splitting

在index.js中咱們不只能夠作同步模塊的引入還能夠作異步模塊的引入
index.js

function getComponent() {
    // 異步加載lodash
    return import('lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}


getComponent().then(ele => {
    document.body.appendChild(ele);
})

從新npm run dev-build,發現打包報錯了
圖片描述

dynamicImport 仍是實驗性的語法,webpack 不支持,咱們須要藉助babel的插件進行轉換

安裝

npm install babel-plugin-dynamic-import-webpack -D
關於這個插件能夠參考 這裏

安裝完成以後再在babelrc文件中進行配置便可

{
    "presets": [
        ["@babel/preset-env", {
            "targets": {
                "chrome": "67"
                },
            "useBuiltIns": "usage",
            "corejs": 3
            }
        ],
        "@babel/preset-react"
    ],
+   "plugins": ["dynamic-import-webpack"]
}

從新npm run dev-build,能夠看到打包成功了,打開index.html文件顯示正常

這裏分割出0.js,0是以id編號來命名
圖片描述
點開這個0.js,在最上面依然能夠看到它把lodash這個工具庫單獨提取出來了

十8、SplitChunksPlugin配置參數詳解

上一節最後,咱們能夠看到打包出來的是0.js,那咱們可不能夠對這個名字重命名呢?

在這種異步加載的代碼中咱們有一種語法,叫作「魔法註釋」,請看下面具體寫法

function getComponent() {
    // 異步加載lodash
    // 意思是異步引入lodash,當作代碼分割的時候,給這個lodash庫單獨進行打包的時候,給它起的名字叫lodash
   return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}

getComponent().then(ele => {
    document.body.appendChild(ele);
})

從新打包npm run dev-build,發現打包出來的仍是0.js,這是爲何呢?這是由於咱們以前配置的這個插件"plugins": ["dynamic-import-webpack"],並非官方的插件,它不支持這種「魔法註釋」的寫法,還如今該怎麼呢?

最簡單的就是不使用這個插件了,取而代之咱們去使用babel官方提供的另外一個插件
安裝

npm install --save-dev @babel/plugin-syntax-dynamic-import
插件詳情請見 官網

安裝好以後,對babelrc文件進行調整

{
    "presets": [
        ["@babel/preset-env", {
            "targets": {
                "chrome": "67"
                },
            "useBuiltIns": "usage",
            "corejs": 3
            }
        ],
        "@babel/preset-react"
    ],
-   "plugins": ["dynamic-import-webpack"]
+   "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

從新npm run dev-build,打包成功以後到dist目錄中看到名字已經變成了vendors~lodash.js了
圖片描述

若是想讓打包出來的文件就叫lodash,咱們須要在webpack.common.js中改變下配置

optimization: {
        splitChunks: {
            chunks: 'all',
 +          cacheGroups: {
 +              vendors: false,
 +              default: false
 +          }
        }
    }

從新打包,能夠看到打包生成的文件就叫lodash了
圖片描述

如今咱們將optimization.splitChunks配成一個空對象

optimization: {
        splitChunks: {
           
        }
    }

而後保存從新打包,能夠看到打包依然能夠成功運行,只不過lodash名字前面多了一個vendors
圖片描述

這是由於若是沒有配置任何內容的時候,它會走它內部默認的一套配置流程,具體默認配置參數見下

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
        }
      }
    }
  }
};

接下來,咱們將這些默認配置項配置進webpack.common.js中的optimization.splitChunks中
從新npm run dev-build,打包依然正常,接下來咱們一項項解釋這些參數有什麼做用
首先,咱們將cacheGroups.vendors和cacheGroups.default都配置成false

optimization: {
        splitChunks: {
            chunks: 'async',
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors: false,
              default: false
            }
        }
    }

(1)、splitChunks.chunks

默認是‘async’,意思是作代碼分割的時候只對異步代碼生效。當是字符串時,有效值還能夠設置爲all和initial

咱們把src/index.js中的異步代碼先註釋,而後同步引入lodash
index.js

// function getComponent() {
//     // 異步加載lodash
//     // 意思是異步引入lodash,當作代碼分割的時候,給這個lodash庫單獨進行打包的時候,給它起的名字叫lodash
//     return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
//         var ele = document.createElement('div');
//         ele.innerHTML = _.join(['Hello', 'World'], '-');
//         return ele;
//     })
// }

// getComponent().then(ele => {
//     document.body.appendChild(ele);
// })

// 同步引入lodash
import _ from 'lodash';
var ele = document.createElement('div');
ele.innerHTML = _.join(['Hello', 'World'], '-');
document.body.appendChild(ele);

從新npm run dev-build,打包完成能夠看到dist目錄中並無打包出lodash.js文件,說明對同步代碼沒有分割成功
圖片描述

當把chunks設置爲all的時候,意思是對同步代碼和異步代碼均可以作代碼分割,咱們看是否是這樣的

optimization: {
        splitChunks: {
            chunks: 'all',
            ...
            cacheGroups: {
              vendors: false,
              default: false
            }
        }
    }

改好以後從新打包,打包完成咱們發現dist目錄中仍是沒有生成lodash.js,說明代碼分割沒有生效,這是爲何呢?是由於咱們還須要對cacheGroups作一些額外配置

optimization: {
    splitChunks: {
      chunks: 'all',
      ...
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: false
      }
    }
  }

當引入同步的lodash庫的時候,設置爲all後webpack知道要對同步代碼作分割了,可是它會繼續往下走,走到cacheGroups的時候,它裏面還有個vendors.test配置項,這個配置項會去檢測你引入的這個庫是否是在node_modules中,很顯然咱們引入的這個庫是經過npm安裝的,確定在node_modules中,那它就符合test這個配置項的要求,因而它會單獨把這個lodash打包到vendors這個組裏面去
從新再去npm run dev-build,在dist目錄中能夠看到它此時已經生成了一個vendors~main.js文件了
圖片描述
文件前面的vendors指的就是這個庫文件符合這個組的要求,因此生成的文件會加上這個組的名字
文件後面的main指的就是入口文件的名字

有的時候咱們但願打包出來的文件名不要加上main這個入口名字了,直接把全部引入的庫打包到vendors.js這個文件裏面,咱們能夠對vendors這樣配置

cacheGroups: {
      vendors:  {
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
 +          filename: 'vendors.js'
         },
      default: false
}

保存從新打包,如今能夠看到名字就叫vendors.js了
圖片描述

當咱們把chunks設置成async或all的時候,讓它去處理異步代碼而且異步代碼中不經過魔法註釋去自定義名字

function getComponent() {
    return import('lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}
getComponent().then(ele => {
    document.body.appendChild(ele);
})

// 同步引入lodash
// import _ from 'lodash';
// var ele = document.createElement('div');
// ele.innerHTML = _.join(['Hello', 'World'], '-');
// document.body.appendChild(ele);

而是在vendors.filename中配置自定義名字,發現打包都會報一樣的錯
圖片描述

從上面這些例子中得出結論:
一、 chunks無論設置成什麼,webpack作代碼分割的時候,都會去匹配cacheGroup這個配置項
二、 chunks設置成async或者all的時候,去處理異步代碼,若是想自定義打包後的名字只能經過魔法註釋,若是想讓打包出來的名字不帶vendors,能夠把venders設置成false,意思是不讓webpack去配置cacheGroup.vendors這個配置項
三、 chunks設置成all的時候,去處理同步代碼,必需要給vendors設置配置項,不能是false,不然沒法打包出文件,若是還想自定義打包後的名字只能經過vendors.filename來配置

(2)、splitChunks.minSize

意思是超過多少大小就進行壓縮,默認是30000即30kb

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 300000000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: false
            }
        }
    }

若是咱們把minSize改到很是大,lodash這個庫大小確定是小於這個值的,從新npm run dev-build,發現dist目錄中並無幫咱們把lodash分割出來,
圖片描述

咱們從新再舉一個例子,如今咱們在src中新建一個test.js
test.js

export default {
    name: 'Hello world'
}

index.js

// function getComponent() {
//     return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
//         var ele = document.createElement('div');
//         ele.innerHTML = _.join(['Hello', 'World'], '-');
//         return ele;
//     })
// }

// getComponent().then(ele => {
//     document.body.appendChild(ele);
// })


// 同步引入lodash
// import _ from 'lodash';
// var ele = document.createElement('div');
// ele.innerHTML = _.join(['Hello', 'World'], '-');
// document.body.appendChild(ele);

import test from './test.js';
console.log(test.name)

而後從新把minSize改回爲默認值30000,咱們本身寫的這個模塊是很是小的,估計連1kb都不到,它打包的時候是不會進行代碼分割的,咱們驗證下,從新npm run build,能夠看到dist目錄中果真沒有幫咱們作分割
圖片描述
那若是咱們把minSize改成0

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 0,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: false
            }
        }
    }

咱們寫的這個模塊大小大於0,照理說應該會進行代碼分割,咱們看是否是這樣的?
從新npm run dev-build,咱們在dist目錄中仍然仍是沒有看到分割出來的代碼,這是爲何呢?
緣由是當咱們引入這個test模塊的時候,它已經符合這個mixSize大於0的要求了,webpack已經知道要對它進行代碼分割,可是它會繼續往下走,走到cacheGroups的時候,會去匹配vendors中的test規則,發現這個test模塊並不在node_modules中,既然沒法匹配這個規則因此打包後的文件不會放到vendors.js中去,要放到哪裏去webpack本身就不知道而此時default咱們又配置的是false,它連默認放到哪裏都不知道

如今咱們對default這個配置項從新配置下

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 0,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true
              }
            }
        }
    }

保存從新打包,打包以後能夠看到它會把這個模塊放到以default這個組名字開頭的文件裏
圖片描述
咱們也能夠在default裏自定義打包後的名字

default: {
    // minChunks: 2,
    priority: -20,
    reuseExistingChunk: true,
+   filename: 'common.js'
}

從新打包,這個test模塊就會被打包進default這個組裏面對應的common.js文件裏面
圖片描述

(3)、splitChunks.maxSize

使用maxSize告訴webpack嘗試將大於maxSize的塊拆分紅更小的部分。拆解後的文件最小值爲minSize,或接近minSize的值。這樣作的目的是避免單個文件過大,增長請求數量,達到減小下載時間的目的,可是通常這個值咱們不會配置

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
-           maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        }
    }

(4)、splitChunks.minChunks

指的是當一個模塊被用了多少次的時候,纔對它進行代碼分割

如今咱們把這個值改成2,可是咱們在index.js中只引用了一次,因此按理是不會進行代碼分割的,咱們驗證下
index.js

// function getComponent() {
//     return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
//         var ele = document.createElement('div');
//         ele.innerHTML = _.join(['Hello', 'World'], '-');
//         return ele;
//     })
// }

// getComponent().then(ele => {
//     document.body.appendChild(ele);
// })


// 同步引入lodash
import _ from 'lodash';
var ele = document.createElement('div');
ele.innerHTML = _.join(['Hello', 'World'], '-');
document.body.appendChild(ele);

// import test from './test.js';
// console.log(test.name)

從新npm run dev-build,打包完成查看dist目錄,果真就沒有幫咱們作代碼分割了
圖片描述

(5)、splitChunks.maxAsyncRequests

默認是5,指的是同時加載的模塊數最大是5個

(6)、splitChunks.maxInitialRequests

指入口文件的最大並行請求數,意思是入口文件引入的庫若是作代碼分割也最多隻能分割出3個js文件,超過3個就不會作代碼分割了,這些配置通常按照默認配置來便可

(7)、splitChunks.automaticNameDelimiter

意思是打包生成後的文件中間使用什麼鏈接符

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '+',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        }
    }

從新打包,能夠看到文件中間的~就變成了+了
圖片描述

驗證完以後將automaticNameDelimiter: '+'從新改成automaticNameDelimiter: '~'

(7)、splitChunks.name

配置true,意思是將根據塊和緩存組密鑰自動生成名稱,通常採用默認值

(8)、splitChunks.cacheGroups

咱們會根據cacheGroups來決定要分割出來的代碼到底放到哪一個文件裏

假如咱們同時引入了一個lodash和一個jquery,若是沒有這個cacheGroups,那麼代碼打包的時候會發現lodash大於30kb要作代碼分割,會生成一個lodash這樣的文件,而後jquery也大於30kb也要作代碼分割,會生成一個jquery這樣的文件,若是咱們要把這兩個文件放到一塊兒單獨生成一個vendors.js文件,沒有cacheGroups就作不到了,它至關於一個緩存組,打包jquery的時候,先把這個文件放到組裏緩存着,打包lodash的時候發現lodash也符合這個組的要求,也緩存到這個組裏,當全部的模塊都分析好以後,而後把全部符合這個組的模塊打包到一塊兒去

假設咱們引入jquery這個第三方模塊,它符合vendors這個組的要求,可是它也符合default這個組的要求,那到底webpack作分割的時候究竟是放到vendors這個組裏仍是放到default這個組裏呢?實際上它是經過priority這個優先級來判斷的,誰的優先級高就放到誰的組裏

假設有a、b兩個模塊,b模塊以前在某個地方已經被引用過了,並且在以前的邏輯中已經打包好了,而a模塊又引用了b模塊,配置reuseExistingChunk爲true,再去打包a的時候,就不會去打包a裏面引用的b模塊了,a裏面用到b就直接去複用以前打包好放到某個地方的b模塊,因此這個參數的意思是若是一個模塊已經被打包過了,若是再打包的時候就忽略這個模塊,直接使用以前被打包好的那個

關於這個插件的參數說明能夠參考 官網

自此SplitChunksPlugin中的幾個基本參數已經講解完畢,如今對webpack中的三個概念module、chunk和bundle作一下總結

  • module :就是js的模塊化webpack支持commonJS、ES6等模塊化規範,簡單來講就是你經過import語句引入的代碼。
  • chunk :chunk是webpack根據功能拆分出來的,包含三種狀況:
    一、你的項目入口(entry)
    二、經過import()動態引入的代碼
    三、經過splitChunks拆分出來的代碼

chunk包含着module,多是一對多也多是一對一。

  • bundle :bundle是webpack打包以後的各個文件,通常就是和chunk是一對一的關係,bundle就是對chunk進行編譯壓縮打包等處理以後的產出。
對SplitChunksPlugin參數更多細緻的理解能夠參考這篇 博客

十9、Lazy Loading

懶加載或者按需加載,是一種很好的優化網頁或應用的方式。這種方式其實是先把你的代碼在一些邏輯斷點處分離開,而後在一些代碼塊中完成某些操做後,當即引用或即將引用另一些新的代碼塊。這樣加快了應用的初始加載速度,減輕了它的整體體積,由於某些代碼塊可能永遠不會被加載。

首先咱們作一些回退處理,刪除src中的test.js文件,此時項目結構爲

webpack-demo
        |- build
          |- webpack.common.js
          |- webpack.dev.js
          |- webpack.prod.js
        |- dist
          |- index.html
          |- main.js
          |- vendors.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js

而後,咱們對index.js中的代碼作一些改進
index.js

function getComponent() {
    return import(/*webpackChunkName:"lodash" */'lodash').then(_ => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['Hello', 'World'], '-');
        return ele;
    })
}

document.addEventListener('click', () => {
    getComponent().then(ele => {
        document.body.appendChild(ele);
    })
})

再到webpack.common.js中把vendors.filename註釋掉
webpack.common.js

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
               // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        }
    }

從新npm run dev-build,打包好後,打開dist目錄下的index.html文件,打開Network,能夠看到剛進頁面的時候,只加載了index.html和main.js文件,而vendors~main.js文件並無加載出來
圖片描述

而只有咱們點了頁面中的某個位置以後,這個文件才被加載出來了
圖片描述

因此經過這種異步import的方式,可讓lodash在被須要的時候才加載出來,這就是懶加載的概念。

懶加載實際上並非webpack裏面的一個概念,而是ES裏面提出的這樣一個實驗性質的語法,它和webpack本質上關係不大,webpack只不過是可以識別出這種import語法,而後對它引入的代碼模塊進行代碼分割而已

ES6裏面引入了async...await的語法,經過這種語法,咱們能夠對index.js中的代碼繼續精簡下
index.js

async function getComponent() {
    const { default: _ } = await import(/*webpackChunkName:"lodash" */'lodash');
    const ele = document.createElement('div');
    ele.innerHTML = _.join(['Hello', 'World'], '-');
    return ele;
}

document.addEventListener('click', () => {
    getComponent().then(ele => {
        document.body.appendChild(ele);
    })
})

從新打包,而後打開dist/index.html文件,點擊頁面某個地方,能夠看到頁面顯示正常,說明這樣寫也是沒問題的

關於懶加載的其餘知識點能夠參考 官網

二10、打包分析,preloading和prefetching

(一)、打包分析

首先,咱們進入webpack提供打包分析的一個官方網站

複製下面這段代碼
圖片描述

進入package.json裏面,而後把這段代碼加入到這個地方
圖片描述
意思是在打包的過程當中,把整個打包過程當中的描述放置到名字叫stats.json的文件中,文件的格式是json

從新npm run dev-build,打包成功以後,能夠看到目錄中已經生成了stats.json的文件
圖片描述

而後咱們在官方網站中點開這個http://webpack.github.com/ana...,點擊選擇文件,將咱們剛剛生成的stats.json文件傳上去,它會幫咱們進行打包代碼的分析
圖片描述

除了官方提供的工具外,還有不少其餘工具供咱們使用:

  • webpack-chart: webpack 數據交互餅圖。
  • webpack-visualizer: 可視化並分析你的 bundle,檢查哪些模塊佔用空間,哪些多是重複使用的。
  • webpack-bundle-analyzer: 一款分析 bundle 內容的插件及 CLI 工具,以便捷的、交互式、可縮放的樹狀圖形式展示給用戶。

(二)、preloading和prefetching

webpack 4.6.0+增長了對prefetching和preloading的支持。

在src/index.js中咱們作一些調整

// async function getComponent() {
//     const { default: _ } = await import(/*webpackChunkName:"lodash" */'lodash');
//     const ele = document.createElement('div');
//     ele.innerHTML = _.join(['Hello', 'World'], '-');
//     return ele;
// }

document.addEventListener('click', () => {
    // getComponent().then(ele => {
    //     document.body.appendChild(ele);
    // })
    const ele = document.createElement('div');
    ele.innerHTML = _.join(['Hello', 'World'], '-');
    document.body.appendChild(ele);
})

從新打包npm run build,而後打開dist/index.html,點擊頁面能夠看到打印出‘Hello World’字樣,說明咱們代碼寫的沒有問題,可是咱們這段代碼是否是徹底沒有優化的空間了呢?
咱們打開頁面的控制檯,而後Ctrl+Shift+p,輸入Coverage,選擇第一個
圖片描述

而後點擊左側的錄製按鈕,再刷新頁面
圖片描述

能夠看到代碼使用率是74.7%,點擊main.js,能夠看到這段代碼在咱們點擊頁面前,是沒有被利用的,只有點擊了頁面這段代碼纔會被執行
圖片描述

剛開始加載main.js的時候,這段代碼不會執行,不會執行的代碼讓頁面加載的時候就去下載它,實際上會消耗頁面加載的性能,webpack但願像這種交互的代碼應該把它放到異步加載的模塊當中去

咱們在src中新建一個click.js的文件
click.js

function handleClick() {
    const ele = document.createElement('div');
    ele.innerHTML = 'Hello World';
    document.body.appendChild(ele);
}

export default handleClick;

index.js

document.addEventListener('click', () => {
   import('./click.js').then(({default: func}) => {
      func();
   })
})

從新npm run build,刷新頁面,此時代碼使用率就變成了80.2%了,由於咱們如今是經過異步的方式引入致使代碼變少了
圖片描述

而後切換到Network,能夠看到此時頁面只加載了一個index.html和main.js文件
圖片描述

點擊頁面,能夠看到1.js被加載出來了
圖片描述
點開1.js,能夠看到裏面正好是建立div標籤,而後往頁面掛載這個dom節點的邏輯
圖片描述

因此這樣去寫代碼纔是讓頁面加載速度最快的一種正確方式,寫高性能前端代碼的時候,不光要考慮緩存,還要考慮代碼使用率 ,因此 webpack 在打包過程當中,是但願咱們多寫這種異步的代碼,才能提高網站性能,這也是webpack默認它的splitChunks.chunks配置項是async的緣由了

固然,這也會出現另外一個問題,就是當用戶點擊的時候,纔去加載業務模塊,若是業務模塊比較大的時候,用戶點擊後並無立馬看到效果,而是要等待幾秒,這樣體驗上也很差,怎麼去解決這種問題 ?
若是訪問首頁的時候,不須要加載詳情頁的邏輯,等用戶首頁加載完了之後,頁面展現出來了,頁面的帶寬被釋放出來了,網絡空閒了,再「偷偷」的去加載詳情頁的內容,而不是等用戶去點擊的時候再去加載 這個解決方案就是依賴 webpack 的 Prefetching/Preloading 特性

修改index.js

document.addEventListener('click', () => {
   import(/* webpackPrefetch: true */'./click.js').then(({default: func}) => {
      func();
   })
})

webpackPrefetch: true 會等你主要的 JS 都加載完了以後,網絡帶寬空閒的時候,它就會預先幫你加載好

保存從新npm run build,而後打開dist/index.html打開控制檯中的Network,能夠看到咱們並無點擊頁面,等主要的js文件加載好了以後,頁面會偷偷的再去加載好1.js文件
圖片描述

而後咱們再去點擊頁面,看到1.js又被加載一遍,可是注意到此次加載的1.js響應速度就不是4ms了,而是1ms,這是由於第一次加載好以後瀏覽器就緩存起來了,第二次再去加載的時候就直接拿緩存了
圖片描述

這裏咱們使用的是 webpackPrefetch,還有一種是 webpackPreload

區別: Prefetch 會等待覈心代碼加載完以後,有空閒以後再去加載。Preload 會和核心的代碼並行加載,仍是不推薦

總結
針對優化,不只僅是侷限於緩存,緩存能帶來的代碼性能提高是很是有限的,而是如何讓代碼的使用率最高,有一些交互後才用的代碼,能夠寫到異步組件裏面去,經過懶加載的形式,去把代碼邏輯加載進來,這樣會使得頁面訪問速度變的更快,若是以爲懶加載會影響用戶體驗,能夠使用 Prefetch 這種方式來預加載

想了解更多這節知識點能夠參考官方網站

二11、CSS文件的代碼分割

(一)、filename和chunkFilename的區別

咱們在webpack的配置中可能還會常常看見一個這樣的配置

module.exports = {
    entry: {
        main: './src/index.js',
    },                        
    output: {
        filename: '[name].js', 
+       chunkFilename: '[name].chunk.js',                      
        path: path.resolve(__dirname, '../dist'),
    },
    ...
}

那filename和chunkFilename有什麼區別呢?

如今作一些回退處理,刪除src/click.js文件,並刪除index.js裏面的內容,將以前的異步代碼加入裏面
index.js

async function getComponent() {
   const { default: _ } = await import(/*webpackChunkName:"lodash" */'lodash');
   const ele = document.createElement('div');
   ele.innerHTML = _.join(['Hello', 'World'], '-');
   return ele;
}

document.addEventListener('click', () => {
   getComponent().then(ele => {
       document.body.appendChild(ele);
   })
})

從新npm run dev-build,能夠看到dist目錄中打包生成的文件名以下,爲何會有一個vendors~lodash.chunk.js文件呢
圖片描述
這是由於在entry中咱們配置的index.js文件是一個入口文件,入口文件打包生成的文件都會走output中的filename這個配置項,因此index.js在作打包的時候它前面的key值是main,因此最終生成的就是main.js文件;main.js文件中會引入lodash,main.js在打包過程當中先執行,而後異步的去加載這個lodash,因此這個lodash並非一個入口的js文件,而是一個間接異步加載的js文件,打包這樣的文件就會走chunkFilename這個配置項

(二)、mini-css-extract-plugin對CSS文件作抽離

接下來,咱們介紹如何進行css的代碼分割,咱們須要藉助webpack官網提供的一個插件MiniCssExtractPlugin,此插件將CSS提取到單獨的文件中。它爲每一個包含CSS的JS文件建立一個CSS文件。它支持CSS和SourceMaps的按需加載。

它創建在新的webpack v4功能(模塊類型)之上,而且須要webpack 4才能工做,以前的版本一直用的都是extract-text-webpack-plugin。

與extract-text-webpack-plugin相比:

  • 異步加載
  • 沒有重複的編譯(性能)
  • 更容易使用
  • 特定於CSS

    安裝

npm install --save-dev mini-css-extract-plugin

而後咱們在webpack.common.js中進行配置,由於咱們以前在CSS Tree Shaking中已經對css文件配置過MiniCssExtractPlugin,如今咱們對scss文件也配置下
webpack.common.js

{
          test: /\.scss$/,
          use: [
  -           'style-loader',
  +            MiniCssExtractPlugin.loader,   
               {
                 loader: 'css-loader',
                 options: {
                    importLoaders: 2
                 }
               },
               'postcss-loader',
               'sass-loader'
        ]
   },

配置好以後,在src中新建一個style.css文件
style.css

body {
    background-color: blue;
}

index.js

import './style.css';

npm run dev-build運行下,能夠看到style.css文件已經被抽離成main.css文件了,而後打開dist目錄下的index.html文件,能夠看到頁面變成了藍色

MiniCssExtractPlugin參數中除了能夠配置filename還能夠配置chunkFilename
webpack.common.js

new MiniCssExtractPlugin({
            filename: '[name].css',
 +          chunkFilename: '[name].chunk.css'                       
        })

從新npm run dev-build,在dist目錄中能夠看到生成的仍是main.css文件,爲何不是main.chunk.css文件呢?這是由於咱們css文件是同步引入的,若是是異步引入就走chunkFilename邏輯,具體能夠參見這裏的回答
圖片描述

在src目錄下,咱們在去建立一個style1.css文件
style1.css

body {
    font-size: 100px;
}

index.js

import './style.css';
+ import './style1.css';

從新npm run dev-build,點開dist目錄下的main.css文件,能夠看到這個插件還會把引入的css文件合併到一塊兒
圖片描述

(三)、optimize-css-assets-webpack-plugin對CSS文件作壓縮

另外咱們還但願打包好的css文件能夠作壓縮,這裏咱們須要使用另一個插件optimize-css-assets-webpack-plugin

安裝

npm install --save-dev optimize-css-assets-webpack-plugin
注意
對於webpack v3或更低版本,請使用optimize-css-assets-webpack-plugin@3.2.0。該optimize-css-assets-webpack-plugin@4.0.0版本及以上版本支持webpack v4。

而後在webpack.common.js中去引入
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')   
+  const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = { 
    ...
    optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        },
+        minimizer: [new OptimizeCSSAssetsPlugin({})]
    }
}

保存從新npm run dev-build,打包完成以後,發現main.css文件並無被壓縮,這裏找了好久的緣由,終於發現是咱們打包命令執行錯了,npm run dev-build最終執行的是webpack.dev.js文件,OptimizeCSSAssetsPlugin的壓縮功能彷佛在開發環境下不起做用;因此咱們從新用npm run build,執行打包,打包完以後發現連css文件都看不到了,這是由於咱們在前面Tree Shaking中提到過,生產模式下,tree shaking把樣式文件忽略了,因此咱們還須要在package.json中從新配置下
package.json

{
   + "sideEffects": ["*.css"],
}

配置好後,從新npm run build,再打開dist/main.css就能夠看到壓縮成功了
圖片描述

注意:webpack:production模式默認有配有js壓縮,若是這裏設置了css壓縮,js壓縮也要從新設置,由於使用minimizer會自動取消webpack的默認配置,所以此請務必同時指定JS minimalizer

因此咱們還須要使用JS壓縮的插件terser-webpack-plugin

安裝

npm install terser-webpack-plugin --save-dev

安裝完以後,再在webpack.common.js中進行配置
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')   
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+  const TerserJSPlugin = require('terser-webpack-plugin');

module.exports = { 
...
    plugins: [
        ...
        minimizer: [
+           new TerserJSPlugin({}), 
            new OptimizeCSSAssetsPlugin({})
        ]
    }
}

從新npm run build,能夠看到main.js已經被壓縮了

假如咱們在entry中有多個入口,每一個入口文件中都引入了css文件
webpack.common.js

entry: {
        main: './src/index.js',
+       sub: './src/index1.js'
    },

而後再在src目錄下新建一個index1.js和style2.css
index1.js

import './style2.css'

style2.css

body {
    width: 200px;
    height: 200px;
    border: 1px solid yellow;
}

保存從新npm run build,能夠看到dist目錄中分別生成了一個mian.css和sub.css文件
圖片描述

若是想讓這些css文件合併成一個css文件怎麼辦呢?咱們能夠藉助splitChunks.cacheGroups這個功能來實現
,它能夠在單個文件中提取全部CSS

optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              },
+             styles: {
+               name: 'styles',    
+               test: /\.css$/,
+               chunks: 'all',
+               enforce: true,     //  意思是忽略掉默認的一些參數,只要是css文件就作代碼的拆分
+             },
            }
        },
        minimizer: [
            new TerserJSPlugin({}), 
            new OptimizeCSSAssetsPlugin({})
        ]
    }

保存從新npm run build,如今就能夠看到只生成了一個CSS文件
圖片描述

關於插件mini-css-extract-plugin的其餘配置項能夠參考 官方網站

此時項目結構爲:

webpack-demo
        |- build
          |- webpack.common.js
          |- webpack.dev.js
          |- webpack.prod.js
        |- dist
          |- index.html
          |- main.js
          |- styles.chunk.css
          |- styles.chunk.js
          |- sub.js
        |- /node_modules
        |- /src
          |- index.js
          |- index.html
          |- index1.js
          |- style.css
          |- style1.css
          |- style2.css
        |- .babelrc
        |- package.json
        |- package-lock.json
        |- postcss.config.js
        |- stats.json

二12、webpack與瀏覽器緩存(Caching)

咱們使用 webpack 來打包咱們的模塊化後的應用程序,webpack 會生成一個可部署的 /dist 目錄,而後把打包後的內容放置在此目錄中。只要 /dist 目錄中的內容部署到服務器上,客戶端(一般是瀏覽器)就可以訪問網站此服務器的網站及其資源。而最後一步獲取資源是比較耗費時間的,這就是爲何瀏覽器使用一種名爲 緩存 的技術。能夠經過命中緩存,以下降網絡流量,使網站加載速度更快,然而,若是咱們在部署新版本時不更改資源的文件名,瀏覽器可能會認爲它沒有被更新,就會使用它的緩存版本。因爲緩存的存在,當你須要獲取新的代碼時,就會顯得很棘手。

這一節經過必要的配置,以確保 webpack 編譯生成的文件可以被客戶端緩存,而在文件內容變化後,可以請求到新的文件。

先作一些回退處理
webpack.common.js

module.exports = { 
    entry: {
        main: './src/index.js',
-       sub: './src/index1.js'
    }, 
    ...                       
    optimization: {
        splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                // filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              },
   -          styles: {
   -            name: 'styles',
   -            test: /\.css$/,
   -            chunks: 'all',
   -            enforce: true,
   -          },
            }
        },
    }
}

而後刪除src下面的index1.js,style.css,style1.css,style2.css,並清空index.js裏面的內容
index.js

import _ from 'lodash';
import $ from 'jquery';

const dom = $('<div>');
dom.html(_.join(['hello world']), ' ');
$('body').append(dom);

由於lodash和jquery是同步引入的,因此咱們能夠在vendors.filename中自定義下名字

splitChunks: {
            chunks: 'all',
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
              vendors:  {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
+               filename: 'vendors.js'
              },
              default: {
                // minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
                filename: 'common.js'
              }
            }
        },

從新npm run build,打開index.html頁面正常顯示,頁面在第一次打開的時候會去請求兩個js文件,一個是main.js一個是vendors.js文件,用戶第二次刷新頁面的時候,這兩個文件實際上已經被緩存進瀏覽器了,就能夠直接到瀏覽器緩存中拿了,加入咱們改了代碼
index.js

import _ from 'lodash';
import $ from 'jquery';

const dom = $('<div>');
dom.html(_.join(['hello', 'world'],'-----'));     //  這裏改成-----鏈接
$('body').append(dom);

從新打包,而後把新生成的dist文件上傳到服務器,當用戶刷新以後會看到改變後的內容嗎?實際上是不會的,由於咱們打包後的名字沒有變,仍是main.js和vendors.js,用戶再刷新頁面的時候,發現這兩個文件本地有緩存了,就會直接用本地的緩存了,而不會用你新上傳上去的這兩個文件,這樣就會產生問題

爲了解決這個問題該怎麼作呢?咱們須要引入一個概念contenthash

webpack.common.js

output: {
-       filename: '[name].js', 
-       chunkFilename: '[name].chunk.js',                     
        path: path.resolve(__dirname, '../dist'),
    },

webpack.dev.js

const devConfig = {
    ...
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
+   output: {
+       filename: '[name].js', 
+       chunkFilename: '[name].chunk.js',   
+   }
}

webpack.prod.js

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
    mode: 'production', 
    devtool: 'cheap-module-source-map',
+    output: {
+        filename: '[name].[contenthash].js',      // contenthash是和name同樣的佔位符
+        chunkFilename: '[name].[contenthash].js', 
+    }
}

module.exports = merge(commonConfig, prodConfig);

從新npm run build,能夠看到生成的hash值是7fa81f74109755cc2cb0
圖片描述

而後咱們對index.js什麼都不改,從新再npm run build,能夠看到生成的hash值仍是同樣的,源代碼沒變,打包出來的文件名也沒變,因此用戶再去請求的時候,仍是拿的緩存。假設咱們對源代碼再次進行修改下
index.js

import _ from 'lodash';
import $ from 'jquery';

const dom = $('<div>');
dom.html(_.join(['hello', 'world'], '======='));   //  ----- 改成=======
$('body').append(dom);

從新npm run build,能夠看到hash已經變成bf597aacb0a0afd970fc
圖片描述

咱們從新把這個最新的dist文件放到服務器上去,用戶再去訪問頁面的時候,就是訪問最新的js文件了

更多Caching的配置能夠參考 官方文檔

二十3、Shimming

webpack 編譯器(compiler)可以識別遵循 ES2015 模塊語法、CommonJS 或 AMD 規範編寫的模塊。然而,一些第三方的庫(library)可能會引用一些全局依賴(例如 jQuery 中的 $)。這些庫也可能建立一些須要被導出的全局變量。這些「不符合規範的模塊」就是 shimming 發揮做用的地方。

在項目中咱們常常會使用一些第三方庫,這裏咱們在src中自創一個這樣的庫jquery.ui.js
src/jquery.ui.js

export function ui() {
    $('body').css('background', 'green');
}

而後咱們在index.js中引入這個庫

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

+  ui()

   const dom = $('<div>');
   dom.html(_.join(['hello', 'world'], '======='));
   $('body').append(dom);

保存以後,運行npm run dev,能夠看到頁面直接報錯,提示咱們「$ is not defined」
圖片描述
實際上這裏是指jquery.ui.js中的$找不到,那咱們會想,在index.js中咱們不是引入了$了嗎?爲何在jquery.ui.js中找不到呢,緣由是由於在webpack中咱們是基於模塊打包的,模塊中的變量是獨立於本身這個模塊的,因此在jquery.ui.js中想去使用另外一個模塊的$是不可能的,經過這種方式能夠保證,模塊與模塊之間不會有任何的耦合,不會由於一個模塊出錯影響到另外一個模塊

若是想要在jquery.ui.js中去使用$,就必須在上面引入這個變量

+  import $ from 'jquery';

   export function ui() {
      $('body').css('background', 'green');
   }

從新刷新頁面,能夠看到頁面就正常顯示綠色了,可是這個庫是第三方的,不是咱們本身寫的,實際上想在這個庫的源碼中直接這樣引入是不現實的,那是否是沒辦法實現了呢?

先在jquery.ui.js中去掉這個(import $ from 'jquery')引入,而後打開webpack.common.js,咱們在這個裏面作點配置

+  const webpack = require('webpack');

   module.exports = { 
    ...
      plugins: [
        ...
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[name].chunk.css'                       
        }),
+       new webpack.ProvidePlugin({
+           $: 'jquery'
+       })
    ]
}

意思是webpack若是發現一個模塊中使用了$變量,就會在這個模塊中自動幫你引入jquery,當這樣配置以後就完美的解決了上面的問題了,接下來咱們驗證下,改了配置文件須要從新npm run dev,能夠看到這個時候頁面就正常顯示了

若是咱們還想在這個庫中直接使用lodash中的某個方法(如:join方法),能夠這樣配置

export function ui() {
    $('body').css('background', join(['green'], ''));
}

webpack.common.js

plugins: [
        ...
        new webpack.ProvidePlugin({
            $: 'jquery',
+           join: ['lodash', 'join']
        })
    ],

保存從新npm run dev,頁面依然正常顯示

接下來介紹Shimming的其它用法
刪掉src/jquery.ui.js這個文件,而後刪掉index.js中的內容,並在這個裏面打印這樣一句話
index.js

console.log(this)

刷新頁面在控制檯能夠看到此時打印出來的是一個對象
圖片描述

實際上這個this指的是index.js這個模塊自身。有的時候咱們但願這個this指向的是window,那咱們能夠藉助這個imports-loader來幫咱們實現

安裝

npm install imports-loader --save-dev

安裝好以後,咱們再在webpack.common.js中進行配置

{ 
        test: /\.js$/, 
        exclude: /node_modules/, 
 -      loader: 'babel-loader'
 +      use: [{
 +            loader: 'babel-loader'
 +        }, {
 +            loader: 'imports-loader?this=>window'
 +      }]
},

意思是當加載js文件的時候,首先會走imports-loader,它會把這個js文件裏面的this改成window,而後再交給babel-loader作js的編譯

從新運行npm run dev,打開瀏覽器控制檯,此時this就是指向window了
圖片描述

更多Shimming的用法能夠參考 官方文檔

二十4、環境變量的使用

對webpack.dev.js修改

const webpack = require('webpack');
-  const merge = require('webpack-merge');
-  const commonConfig = require('./webpack.common.js');

const devConfig = {
    mode: 'development', 
    devtool: 'cheap-module-eval-source-map',  
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        proxy: {
            '/api:': 'http://localhost:3000'
        },
        hot: true          
    },
    optimization: {
        usedExports: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    output: {
        filename: '[name].js', 
        chunkFilename: '[name].chunk.js',   
    }
}

-   module.exports = merge(commonConfig, devConfig);
+   module.exports = devConfig;

對webpack.prod.js修改

-  const merge = require('webpack-merge');
-  const commonConfig = require('./webpack.common.js');

const prodConfig = {
    mode: 'production', 
    devtool: 'cheap-module-source-map',
    output: {
        filename: '[name].[contenthash].js', 
        chunkFilename: '[name].[contenthash].js', 
    }
}

-  module.exports = merge(commonConfig, prodConfig);
+  module.exports = prodConfig;

對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');   
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
+  const merge = require('webpack-merge');
+  const devConfig = require('./webpack.dev.js');
+  const prodConfig = require('./webpack.prod.js');

-  module.exports = { 
+  const commonConfig = { 
    ...
}


// 以前導出的是對象,如今咱們導出的是一個函數,函數參數咱們能夠在webpack命令行環境配置中,經過設置 --env進行配置
+ module.exports = env => {
+    if(env && env.production) {
+        return merge(commonConfig, prodConfig);
+    } else {
+        return merge(commonConfig, devConfig);
+    }
+ }

經過上面這樣修改,咱們打包的時候就須要往webpack.common.js這個配置文件中傳遞env這樣一個全局變量

打開package.json

"scripts": {
-    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js",
+    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.common.js",
-    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
+    "dev": "webpack-dev-server --config ./build/webpack.common.js",
-    "build": "webpack --config ./build/webpack.prod.js"
+    "build": "webpack --env.production --config ./build/webpack.common.js"
  },
若是設置 env 變量,卻沒有賦值,--env.production 默認將 --env.production 設置爲 true。還有其餘能夠使用的語法。有關詳細信息,請查看 webpack CLI文檔。

保存,咱們驗證下
執行npm run build,能夠看到打包正常
圖片描述

執行npm run dev,打包仍是正常
圖片描述

而後執行npm run dev-build,打包依然正常,說明咱們的環境變量配置好了

相關文章
相關標籤/搜索