1. 程式人生 > >webpack4.x最詳細入門講解

webpack4.x最詳細入門講解

前言

Webpack其實沒有想象中的那麼難,用得多了,其實套路都一樣,本文主要是針對webpack4.x版本,會對平時常用的Webpack配置一一講解,所以本文也比較長,但如果你能動手跟著本文中的例子完整寫一次,相信你會覺得Webpack也不過如此。

一、什麼是webpack,為什麼使用它?

1

1.1 什麼是webpack?

簡單來說,它其實就是一個模組打包器

1.2 為什麼使用它?

如果像以前開發時一個html檔案可能會引用十幾個js檔案,而且順序還不能亂,因為它們存在依賴關係,同時對於ES6+等新的語法,less, sass等CSS預處理都不能很好的解決……,此時就需要一個處理這些問題的工具。

Webpack就是為處理這些問題而生的,它就是把你的專案當成一個整體,通過一個入口主檔案(如:index.js),從這個檔案開始找到你的專案所有的依賴檔案並處理它們,最後打包成一個(或多個)瀏覽器可識別的JavaScript檔案。

二、一個簡單的打包例子

2.1 準備工作

首先新建一個空資料夾,用於建立專案,在終端中進入資料夾,如下我在桌面建了一個名為webpack-project的資料夾,使用終端進入資料夾後(如果對命令列不太熟悉,可參考我的部落格:前端常用命令列),使用npm init命令建立一個package.json檔案。

npm init

2

輸入這個命令後,終端會問你一系列諸如專案名稱,專案描述,作者等資訊,不過如果你不打算髮布這個模組,直接一路回車就好。(也可以使用npm init -y

這個命令來一次生成package.json檔案,這樣終端不會詢問你問題)。

2.2 安裝webpack

如果你想一步到位的話,就把全域性webpack和本地專案webpack全都先裝了,因為後面一些模組會用到。安裝本地專案webapck時把webpack-cli也裝上,因為webpack模組把一些功能分到了webpack-cli模組,安裝方法如下:

npm install webpack --global                //這是安裝全域性webpack命令
npm install webpack webpack-cli --save-dev  //這是安裝本地專案模組
tips:

上述命令可採用簡寫,install

可簡寫為i,--global可簡寫為-g,--save-dev可簡寫為-D(這個命令是用於把配置新增到package.json的開發環境配置列表中,後面會提到),--save可簡寫為-S,同時國內我們可以採用cnpm,配置方法可去這裡檢視,這樣安裝速度會相對較快。如下:

cnpm i webpack -g               //這是安裝全域性webpack命令
cnpm i webpack webpack-cli -D   //這是安裝本地專案模組
2.3 新建檔案

webpack-project資料夾中新建兩個資料夾,分別為src資料夾和dist資料夾,接下來再建立三個檔案:

  • index.html --放在dist資料夾中;
  • hello.js --放在src資料夾中;
  • index.js --放在src資料夾中;

此時,專案結構如下:

3

我們在index.html中寫下html程式碼,它的作用是為了引入我們打包後的js檔案:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Project</title>
</head>
<body>
    <div id='root'></div>
    <script src="bundle.js"></script>   <!--這是打包之後的js檔案,我們暫時命名為bundle.js-->
</body>
</html>

我們在hello.js中匯出一個模組:

// hello.js
module.exports = function() {
    let hello = document.createElement('div');
    hello.innerHTML = "Long time no see!";
    return hello;
  };

然後在index.js中引入這個模組(hello.js):

//index.js 
const hello = require('./hello.js');
document.querySelector("#root").appendChild(hello());

上述操作就相當於我們把hello.js模組合併到了index.js模組,之後我們打包時就只需把index.js模組打包成bundle.js,然後供index.html引用即可,這就是最簡單的webpack打包原理。

2.4 開始進行webpack打包

在終端中使用如下命令進行打包: // webpack全域性安裝的情況下 webpack src/index.js --output dist/bundle.js // --output可簡寫為-o 上述就相當於把src資料夾下的index.js檔案打包到dist檔案下的bundle.js,這時就生成了bundle.jsindex.html檔案引用。

結果如下:

4

可以看出webpack同時編譯了index.jshello.js,現在開啟index.html,可以看到如下結果:

5

沒錯,我們已經成功使用webpack進行打包,原來webpack也不過如此嘛!但是,每次都在終端中輸入這麼長的命令,感覺好煩啊,還好有懶人方法,讓我們看看。

2.5 通過配置檔案來使用webpack

其實webpack是有很多功能的,也是很方便的,我們可以在當前專案的根目錄下新建一個配置檔案webpack.config.js,我們寫下如下簡單配置程式碼,目前只涉及入口配置(相當於我們的index.js,從它開始打包)和出口配置(相當於我們打包生成的bundle.js)。

// webpack.config.js
module.exports = {
    entry: __dirname + "/src/index.js", // 入口檔案
    output: {
        path: __dirname + "/dist", //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    }
}

注:__dirname是node.js中的一個全域性變數,它指向當前執行指令碼所在的目錄,即C:\Users\sjt\DeskTop\webpack-project(這是我當前的目錄)

但平時我們看到的腳手架配置也比較喜歡採用node.js的path模組來處理絕對路徑,所以我們也可以採用如下的寫法,和上述的效果是一樣的:

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    }
}

注:path.join的功能是拼接路徑片段。

有了這個配置檔案,我們只需在終端中執行webpack命令就可進行打包,這條命令會自動引用webpack.config.js檔案中的配置選項,示例如下:

6

搞定,是不是這樣更方便了,感覺沒那麼low了,但還能不能更便捷智慧呢?那必須的!

2.6 更智慧的打包方式

我們現在只在終端中使用webpack命令來進行打包,要是以後在打包的同時還有更多的操作呢,那不是還得寫上更多的命令?所以我們得想辦法把這些命令都整合起來,這時候之前的package.json檔案就派上用場了。 現在的package.json檔案大概就是如下這樣:

{
  "name": "webpack-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1" //我們要修改的是這裡,JSON檔案不支援註釋,引用時請清除
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2"
  }
}

修改如下:

{
  "name": "webpack-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack", //改成這樣,注意使用時把註釋刪掉
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2"
  }
}

注:package.json中的script會按你設定的命令名稱來執行對應的命令。

這樣我們就可以在終端中直接執行npm start命令來進行打包,start命令比較特殊,可以直接npm加上start就可以執行,如果我們想起其他的名稱,如build時,就需要使用npm run加上build,即npm run build命令。 現在我們執行npm start命令:

7

OK,搞定,是不是很簡單,但webpack的功能遠不止於此,下面我們繼續。

三、構建本地伺服器

現在我們是通過開啟本地檔案來檢視頁面的,看起來總感覺比較low,看別人用vue,react框架時都是執行在本地伺服器上的,那我們能不能也那樣呢?那必須的!

3.1 webpack-dev-server配置本地伺服器

Webpack提供了一個可選的本地開發伺服器,這個本地伺服器基於node.js構建,它是一個單獨的元件,在webpack中進行配置之前需要單獨安裝它作為專案依賴:

cnpm i webpack-dev-server -D

devServer作為webpack配置選項中的一項,以下是它的一些配置選項: | devServer配置選項 | 功能描述 | | ------ | ------ | | contentBase | 設定伺服器所讀取檔案的目錄,當前我們設定為"./dist" | | port | 設定埠號,如果省略,預設為8080 | | inline | 設定為true,當原始檔改變時會自動重新整理頁面 | | historyApiFallback | 設定為true,所有的跳轉將指向index.html |

現在我們把這些配置加到webpack.config.js檔案上,如下:

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",   // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    }
}

我們繼續在package.json檔案中新增啟動命令:

{
  "name": "webpack-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}

我們把start命令名稱改為了build,這樣比較語義化,平時的腳手架也多數採用這個名稱,我們用dev(development的縮寫,意指開發環境)來啟動本地伺服器,webpack-dev-server就是啟動伺服器的命令,--open是用於啟動完伺服器後自動開啟瀏覽器,這時候我們自定義命令方式的便捷性就體現出來了,可以多個命令整合在一起執行,即我們定義了一個dev命令名稱就可以同時運行了webpack-dev-server--open兩個命令。

現在在終端輸入npm run dev執行伺服器:

8

這樣我們即可在http://localhost:8088/中檢視頁面(退出伺服器,可使用ctrl+c後,再按y確認,即可退出伺服器執行)

3.2 Source Maps除錯配置

作為開發,程式碼除錯當然少不了,那麼問題來了,經過打包後的檔案,你是不容易找到出錯的地方的,Source Map就是用來解決這個問題的。

通過如下配置,我們會在打包時生成對應於打包檔案的.map檔案,使得編譯後的程式碼可讀性更高,更易於除錯。

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    },
    devtool: 'source-map'  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
}

配置好後,我們再次執行npm run build進行打包,這時我們會發現在dist資料夾中多出了一個bundle.js.map檔案如下: 9 如果我們的程式碼有bug,在瀏覽器的除錯工具中會提示錯誤出現的位置,這就是devtool: 'source-map'配置項的作用。

四、Loaders

loaders是webpack最強大的功能之一,通過不同的loader,webpack有能力呼叫外部的指令碼或工具,實現對不同格式的檔案的處理,例如把scss轉為css,將ES66、ES7等語法轉化為當前瀏覽器能識別的語法,將JSX轉化為js等多項功能。

Loaders需要單獨安裝並且需要在webpack.config.js中的modules配置項下進行配置,Loaders的配置包括以下幾方面:

  • test:一個用以匹配loaders所處理檔案的拓展名的正則表示式(必須)
  • loader:loader的名稱(必須)
  • include/exclude:手動新增必須處理的檔案(資料夾)或遮蔽不需要處理的檔案(資料夾)(可選);
  • options:為loaders提供額外的設定選項(可選)
4.1 配置css-loader

如果我們要載入一個css檔案,需要安裝配置style-loadercss-loader:

cnpm i style-loader css-loader -D
// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    },
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            }
        ]
    }
}

我們在src資料夾下新建css資料夾,該資料夾內新建style.css檔案:

/* style.css */
body {
    background: gray;
}

index.js中引用它:

//index.js 
import './css/style.css';  //匯入css

const hello = require('./hello.js');
document.querySelector("#root").appendChild(hello());

這時我們執行npm run dev,會發現頁面背景變成了灰色。

如果是要編譯sass檔案呢?

4.2 配置sass
cnpm i sass-loader node-sass -D // 因為sass-loader依賴於node-sass,所以還要安裝node-sass

增加sass的rules:

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    },
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {
                test: /\.(scss|sass)$/,   // 正則匹配以.scss和.sass結尾的檔案
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            }
        ]
    }
}

在css資料夾中新建blue.scss檔案:

/* blue.scss */
$blue: blue;
body{
    color: $blue;
} 

index.js中引入blue.scss

//index.js 
import './css/style.css';   // 匯入css
import './css/blue.scss';   // 匯入scss

const hello = require('./hello.js');
document.querySelector("#root").appendChild(hello());

這時npm run dev重新啟動伺服器,應該會出現如下結果:

10

還有諸如圖片loader、字型loader等就不一一列出來了,感興趣的可前往webpack官網檢視,都是一樣的套路。

五、Babel

Babel其實是一個編譯JavaScript的平臺,它可以編譯程式碼幫你達到以下目的:

  • 讓你能使用最新的JavaScript程式碼(ES6,ES7...),而不用管新標準是否被當前使用的瀏覽器完全支援;
  • 讓你能使用基於JavaScript進行了拓展的語言,比如React的JSX;
5.1 Babel的安裝與配置

Babel其實是幾個模組化的包,其核心功能位於稱為babel-core的npm包中,webpack可以把其不同的包整合在一起使用,對於每一個你需要的功能或拓展,你都需要安裝單獨的包(用得最多的是解析ES6的babel-preset-env包和解析JSX的babel-preset-react包)。

cnpm i babel-core babel-loader babel-preset-env babel-preset-react -D
// babel-preset-env的env表示是對當前環境的預處理,而不是像以前使用babel-preset-es2015只能針對某個環境
// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    },
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {
                test: /\.(scss|sass)$/,   // 正則匹配以.scss和.sass結尾的檔案
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use選擇如果有多項配置,可寫成這種物件形式
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
}

現在我們已經可以支援ES6及JSX的語法了,我們用react來試試,但使用react還得先安裝兩個模組reactreact-dom

cnpm i react react-dom -D

接下來我們把hello.js檔案修改一下:

// hello.js
import React, {Component} from 'react'; // 這兩個模組必須引入

let name = Alan;

export default class Hello extends Component{
    render() {
        return (
            <div>
                {name}
            </div>
        );
    }
}

修改index.js檔案:

//index.js 
import './css/style.css';  // 匯入css
import './css/blue.scss';  // 匯入scss

import React from 'react';
import {render} from 'react-dom';
import Hello from './hello'; // 可省略.js字尾名

render(<Hello />, document.getElementById('root'));

此時執行npm run dev後你可能會發現如下結果:

11

這是因為官方預設babel-loader | babel對應的版本需要一致: 即babel-loader需要搭配最新版本babel,詳細可參考這篇部落格

兩種解決方案:

  • 回退低版本
cnpm i [email protected] babel-core babel-preset-env -D
  • 更新到最高版本:
cnpm i babel-loader @babel/core @babel/preset-env webpack -D

我這裡採取的是第一個方案,回退後,再此執行npm run dev,得到如下結果:

12

到這裡了是不是感覺很爽,不就是配置嘛,想要使用什麼就配置什麼。

5.2 優化babel配置

雖然babel完全可以在webpack.config.js中進行配置,但現在不是都提倡模組化嘛,也許之後babel膨脹了,增加了更多的配置項呢? 那我們不如把它提取出來,把它放到根目錄下的.babelrc檔案下(webpack會自動呼叫.babelrc裡的babel配置選項)。

我們在專案根目錄下新建.babelrc檔案:

13

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    },
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {
                test: /\.(scss|sass)$/,   // 正則匹配以.scss和.sass結尾的檔案
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use選擇如果有多項配置,可寫成這種物件形式
                    loader: "babel-loader"
                },
                exclude: /node_modules/   // 排除匹配node_modules模組
            }
        ]
    }
}
// .babelrc 使用時把註釋刪掉,該檔案不能添加註釋
{
    "presets": ["env", "react"]
}

此時不出問題的話應該一切執行正常,接下來讓我們進入強大的外掛模組。

六、外掛(Plugins)

外掛(Plugins)是用來拓展Webpack功能的,它們會在整個構建過程中生效,執行相關的任務。 Loaders和Plugins常常被弄混,但是他們其實是完全不同的東西,可以這麼來說,loaders是在打包構建過程中用來處理原始檔的(JSX,Scss,Less..),一次處理一個,外掛並不直接操作單個檔案,它直接對整個構建過程其作用。

6.1 如何使用

使用某個外掛,需要通過npm進行安裝,然後在webpack.config.js配置檔案的plugins(是一個數組)配置項中新增該外掛的例項,下面我們先來使用一個簡單的版權宣告外掛。

// webpack.config.js
const webpack = require('webpack');  // 這個外掛不需要安裝,是基於webpack的,需要引入webpack模組

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {
                test: /\.(scss|sass)$/,   // 正則匹配以.scss和.sass結尾的檔案
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use選擇如果有多項配置,可寫成這種物件形式
                    loader: "babel-loader"
                },
                exclude: /node_modules/   // 排除匹配node_modules模組
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究')  // new一個外掛的例項 
    ]
}

執行npm run build打包後我們看到bundle.js檔案顯示如下:

14

6.2 自動生成html檔案(HtmlWebpackPlugin)

到目前為止我們都是使用一開始建好的index.html檔案,而且也是手動引入bundle.js,要是以後我們引入不止一個js檔案,而且更改js檔名的話,也得手動更改index.html中的js檔名,所以能不能自動生成index.html且自動引用打包後的js呢?HtmlWebpackPlugin外掛就是用來解決這個問題的:

首先安裝該外掛

cnpm i html-webpack-plugin -D

然後我們對專案結構進行一些更改:

  1. dist整個資料夾刪除;
  2. src資料夾下新建一個index.template.html(名稱自定義)檔案模板(當然這個是可選的,因為就算不設定模板,HtmlWebpackPlugin外掛也會生成預設html檔案,這裡我們設定模組會讓我們的開發更加靈活),如下:
<!-- index.template.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Here is Template</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

webpack.config.js中我們引入了HtmlWebpackPlugin外掛,並配置了引用了我們設定的模板,如下:

// webpack.config.js
const path = require('path');  // 路徑處理模組
const webpack = require('webpack');  // 這個外掛不需要安裝,是基於webpack的,需要引入webpack模組
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入HtmlWebpackPlugin外掛

module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
    },
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {
                test: /\.(scss|sass)$/,   // 正則匹配以.scss和.sass結尾的檔案
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use選擇如果有多項配置,可寫成這種物件形式
                    loader: "babel-loader"
                },
                exclude: /node_modules/   // 排除匹配node_modules模組
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究'),  // new一個外掛的例項 
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "/src/index.template.html")// new一個這個外掛的例項,並傳入相關的引數
        })
    ]
}

然後我們使用npm run build進行打包,你會發現,dist資料夾和html檔案都會自動生成,如下:

15

為什麼會自動生成dist資料夾呢?因為我們在output出口配置項中定義了出口檔案所在的位置為dist資料夾,且出口檔名為bundle.js,所以HtmlWebpackPlugin會自動幫你在index.html中引用名為bundle.js檔案,如果你在webpack.config.js檔案中更改了出口檔名,index.html中也會自動更改該檔名,這樣以後修改起來是不是方便多了?

6.3 清理/dist資料夾(CleanWebpackPlugin)

你可能已經注意到,在我們刪掉/dist資料夾之前,由於前面的程式碼示例遺留,導致我們的/dist資料夾比較雜亂。webpack會生成檔案,然後將這些檔案放置在/dist資料夾中,但是webpack無法追蹤到哪些檔案是實際在專案中用到的。

通常,在每次構建前清理/dist資料夾,是比較推薦的做法,因此只會生成用到的檔案,這時候就用到CleanWebpackPlugin外掛了。

cnpm i clean-webpack-plugin -D
// webpack.config.js
...
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 引入CleanWebpackPlugin外掛

module.exports = {
    ...
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究'),  // new一個外掛的例項 
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "/src/index.template.html")// new一個這個外掛的例項,並傳入相關的引數
        }),
        new CleanWebpackPlugin(['dist']),  // 所要清理的資料夾名稱
    ]
}

外掛的使用方法都是一樣的,首先引入,然後new一個例項,例項可傳入引數。

現在我們執行npm run build後就會發現,webpack會先將/dist資料夾刪除,然後再生產新的/dist資料夾。

6.4 熱更新(HotModuleReplacementPlugin)

HotModuleReplacementPlugin(HMR)是一個很實用的外掛,可以在我們修改程式碼後自動重新整理預覽效果。

方法:

  1. devServer配置項中新增hot: true引數。
  2. 因為HotModuleReplacementPluginwebpack模組自帶的,所以引入webpack後,在plugins配置項中直接使用即可。
// webpack.config.js
...
const webpack = require('webpack');  // 這個外掛不需要安裝,是基於webpack的,需要引入webpack模組

module.exports = {
    ...
    devServer: {
        contentBase: "./dist", // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true, // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
        hot: true // 熱更新
    },
    ...
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究'),  // new一個外掛的例項 
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "/src/index.template.html")// new一個這個外掛的例項,並傳入相關的引數
        }),
        new CleanWebpackPlugin(['dist']),  // 傳入所要清理的資料夾名稱
        new webpack.HotModuleReplacementPlugin() // 熱更新外掛 
    ]
}

此時我們重新啟動專案npm run dev後,修改hello.js的內容,會發現瀏覽器預覽效果會自動重新整理(也許反應會比較慢,因為我們使用了source-map和其他配置的影響,後面程式碼分離的時候我們再處理)。

七、專案優化及拓展

7.1 程式碼分離

在當前的開發環境都是提倡模組化,webpack自然不例外,我們前面的webpack.config.js配置檔案,其實也沒配置多少東西就這麼多了,要是以後增加了更多配置,豈不是看得眼花繚亂,所以最好的方法就是把它拆分,方便管理:

  1. 我們在根目錄下新建三個檔案,分別為webpack.common.jswebpack.dev.jswebpack.prod.js,分別代表公共配置檔案、開發環境配置檔案、生產環境(指專案上線時的環境)配置檔案。
  2. 安裝一個合併模組外掛:
cnpm i webpack-merge -D
  1. webpack.config.js的程式碼拆分到上述新建的三個檔案中,然後把webpack.config.js檔案刪除,具體如下:
// webpack.common.js
const path = require('path');  // 路徑處理模組
const webpack = require('webpack');  // 這個外掛不需要安裝,是基於webpack的,需要引入webpack模組
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入HtmlWebpackPlugin外掛

module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口檔案
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "bundle.js" //打包後輸出檔案的檔名
    },
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {
                test: /\.(scss|sass)$/,   // 正則匹配以.scss和.sass結尾的檔案
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是這個順序,因為呼叫loader是從右往左編譯的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use選擇如果有多項配置,可寫成這種物件形式
                    loader: "babel-loader"
                },
                exclude: /node_modules/   // 排除匹配node_modules模組
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版權所有,翻版必究'),  // new一個外掛的例項 
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "/src/index.template.html")// new一個這個外掛的例項,並傳入相關的引數
        }),
        new webpack.HotModuleReplacementPlugin()
    ]
}
// webpack.dev.js
const merge = require('webpack-merge');  // 引入webpack-merge功能模組
const common = require('./webpack.common.js'); // 引入webpack.common.js

module.exports = merge(common, {   // 將webpack.common.js合併到當前檔案
    devServer: {
        contentBase: "./dist",   // 本地伺服器所載入檔案的目錄
        port: "8088",  // 設定埠號為8088
        inline: true,  // 檔案修改後實時重新整理
        historyApiFallback: true, //不跳轉
        hot: true     //熱載入
    }
})
// webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 引入CleanWebpackPlugin外掛

module.exports = merge(common, { // 將webpack.common.js合併到當前檔案
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    plugins: [
        new CleanWebpackPlugin(['dist']),  // 所要清理的資料夾名稱
    ]
})

此時我們的專案目錄如下:

17

  1. 設定package.jsonscripts命令:
{
  "name": "webpack-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --config webpack.prod.js",
    "dev": "webpack-dev-server --open --config webpack.dev.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-preset-env": "^1.7.0",
    "babel-preset-react": "^6.24.1",
    "clean-webpack-plugin": "^0.1.19",
    "css-loader": "^1.0.0",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.9.4",
    "react": "^16.6.0",
    "react-dom": "^16.6.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10",
    "webpack-merge": "^4.1.4"
  }
}

我們把build命令改為了webpack --config webpack.prod.js,意思是把打包配置指向webpack.prod.js配置檔案,而之前我們只需要使用一個webpack命令為什麼就可以運行了?因為webpack命令是預設指向webpack.config.js這個檔名稱了,現在我們把檔名稱改了,所以就需要自定義指向新的檔案,dev命令中的指令也同理。

然後我們執行npm run buildnpm run dev,效果應該和我們分離程式碼前是一樣的。

注:說道package.json檔案,順便就多提幾句,因為也許有些朋友可能對我們安裝模組時加的-D-S-g命令存在一些疑惑,因為不知道什麼時候加什麼尾綴。 其實這個package.json檔案是用於我們安裝依賴的,可以把它當成一份依賴安裝說明表,就是如果我們把專案上傳或者發給其他的開發同事,肯定不會把/node_modules資料夾也傳送過去,因為這太大了,不現實也沒必要。 開發同事只需要有這份package.json檔案,然後npm install就可以把我們所需要的依賴都安裝下來,但前提是package.json檔案上有記錄,這就是安裝模組時加上-D,-S命令的原因。 -D的全稱是--save-dev指開發環境時需要用到的依賴,會記錄在package.json檔案中的devDependencies選項中,而-S--save是指生產環境也就是上線環境中需要用到的依賴,會記錄在package.json檔案中的dependencies選項中,-g的全稱是--global指安裝全域性命令,就是我們在本電腦的任何專案中都能使用到的命令,比如安裝cnpm這個淘寶映象命令就會用到-g命令。 所以我們在安裝模組時一定不要忘了加上對應的尾綴命令,讓我們的模組有跡可循,否則其他的開發同事接手你的專案的話,會不會下班後(放學後)在門口等你就不知道了。

扯遠了,希望不要嫌棄,也是想講得更詳細嘛!

7.2 多入口多出口

到目前為止我們都是一個入口檔案和一個出口檔案,要是我不止一個入口檔案呢?下面我們來試試:

webpack.common.js中的entry入口有三種寫法,分別為字串、陣列和物件,平時我們用得比較多的是物件,所以我們把它改為物件的寫法,首先我們在src資料夾下新建two.js檔案,名稱任意。因為有多個入口,所以肯定得多個出口來進行一一對應了,所以entryoutput配置如下:

// webpack.common.js
...
module.exports = {
    entry: {
        index: path.join(__dirname, "/src/index.js"),
        two: path.join(__dirname, "/src/two.js")
    }, 
    output: {
        path: path.join( __dirname, "/dist"), //打包後的檔案存放的地方
        filename: "[name].js" //打包後輸出檔案的檔名
    },
    ...
}
// two.js
function two() {
    let element = document.createElement('div');
    element.innerHTML = '我是第二個入口檔案';
    return element;
}

document.getElementById('root').appendChild(two());

然後我們執行npm run build打包後發現/dist資料夾下會多出two.js檔案,同時index.html也會自動將two.js引入,然後我們執行npm run dev顯示如下:

18

7.3 增加css字首、分離css、消除冗餘css、分離圖片
  1. 增加css字首 平時我們寫css時,一些屬性需要手動加上字首,比如-webkit-border-radius: 10px;,在webpack中我們能不能讓它自動加上呢?那是必須的,首先肯定得安裝模組了:
cnpm i postcss-loader autoprefixer -D

安裝好這兩個模組後,在專案根目錄下新建postcss.config.js檔案:

// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')  // 引用autoprefixer模組
    ]
}

style.css中增加以下樣式:

/* style.css */
body {
    background: #999;
}

#root div{
    width: 200px;
    margin-top: 50px;
    transform: rotate(45deg); /* 這個屬性會產生字首 */
}

修改webpack.common.js檔案中的css-loader配置:

...
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: [            
                    {loader: 'style-loader'}, // 這裡採用的是物件配置loader的寫法
                    {loader: 'css-loader'},
                    {loader: 'postcss-loader'} // 使用postcss-loader
                ]  
            },
            ...
        ]
    },
    ...
}

然後我們執行npm run dev後css樣式中會自動新增字首,顯示如下:

19

  1. 分離css 雖然webpack的理念是把css、js全都打包到一個檔案裡,但要是我們想把css分離出來該怎麼做呢?
cnpm i [email protected] -D  // 加上@next是為了安裝最新的,否則會出錯

安裝完以上外掛後在webpack.common.js檔案中引入並使用該外掛:

// webpack.common.js
...
const ExtractTextPlugin = require('extract-text-webpack-plugin') //引入分離外掛

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ExtractTextPlugin.extract({  // 這裡我們需要呼叫分離外掛內的extract方法
                    fallback: 'style-loader',  // 相當於回滾,經postcss-loader和css-loader處理過的css最終再經過style-loader處理
                    use: ['css-loader', 'postcss-loader']
                })
            },
            ...
        ]
    },
    plugins: [
        ...
        new ExtractTextPlugin('css/index.css') // 將css分離到/dist資料夾下的css資料夾中的index.css
    ]
}

執行npm run build後會發現/dist資料夾內多出了/css資料夾及index.css檔案。

  1. 消除冗餘css 有時候我們css寫得多了,可能會不自覺的寫重複了一些樣式,這就造成了多餘的程式碼,上線前又忘了檢查,對於這方面,我們應該儘量去優化它,webpack就有這個功能。

cnpm i purifycss-webpack purify-css glob -D 安裝完上述三個模組後,因為正常來說是在生產環境中優化程式碼,所以我們應該是在webpack.prod.js檔案中進行配置,引入clean-webpack-pluginglob外掛並使用它們:

// webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 引入CleanWebpackPlugin外掛

const path = require('path');
const PurifyCssWebpack = require('purifycss-webpack'); // 引入PurifyCssWebpack外掛
const glob = require('glob');  // 引入glob模組,用於掃描全部html檔案中所引用的css

module.exports = merge(common, {   // 將webpack.common.js合併到當前檔案
    devtool: 'source-map',  // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度
    plugins: [
        new CleanWebpackPlugin(['dist']),  // 所要清理的資料夾名稱
        new PurifyCssWebpack({
            paths: glob.sync(path.join(__dirname, 'src/*.html')) // 同步掃描所有html檔案中所引用的css
        })
    ]
})

我們在style.css檔案中增加一些多餘的程式碼試試:

/* style.css */
body {
    background: #999;
}

#root div{
    width: 200px;
    margin-top: 50px;
    transform: rotate(45deg); /* 這個屬性會產生字首 */
}

.a{                 /* 冗餘css */
    color: black;     
}

.b{                 /* 冗餘css */
    width: 50px;
    height: 50px;
    background: yellow;
}

然後我們執行npm run build後發現打包後的index.css中是沒有多餘的.a.b程式碼的:

/* index.css */

body {
  background: #999;
}

#root div {
  width: 200px;
  margin-top: 50px;
  -webkit-transform: rotate(45deg);
  transform: rotate(45deg);
  /* 這個屬性會產生字首 */
}
/*# sourceMappingURL=index.css.map*/
  1. 處理圖片 到目前為止我們還沒講到圖片的問題,如果要使用圖片,我們得安裝兩個loader:
// 雖然我們只需使用url-loader,但url-loader是依賴於file-loader的,所以也要安裝
cnpm i url-loader file-loader -D 

然後在webpack.common.js中配置url-loader

// webpack.common.js
...
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader']
                })
            },
            {
                test: /\.(png|jpg|svg|gif)$/,  // 正則匹配圖片格式名
                use: [
                    {
                        loader: 'url-loader'  // 使用url-loader
                    }
                ]
            },
            ...
        ]
    },
    ...
}

我們修改一下style.css,把背景改為圖片背景:

/* style.css */
body {
    background: url(../images/coffee.png) top right repeat-y;  /* 設為圖片背景 */
}

#root div{
    width: 200px;
    margin-top: 50px;
    transform: rotate(45deg); /* 這個屬性會產生字首 */
}

.a{
    color: black;
}

.b{
    width: 50px;
    height: 50px;
    background: yellow;
}

執行npm run dev後顯示如下:

20

但是背景圖片變成了base64,因為webpack會自動優化圖片,減少傳送請求,但是如果我想把它變成路徑的該怎麼做?

我們可以把webpack.common.js的loader配置更改一下,增加options選項:

// webpack.common.js
...
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader']
                })
            },
            {
                test: /\.(png|jpg|svg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 1000  // 限制只有小於1kb的圖片才轉為base64,例子圖片為1.47kb,所以不會被轉化
                        }
                    }
                ]
            },
            ...
        ]
    },
    ...
}

然後我們執行npm run build後,再執行npm run dev,額,圖片是沒有轉成base64了,但是圖片怎麼不顯示了?

21

問題就出在路徑上,我們之前圖片的路徑是在../images資料夾下,但是打包出來後沒有這個路徑了,圖片直接和檔案同級了,所以我們需要在webpack.common.js中給它設定一個資料夾:

// webpack.common.js
...
module.exports = {
    ...
    module: {
        rules: [
            ...
            {
                test: /\.(png|jpg|svg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 1000,  // 限制只有小於1kb的圖片才轉為base64,例子圖片為1.47kb,所以不會被轉化
                            outputPath: 'images'  // 設定打包後圖片存放的資料夾名稱
                        }
                    }
                ]
            },
            ...
        ]
    },
    ...
}

繼續npm run build打包再npm run dev執行,我的天!圖片還是不顯示! 除錯工具上看圖片路徑有images檔案夾了,但是我的../呢?

22

這又涉及到配置路徑的問題上了,我們還需要在css-loader中給背景圖片設定一個公共路徑publicPath: '../',如下:

// webpack.common.js
...
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正則匹配以.css結尾的檔案
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader'],
                    publicPath: '../'  // 給背景圖片設定一個公共路徑
                })
            },
            {
                test: /\.(png|jpg|svg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 1000,  // 限制只有小於1kb的圖片才轉為base64,例子圖片為1.47kb,所以不會被轉化
                            outputPath: 'images'  // 設定打包後圖片存放的資料夾名稱
                        }
                    }
                ]
            },
            ...
        ]
    },
    ...
}

現在再npm run build打包再npm run dev啟動,OK!沒毛病!

23

是不是很熱鬧?到現在我們不知不覺中也同時解決了圖片分離的問題,偷偷高興一下吧!

7.4 壓縮程式碼

在webpack4.x版本中當你打包時會自動把js壓縮了,而且npm run dev執行伺服器時,當你修改程式碼時,熱更新很慢,這是因為你修改後webpack又自動為你打包,這就導致了在開發環境中效率很慢,所以我們需要把開發環境和生產環境區分開來,這時就體現出我們程式碼分離的便捷性了,webpack.dev.js代表開發環境的配置,webpack.prod.js代表生產環境的配置,這時我們只要在package.json檔案中配置對應環境的命令即可:

{
  ...
  "scripts": {
    "build": "webpack --config webpack.prod.js --mode production",
    "dev": "webpack-dev-server --open --config webpack.dev.js --mode development"
  },
  ...
  }
}

--mode production表示打包時是生產環境,會自己將js進行壓縮,而--mode development表示當前是開發環境,不需要進行壓縮。這同時也解決了之前一直遺留的警告問題:

24

總結

好了,到現在我們基本把webapck常用的功能都走了一遍,寫得有點長,感謝你能仔細的看到這裡,希望能對你有所幫助,如果有發現不對的地方,也請多多指教。其實webpack還有很多功能,這裡也沒講述完全,但相信你現在對webpack也有了一定的瞭解,更多的webpack探索一定難不倒你!