webpack4入門

笪笪發表於2019-08-26

前言

webpack 作為前端領域的模組化打包工具,相信大家都不陌生。現在很火的 react 和 vue 的一些腳手架都是基於 webpack 開發定製的,因此,瞭解並會配置 webpack 還是很有必要的(文章基於 webpack4.x 版本來講解)。

PS:文章內容可能有點長,大家提前做好心理準備。

1.webpack 是什麼

官方定義
webpack 是一個現代 JavaScript 應用程式的靜態模組打包器。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖,其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle。

個人理解
webpack 作為一個模組化打包工具,根據入口檔案(任何型別檔案,不一定是 js 檔案)遞迴處理模組中引入的 js/css/scss/image 等檔案,將其轉換打包為瀏覽器可以識別的基礎檔案(js/css/image 檔案等)。

webpack

與 grunt/gulp 等區別:
1.grunt 與 gulp 屬於自動化流程工具,通過配置檔案指明對哪些檔案執行編譯、組合、壓縮等具體任務,由工具自動完成這些任務。
2.webpack 作為模組化打包工具,把專案作為一個整體,通過入口檔案,遞迴找到所有依賴檔案,通過 loader 和 plugin 針對檔案進行處理,最後打包生成不同的 bundle 檔案。

2.webpack 基本配置

       當你想使用 webpack 打包專案時,需要在專案目錄下新建 webpack.config.js,webpack 預設會讀取 webpack.config.js 作為配置檔案,進而執行打包構建流程。

       先來看一下 webpack 的基本配置項,留個印象先。

webpack.config.js

const path = require('path');

module.exports = {
  mode: 'production/development/none', // 打包模式,使用對應模式的內建優化
  entry: './src/index.js', // 入口,支援單入口、多入口
  output: { // 輸出相關配置
    filename: 'xx.js', // 輸出檔案的檔名
    path: path.resolve(__dirname, 'dist') // 輸出檔案的絕對路徑,預設為dist
  },
  module: { // 針對不同型別檔案的轉換
    rules: [
      {
        test: /\.xx$/, // 針對某型別檔案處理,使用正則匹配檔案
        use: [
          {
            loader: 'xx-loader', // 使用xx-loader進行轉換
            options: {} // xx-loader的配置
          }
        ]
      }
    ]
  },
  plugins: [ // 外掛,完成特定任務,如壓縮/拆分
    new xxPlugin({ options });
  ]
};

複製程式碼

       webpack 有五個核心概念:入口(entry)、輸出(output)、模式(mode)、loader、外掛(plugins)。

2.1.入口(entry)

入口指示 webpack 應該使用哪個模組,來作為構建其內部依賴圖的開始。預設值為./src

2.1.1.單入口

單入口是指 webpack 打包只有一個入口,單入口支援單檔案和多檔案打包。

通常像 vue/react spa 應用都屬於單入口形式,以src/index.js作為入口檔案。

(1)單檔案打包

不指定入口檔案的 entryChunkName 時,預設為 main。

// webpack.config.js

module.exports = {
  entry: "./src/index.js"
};
複製程式碼

上面的單入口語法,是下面的簡寫:

module.exports = {
  entry: {
    main: "./src/index.js"
  }
};
複製程式碼

main 表示 entryChunkName 為 main,打包後生成的檔案 filename 為 main。

webpack 打包後,dist 資料夾生成 main.js 檔案。

webpack打包單檔案

也可以將 entryChunkName 修改為其他值,打包出的 filename 也會對應改變。

(2)多檔案打包

多檔案打包入口以陣列形式表示,表示將多個檔案一起注入到 bundle 檔案中。

module.exports = {
  entry: ["./src/index.js", "./src/main.js"]
};

複製程式碼

2.1.2.多入口

多入口是指 webpack 打包有多個入口模組,多入口 entry 一般採用物件語法表示,應用場景:

(1)分離應用程式 app 和第三方入口(vendor)

module.exports = {
  entry: {
    app: "./src/index.js",
    vendor: "./src/vendor.js"
  }
};
複製程式碼

webpack 打包後,生成應用程式 app.js 和 vendor.js。

分離應用程式和第三方

(2)多頁面打包,一般指多個 html 文件形式,每個文件只使用一個入口。

module.exports = {
  entry: {
    app: "./src/app.js",
    home: "./src/home.js",
    main: "./src/main.js"
  }
};
複製程式碼

webpack 打包後,dist 資料夾下生成 app.js、home.js、main.js 三個檔案。

多入口打包

2.2.輸出(output)

output 選項可以控制 webpack 如何輸出打包檔案,output 屬性包含 2 個屬性:

  • filename:輸出檔案的檔名
  • path:輸出目錄的絕對路徑(注意是絕對路徑)

即使存在多個入口起點,webpack 只有一個輸出配置,不對 output 進行配置時,預設輸出到./dist 資料夾。

2.2.1.單入口輸出

單入口打包常用配置如下:

const path = require("path");

module.exports = {
  entry: {
    app: "./src/app.js"
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist") // __dirname表示js檔案執行的絕對路徑,使用path.resolve生成dist資料夾的絕對路徑
  }
};
複製程式碼

webpack 打包後,dist 資料夾下生成 bundle.js 檔案

單入口輸出

2.2.2.多入口輸出

當存在多入口時,應該使用佔位符來確保每個檔案具有唯一的名稱,否則 webpack 打包會報錯。

webpack打包報錯

佔位符 name 與 entry 物件中的 key 一一對應

正確的寫法如下:

const path = require("path");

module.exports = {
  entry: {
    app: "./src/app.js",
    main: "./src/main.js",
    home: "./src/home.js"
  },
  output: {
    filename: "[name].js", // 使用佔位符來表示
    path: path.resolve(__dirname, "dist")
  }
};
複製程式碼

webpack 打包後,在 dist 資料夾下生成了 app.js、home.js、main.js 檔案。

多入口輸出

2.2.3.hash、chunkhash、contenthash 揭祕

在揭祕 hash、chunkhash、contenthash 之前,我們先看下 webpack 打包輸出資訊。

webpack打包資訊

Hash:與整個專案構建相關,當專案中不存在檔案內容變更時,hash 值不變,當存在檔案修改時,會生成新的一個 hash 值。
Version:webpack 版本
Time:構建時間
Build at:開始構建時間
Asset:輸出檔案
Size:輸出檔案大小
Chunks:chunk id
ChunkNames:對應 entryChunkName
Entrypoint:入口與輸出檔案的對應關係

如果使用佔位符來表示檔案,當檔案內容變更時,仍然生成同樣的檔案,無法解決瀏覽器快取檔案問題。藉助於 hash、chunkhash、contenthash 可以有效解決問題。

(1)hash

整個專案構建生成的一個 md5 值,專案檔案內容不變,hash 值不變。

使用 hash 關聯輸出檔名稱

const path = require("path");

module.exports = {
  entry: {
    app: "./src/app.js",
    main: "./src/main.js",
    home: "./src/home.js"
  },
  output: {
    filename: "[name].[hash].js",
    path: path.resolve(__dirname, "dist")
  }
};

複製程式碼

filename: "[name].[hash:7].js"表示去 hash 值的前 7 位

webpack 打包,看到新生成檔案帶上了 hash 值

hash

當我們修改 app.js 檔案內容後,重新打包,發現可以生成了新的 hash 值,所有檔案的名稱都發生了變更。

新hash

問題:當我修改了專案中的任何一個檔案時,導致未修改檔案快取都將失效。

(2)chunkhash

webpack 構建時,根據不同的入口檔案,構建對應的 chunk,生成對應的 hash 值,每個 chunk 的 hash 值都是不同的。

使用 chunkhash 關聯檔名

const path = require("path");

module.exports = {
  entry: {
    app: "./src/app.js",
    main: "./src/main.js",
    home: "./src/home.js"
  },
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  }
};

複製程式碼

使用 webpack 打包後,dist 目錄下,每個 bundle 檔案都帶有不同的 chunkash 值。

chunkhash

修改 app.js 內容,重新打包,只有 app 檔名稱發生了變更。

新chunkash

使用 chunkhash 可以有效解決 hash 快取失效問題,但是當在 js 檔案裡面引入 css 檔案時,將 js、css 分別打包,若 js 件內容變化時,css 檔名稱也會變更。

app.js 中引入了 css 檔案

import "./css/style.css";

console.log("app");
複製程式碼

webpack 配置

const path = require("path");
const miniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  entry: {
    app: "./src/app.js",
    main: "./src/main.js",
    home: "./src/home.js"
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [miniCssExtractPlugin.loader, "css-loader"]
      }
    ]
  },
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(), // 清空dist目錄
    new miniCssExtractPlugin({
      // 抽離css檔案
      filename: "css/style.[chunkhash].css"
    })
  ]
};
複製程式碼

打包,dist 資料夾下生成了 css 與 js 檔案,chunkhash 一致。

chunkhash-css-js

當我們修改 app.js 檔案內容後,重新打包,發現 css 檔名也變更了,css 檔案快取將失效,這顯然不是我們想要的結果。

chunk-css-js1

問題:js 引入 css 等其他檔案時,js 檔案變更,css 等檔名也會變更,快取失效。

(3)contenthash

contenthash 表示由檔案內容產生的 hash 值,內容不同產生的 contenthash 值也不一樣。藉助於 contenthash 可以解決上述問題,只要 css 檔案不變,快取一直有效。

修改 webpack 配置,css filename 使用 contenthash 表示。

const path = require("path");
const miniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  entry: {
    app: "./src/app.js",
    main: "./src/main.js",
    home: "./src/home.js"
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [miniCssExtractPlugin.loader, "css-loader"]
      }
    ]
  },
  output: {
    filename: "[name].[contenthash].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(), // 清空dist目錄
    new miniCssExtractPlugin({
      // 抽離css檔案
      filename: "css/style.[contenthash].css"
    })
  ]
};

複製程式碼

打包後,dist 目錄下,生成了 css、js 檔案,app 檔案包含 chunkhash 值,css 檔案包含 contenthash 值。

contenthash
;

修改 app.js 檔案內容,重新打包,app 檔案重新命名了,css 檔案沒變,快取有效。

contenthash1

專案中 css 等非 js 檔案抽離最好使用 contenthash。

2.3.模式(mode)

webpack 提供了 mode 配置選項,用來選擇使用響應的內建優化,不配置 mode 選項時,預設使用 production 模式。

mode 選項有 3 個可選值:production(生產模式、預設)、development(開發模式)、none。

2.3.1.production 模式

production 模式下,會自動開啟 Tree Shaking(去除無用程式碼)和檔案壓縮(UglifyJs)。

在 fun.js 中定義了 2 個函式

export function f1() {
  console.log("f1");
}

export function f2() {
  console.log("f2");
}
複製程式碼

在 app.js 中只引入了 f1

import { f1 } from "./fun";

f1();
複製程式碼

production 模式打包,檢視打包後的檔案,只引入了 f1,並且程式碼進行了壓縮。

production模式打包

2.3.2.development 模式

development 模式下,webpack 會啟用 NamedChunksPlugin 和 NamedModulesPlugin 外掛。

同樣的程式碼,development 模式下打包,將 f1 和 f2 都一起打包了,而且程式碼並沒有進行壓縮。

development模式下打包

2.3.3.none 模式

none 模式下,webpack 不會使用任何內建優化,這種模式很少使用。

2.4.loader

loader 用於對模組的原始碼進行轉換。loader 可以實現檔案內容的轉換,比如將 es6 語法轉換為 es5 語法,將 scss 轉換為 css 檔案,將 image 轉換為 base 64 位編碼。一般 loader 配置在 module 的 rules 選項中。

常用的 loader 有:

  • 處理 js/jsx/ts
    babel-loader:將程式碼轉換為 ES5 程式碼
    ts-loader:將 ts 程式碼轉換為 js 程式碼
  • 處理樣式
    style-loader:將模組的匯出作為樣式新增到 DOM style 中
    css-loader:解析 css 檔案後,使用 import 載入,並且返回 CSS 程式碼
    less-loader:載入和轉譯 less 檔案
    sass-loader:載入和轉譯 sass/scss 檔案
  • 檔案轉換
    file-loader:將檔案傳送到輸出資料夾,返回相對 url,一般用於處理圖片、字型
    url-loader:和 file-loader 功能一樣,但如果檔案小於限制,返回 data URL,常用於圖片 base 64 轉換

下面就以 scss 轉換的例子,描述如何使用 loader

app.js 中引入了 main.scss 檔案

// app.js

import "./css/main.scss";
複製程式碼

webpack 配置如下

const path = require("path");
const miniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  mode: "development",
  entry: {
    app: "./src/app.js"
  },
  module: { // 針對專案中不同型別模組的處理
    rules: [ // 匹配請求的規則陣列
      {
        test: /\.scss$/, // 檢測scss檔案結尾的檔案
        exclude: /node_modules/, // 排除查詢範圍
        include: [path.resolve(__dirname, "src/css")], // 限定查詢範圍
        use: [miniCssExtractPlugin.loader, "css-loader", "sass-loader"] // loader鏈式呼叫,從最右邊向左處理
      }
    ]
  },
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(), // 清空dist目錄
    new miniCssExtractPlugin({
      // 抽離css檔案
      filename: "css/style.[contenthash].css"
    })
  ]
};

複製程式碼

其中,sass-loader 用於將 scss 檔案編譯成 css 檔案,css-loader 用於解釋 import(),miniCssExtractPlugin 用於將 css 抽離到單獨的檔案中。

關於 loader 有幾點說明:

1.loader 支援鏈式呼叫,一組鏈式 loader 按照相反的順序執行,loader 鏈中的前一個 loader 返回值給下一個 loader,最後一個 loader 輸出檔案。
上面例子中,loader 執行順序:sass-loader => css-loader => miniCssExtractPlugin.loader。

2.loader 可以使用 options 物件進行配置,像下面這樣:

module: {
    //
    rules: [
      {
        test: /\.scss$/, // 檢測scss檔案結尾的檔案
        exclude: /node_modules/, // 排除查詢範圍
        include: [path.resolve(__dirname, "src/css")], // 限定查詢範圍
        use: [
          miniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              modules: true
            }
          },
          "sass-loader"
        ] // loader鏈式呼叫,從最右邊向左處理
      }
    ]
  },
複製程式碼

2.5.plugins(外掛)

外掛是 webpack 的支柱功能,旨在解決 loader 無法實現的其他事。外掛可以攜帶引數,配置外掛需要向 plugins 陣列中傳入 new 例項。

常用的外掛有:

clean-webpack-plugin:清空 dist 資料夾
html-webpack-plugin:生成 html 檔案
mini-css-extract-plugin:抽離 css 檔案
optimize-css-assets-webpack-plugin:優化和壓縮 css
css-split-webpack-plugin:針對 css 大檔案進行拆分
webpack-bundle-analyzer:webpack 打包結果分析
webpack.DllPlugin:建立 dll 檔案和 manifest 檔案
webpack.DllReferencePlugin:把只有 dll 的 bundle 引用到需要的預編譯的依賴。
SplitChunksPlugin:拆分程式碼塊,在 optimization.splitChunks 中配置。

下面以 html-webpack-plugin 為例說明 plugins 的用法,這裡只列出 plugins 部分的配置

plugins: [
    new CleanWebpackPlugin(), // 清空dist目錄
    new miniCssExtractPlugin({
      // 抽離css檔案
      filename: "css/style.[contenthash].css"
    }),
    new HtmlWebpackPlugin({ // 生成外掛例項
      filename: "index.html", // 生成模板的名稱
      minify: {
        collapseWhitespace: true, // 去除空格
        minifyCSS: true, // 壓縮css
        minifyJS: true, // 壓縮js
        removeComments: true, // 移除註釋
        removeEmptyElements: true, // 移除空元素
        removeScriptTypeAttributes: true, // 移除script type屬性
        removeStyleLinkTypeAttributes: true // 移除link type屬性
      }
    })
  ]
複製程式碼

打包後,生成了 index.html 檔案

html-webpack-plugin1

開啟 index.html,看到 css 和 js 檔案被引入了

html-webpack-plugin2

接下來描述 webpack 其他常用的一些配置 resolve、devServer、devtool。

2.6.resolve(解析)

resolve 選項設定模組如何被解析。

2.6.1.alias

建立 import 或 require 的別名,確保模組引入變得簡單。

下面的例子針對 css、util 資料夾 設定了 alias 別名,引入資料夾下面的檔案可以直接使用相對地址。

resolve: {
    alias: {
      css: path.resolve(__dirname, "src/css"),
      util: path.resolve(__dirname, "src/util")
    }
  },
複製程式碼

app.js 檔案中引入 util 資料夾下的 common.js 檔案,就會引入 src/util/common.js 檔案。

import fun1 from "util/common.js";
複製程式碼

2.6.2.extensions

自動解析引入模組的擴充套件,按照從左到右的順序解析。

resolve: {
  extensions: [".js", ".json"]
}
複製程式碼

在 app.js 中引入 common.js 可以不攜帶字尾,由 webpack 自動解析。

import fun1 from "util/common";
複製程式碼

2.6.3.mainFiles

解析目錄時要使用的檔名,預設

mainFiles: ["index"]
複製程式碼

也可以指定多個 mainFiles,會依次從左到右解析

mainFiles: ["index", "main"]
複製程式碼

比如需要從 util 資料夾下引入 index.js 檔案,import 只需要匯入到 util 資料夾,webpack 會自動從 util 資料夾下引入 index.js 檔案。

import index from "util";
複製程式碼

extentsion 和 mainFiles 屬性雖然會方便開發者簡寫,但是會增加 webpack 額外的解析時間。

2.7.devServer

devServer 主要用於 development 模式配置本地開發伺服器,需要安裝 webpack-dev-server。

npm i webpack-dev-server -g
複製程式碼

devServer 常用的配置項如下:

devServer: {
    contentBase: path.resolve(__dirname, "dist"), // 告訴伺服器從哪裡提供內容
    host: "localhost", // 制定一個host,預設localhost
    port: 3000, // 請求埠號,預設8080
    compress: true, // 啟用gzip壓縮
    https: true, // 開啟http服務
    hot: true, // 啟用模組熱替換
    open: true, // 自動開啟預設瀏覽器
    index: "index.html", // 頁面入口html檔案,預設index.html
    headers: {
      // 所有響應中新增首部內容
      "X-Custom-Foo": "bar"
    },
    proxy: {
      "/api": "http://localhost:3000"
    }
  }
複製程式碼

讀取配置檔案,啟動開發伺服器。

webpack-dev-server --config webpack.dev.js
複製程式碼

2.8.devtool

source map 一個儲存原始碼與編譯程式碼對應位置的對映資訊檔案,它是專門給偵錯程式準備的,它主要用於 debug。

webpack 通過配置 devtool 屬性來選擇一種 source map 來增強除錯過程。

以下是官方對於 devtool 的各種 source map 的比較:

devtool

development 模式下 devtool 設定為 cheap-module-eval-source-map,production 模式下 devtool 設定為 souce-map。

3.webpack 實踐

接下來將通過一個完整的例子實現 react 專案的完整 webpack 配置。 先全域性安裝 webpack 和 webpack-dev-server。

npm i webpack webpack-dev-server -g
複製程式碼

3.1.配置執行檔案

新建一個目錄,結構如下:

目錄完整結構

其中 public 資料夾下包含 index.html 入口 html 檔案,src 資料夾下包含 index.js 入口 js 檔案,css 資料夾、font 資料夾、image 資料夾。

webpack 配置如下:

// webpack.config.js

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "[name].[chunkhash:7].js",
    path: path.resolve(__dirname, "dist")
  }
};
複製程式碼

在目錄下使用npm init新建 package.json 檔案,設定 dev 和 build 的 script, 分別用於開發模式和生產模式。

package.json

3.1.處理 jsx、es6

在 react 專案中,我們使用 jsx 和 es6 語法,為了相容低版本瀏覽器,需要通過 babel 轉換。

先安裝 babel 相關依賴包

npm i babel-loader @babel/core @babel/preset-env  @babel/plugin-transform-runtime  @babel/preset-react @babel/polyfill @babel/runtime -D
複製程式碼

babel-loader:處理 ES6 語法,將其編譯為瀏覽器可以執行的 js 語法
@babel/core-babel:babel 核心模組
@babel/preset-env:轉換 es6 語法,支援最新的 javaScript 語法
@babel/preset-react:轉換 jsx 語法
@babel/plugin-transform-runtime: 避免 polyfill 汙染全域性變數,減小打包體積
@babel/polyfill: ES6 內建方法和函式轉化墊片

將 index.js 作為入口檔案,引入 App.jsx 元件

//index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./views/App";
console.log(App);

ReactDOM.render(<App />, document.getElementById("root"));

//App.jsx
import React, { Component } from "react";

class App extends Component {
  render() {
    return <h2>This is a react app.</h2>;
  }
}

export default App;
複製程式碼

webpack 配置如下:

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); // clean-webpack-plugin用來清空dist資料夾

module.exports = {
  mode: "production",
  entry: {
    app: "./src/index.js"
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader"
          }
        ]
      }
    ]
  },
  resolve: {
    extensions: [".jsx", ".js"]
  },
  output: {
    filename: "[name].[chunkhash:7].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [new CleanWebpackPlugin()]
};
複製程式碼

clean-webpack-plugin:清空 dist 資料夾

新建.babelrc 檔案

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["@babel/plugin-transform-runtime"]
}
複製程式碼

執行npm run build,打包成功,在 dist 資料夾下生成了 app.js 檔案

jsx轉換

3.2.配置 html 模板

配置 html 模板表示配置 index.html 檔案相關配置,將打包後的檔案引入到 index.html 檔案,通過 html-webpack-plugin 外掛實現。

先安裝 html-webpack-plugin 外掛

npm i html-webpack-plugin -D
複製程式碼

webpack 配置如下:

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); // 引入html-webpack-plugin外掛

module.exports = {
  mode: "production",
  entry: {
    app: "./src/index.js"
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader"
          }
        ]
      }
    ]
  },
  resolve: {
    extensions: [".jsx", ".js"]
  },
  output: {
    filename: "[name].[chunkhash:7].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: "index.html", // 模板檔名
      template: path.resolve(__dirname, "public/index.html"), // 模板檔案源
      minify: {
        collapseWhitespace: true, // 壓縮空格
        minifyCSS: true, // 壓縮css
        minifyJS: true, // 壓縮js
        removeComments: true, // 移除註釋
        caseSensitive: true, // 去除大小寫
        removeScriptTypeAttributes: true, // 移除script的type屬性
        removeStyleLinkTypeAttributes: true // 移除link的type屬性
      }
    })
  ]
};
複製程式碼

執行 npm run build,打包成功,在 dist 資料夾下生成了 index.html 和 app.js 打包檔案

模板配置

開啟 index.html,引入了 app.js 打包檔案

webpack4入門

3.3.編譯 css、scss

在 index.js 中引入 main.scss 檔案

import React from "react";
import ReactDOM from "react-dom";
import App from "./views/App";
import "./css/main.scss";

ReactDOM.render(<App />, document.getElementById("root"));
複製程式碼

當在 js 檔案中引入 css/scss 檔案時,需要經過 loader 轉換,才能引入到 index.html 檔案中。

安裝相關依賴包

npm i css-loader sass-loader node-sass mini-css-extract-plugin optimize-css-assets-webpack-plugin css-split-webpack-plugin -D
複製程式碼

sass-loader:將 scss/sass 檔案編譯為 css
css-loader:解析 import/require 匯入的 css 檔案
mini-css-extract-plugin:將 js 中引入的 css 檔案抽離成單獨的 css 檔案
optimize-css-assets-webpack-plugin:優化和壓縮 css 檔案
css-split-webpack-plugin:css 檔案拆分

webpack 配置如下:

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const CSSSplitWebpackPlugin = require("css-split-webpack-plugin").default;

module.exports = {
  mode: "production",
  entry: {
    app: "./src/index.js"
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        exclude: /node_modules/,
        use: ["babel-loader"]
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
      }
    ]
  },
  resolve: {
    extensions: [".jsx", ".js"]
  },
  output: {
    filename: "[name].[chunkhash:7].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "css/[name].[hash:7].css",
      chunkFilename: "[id].css"
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require("cssnano"), // //引入cssnano配置壓縮選項
      cssProcessorPluginOptions: {
        preset: [
          "default",
          {
            discardComments: {
              // 移除註釋
              removeAll: true
            },
            normalizeUnicode: false
          }
        ]
      },
      canPrint: true
    }),
    new CSSSplitWebpackPlugin({
      size: 4000, // 超過4kb的css檔案進行拆分
      filename: "[name]-[part].[ext]"
    }),
    new HtmlWebpackPlugin({
      filename: "index.html", // 模板檔名
      template: path.resolve(__dirname, "public/index.html"), // 模板檔案源
      minify: {
        collapseWhitespace: true, // 壓縮空格
        minifyCSS: true, // 壓縮css
        minifyJS: true, // 壓縮js
        removeComments: true, // 移除註釋
        caseSensitive: true, // 去除大小寫
        removeScriptTypeAttributes: true, // 移除script的type屬性
        removeStyleLinkTypeAttributes: true // 移除link的type屬性
      }
    })
  ]
};
複製程式碼

執行npm run build,在 dist 資料夾下生成了 css 資料夾和編譯的 css 檔案

編譯css、scss檔案

開啟 index.html,css 檔案被引入到 index.html 中。

編譯css、scss到index.html

3.4.處理圖片、字型檔案

在 index.js 中引入圖片,在 main.scss 檔案中引入字型庫

// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./views/App";
import "./css/main.scss";
import image from "./image/image1.png";

const newImage = new Image();
newImage.src = image;
newImage.style.cssText = "width: 100px; height: 100px;";
document.body.append(newImage);

ReactDOM.render(<App />, document.getElementById("root"));
複製程式碼

在 main.scss 檔案中引入字型庫

// main.scss
@font-face {
  font-family: 'MyFont';
  src: url('../font/icomoon.eot') format('eot'),
    + url('../font/icomoon.woff') format('woff');
  font-weight: 600;
  font-style: normal;
}

body {
  background-color: blue;
}
複製程式碼

當在 js 檔案中引入圖片或字型檔案時,需要通過 url-loader 和 file-loader 來處理。

file-loader:解析 import/require 匯入的檔案,將其輸出到生產目錄,併產生一個 url 地址。
url-loader:不超過限定 limit 時,轉換為 base64 url。

安裝 file-loader 和 url-loader

npm i url-loader file-loader -D
複製程式碼

webpack module 部分配置如下:

...
module: {
  ...
  rules: [
    {
        test: /\.(png|jpg|jpeg|gif|svg)/,
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name]_[hash].[ext]",
              outputPath: "images/",
              limit: 204800 // 小於200kb,進行base64轉碼
            }
          }
        ]
      },
      {
        test: /\.(eot|woff2?|ttf)/,
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name]-[hash:5].min.[ext]",
              limit: 5000,
              outputPath: "fonts/"
            }
          }
        ]
      }
  ]
  ...
}
...
複製程式碼

執行npm run build打包,在 dist 目錄下生成了 image 資料夾和 font 資料夾

圖片字型轉換

開啟 index.html,圖片成功引入

圖片字型轉換

3.5.配置 devServer

webpack-dev-server 就是在本地為搭建了一個小型的靜態檔案伺服器,有實時重載入的功能,為將打包生成的資源提供了 web 服務,適用於本地開發模式。

 devServer: {
    contentBase: path.join(__dirname, "../dist"), // 資源目錄
    host: 'localhost', // 預設localhost
    port: 3000, // 預設8080
    hot: true, // 支援熱更新
    inline: true
  }
複製程式碼

執行npm run dev,web 服務起在 localhost:3000

devServer配置

3.6.提取公共程式碼

當我們在程式碼裡引入了第三方庫和公共程式碼時,可以使用 splitChunks 提取公共程式碼,避免載入的包太大。

webpack 配置如下:

...
optimization: {
    splitChunks: {
      // 提取公共程式碼
      chunks: "all", //  async(動態載入模組),initital(入口模組),all(全部模組入口和動態的)
      minSize: 3000, // 抽取出來的檔案壓縮前最小大小
      maxSize: 0, // 抽取出來的檔案壓縮前的最大大小
      minChunks: 1, // 被引用次數,預設為1
      maxAsyncRequests: 5, // 最大的按需(非同步)載入次數,預設為 5;
      maxInitialRequests: 3, // 最大的初始化載入次數,預設為 3;
      automaticNameDelimiter: "~", // 抽取出來的檔案的自動生成名字的分割符,預設為 ~;
      name: "vendor/vendor", // 抽取出的檔名,預設為true,表示自動生成檔名
      cacheGroups: {
        // 快取組
        common: {
          // 將node_modules模組被不同的chunk引入超過1次的抽取為common
          test: /[\\/]node_modules[\\/]/,
          name: "common",
          chunks: "initial",
          priority: 2,
          minChunks: 2
        },
        default: {
          reuseExistingChunk: true, // 避免被重複打包分割
          filename: "common.js", // 其他公共函式打包成common.js
          priority: -20
        }
      }
    }
  },
  ...
複製程式碼

執行npm run build,在 dist 資料夾下生成了 vendor.js 包

提取公共程式碼

開啟 index.html,vendor.js 成功引入

提取公共程式碼index.html

3.7.分離 webpack 配置檔案

由於開發環境和生產環境下的 webpack 配置存在公共配置,因此最好將公共配置抽離成 webpack.common.js,然後針對開發環境和生產環境分別配置,通過 webpack-merge merge 配置,即可滿足開發環境和生產環境不同的配置。

先安裝 webpack-merge,用來 merge webpack 配置項

npm i webpack-merge -D
複製程式碼

在目錄下新建 tools 資料夾,存放 webpack 相關配置

建立tools資料夾

新建 pathConfig.js 檔案,返回 entry js、output 目錄及 index.html 模板目錄的絕對地址

// pathConfig.js
const path = require("path");
const fs = require("fs");

const appDirectory = fs.realpathSync(process.cwd()); // 獲取當前根目錄
const resolvePath = (relativePath) => path.resolve(appDirectory, relativePath);

module.exports = {
  appHtml: resolvePath("public/index.html"), // 模板html
  appBuild: resolvePath("dist"), // 打包目錄
  appIndexJs: resolvePath("src/index.js") // 入口js檔案
};
複製程式碼

webpack 公共配置,引入 pathConfig.js 檔案

// webpack.common.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const CSSSplitWebpackPlugin = require("css-split-webpack-plugin").default;
const { appIndexJs, appBuild, appHtml } = require("./pathConfig");

module.exports = {
  entry: {
    app: appIndexJs
  },
  output: {
    filename: "[name].[hash:7].js",
    path: appBuild
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/, // jsx、js處理
        exclude: /node_modules/,
        use: ["babel-loader"]
      },
      {
        test: /\.(sa|sc|c)ss$/, // scss、css處理
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg)/, // 圖片處理
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name]_[hash].[ext]",
              outputPath: "images/",
              limit: 204800 // 小於200kb採用base64轉碼
            }
          }
        ]
      },
      {
        test: /\.(eot|woff2?|ttf)/, // 字型處理
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name]-[hash:5].min.[ext]",
              limit: 5000, // 5kb限制
              outputPath: "fonts/"
            }
          }
        ]
      }
    ]
  },
  resolve: {
    extensions: [".jsx", ".js"]
  },
  optimization: {
    splitChunks: {
      // 提取公共程式碼
      chunks: "all", //  async(動態載入模組),initital(入口模組),all(全部模組入口和動態的)
      minSize: 3000, // 抽取出來的檔案壓縮前最小大小
      maxSize: 0, // 抽取出來的檔案壓縮前的最大大小
      minChunks: 1, // 被引用次數,預設為1
      maxAsyncRequests: 5, // 最大的按需(非同步)載入次數,預設為 5;
      maxInitialRequests: 3, // 最大的初始化載入次數,預設為 3;
      automaticNameDelimiter: "~", // 抽取出來的檔案的自動生成名字的分割符,預設為 ~;
      name: "vendor/vendor", // 抽取出的檔名,預設為true,表示自動生成檔名
      cacheGroups: {
        // 快取組
        common: {
          // 將node_modules模組被不同的chunk引入超過1次的抽取為common
          test: /[\\/]node_modules[\\/]/,
          name: "common",
          chunks: "initial",
          priority: 2,
          minChunks: 2
        },
        default: {
          reuseExistingChunk: true, // 避免被重複打包分割
          filename: "common.js", // 其他公共函式打包成common.js
          priority: -20
        }
      }
    }
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "css/[name].[hash:7].css",
      chunkFilename: "[id].css"
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require("cssnano"), // //引入cssnano配置壓縮選項
      cssProcessorPluginOptions: {
        preset: [
          "default",
          {
            discardComments: {
              // 移除註釋
              removeAll: true
            },
            normalizeUnicode: false
          }
        ]
      },
      canPrint: true
    }),
    new CSSSplitWebpackPlugin({
      size: 4000, // 超過4kb進行拆分
      filename: "[name]-[part].[ext]"
    }),
    new HtmlWebpackPlugin({
      filename: "index.html", // 模板檔名
      template: appHtml, // 模板檔案源
      minify: {
        collapseWhitespace: true, // 壓縮空格
        minifyCSS: true, // 壓縮css
        minifyJS: true, // 壓縮js
        removeComments: true, // 移除註釋
        caseSensitive: true, // 去除大小寫
        removeScriptTypeAttributes: true, // 移除script的type屬性
        removeStyleLinkTypeAttributes: true // 移除link的type屬性
      }
    })
  ]
};
複製程式碼

開發環境 webpack 配置

// webpack.dev.config.js
const path = require("path");
const merge = require("webpack-merge");
const baseConfig = require("./webpack.config");

module.exports = merge(baseConfig, {
  mode: "development",
  devtool: "cheap-module-eval-source-map",
  devServer: {
    contentBase: path.join(__dirname, "../dist"),
    port: 3000,
    historyApiFallback: true,
    hot: true,
    inline: true
  }
});
複製程式碼

生產環境配置,啟用 webpack-bundle-analyzer 進行打包分析,啟用 compression-webpack-plugin 生成 gzip 壓縮。

// webpack.prod.config.js
const merge = require("webpack-merge");
const baseConfig = require("./webpack.com.config");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const CompressionWebpackPlugin = require("compression-webpack-plugin");

module.exports = merge(baseConfig, {
  mode: "production",
  devtool: "source-map",
  plugins: [
    new CompressionWebpackPlugin({
      filename: "[path].gz[query]", // path-原資源路徑,query-原查詢字串
      algorithm: "gzip", // 壓縮演算法
      threshold: 0, // 檔案壓縮閾值
      minRatio: 0.8 // 最小壓縮比例
    }),
    new BundleAnalyzerPlugin()
  ]
});
複製程式碼

修改 package.json 中 script 中 dev 和 build,--config 表示讀取後面的檔案作為配置檔案。

 "scripts": {
    "dev": "webpack-dev-server --config ./tools/webpack.dev.config.js",
    "build": "webpack --config ./tools/webpack.prod.config.js"
  },
複製程式碼

執行npm run build,專案打包成功

打包成功

執行npm run dev,啟動開發者模式,執行在 localhost:3000。

開發成功

到此為止,我們的案例就完成了。

程式碼地址:案例連結

結語

看完這篇文章,相信大家對於 webpack 已經有了一個初步的瞭解,學習 webpack 最好的方式還是多動手實踐,覺得不錯的小夥伴可以點個贊(碼字不易,灰常感謝)。

相關連結

webpack 官方連結:www.webpackjs.com/

相關文章