【webpack系列】從基礎設定到掌握進階用法

2023-06-25 12:00:35

前言

本篇文章將介紹一些webpack的進階用法,演示內容繼承自上一篇文章的內容,所以沒看過上一篇文章的建議先學習上一篇內容再閱讀此篇內容,會更有利於此篇的學習~

檔案指紋

檔案指紋指的是打包輸出的檔名字尾,一般用來做版本管理、快取等

webpack的指紋策略有三種:hashchunkhashcontenthash,它們之間最主要的區別就是每種hash影響的範圍不同。

預留位置

webpack提供預留位置用於將特定資訊附加在打包輸出的檔案上

名稱 含義
[ext] 資源字尾名
[id] 檔案識別符號
[name] 檔名稱
[path] 檔案的相對路徑
[folder] 檔案所在的資料夾
[hash] 模組識別符號的 hash,預設是 md5 生成
[chunkhash] chunk 內容的 hash,預設是 md5 生成
[contenthash] 檔案內容的 hash,預設是 md5 生成
[query] 檔案的 query,例如,檔名 ? 後面的字串
[emoji] 一個隨機的指代檔案內容的 emoji

我們可以使用特定的語法,對 hashchunkhashcontenthash 進行切片:[chunkhash:4],像 8c4cbfdb91ff93f3f3c5 這樣的雜湊會最後會變為 8c4c

hash

與整個專案的構建有關,只要專案內檔案有修改,整個專案構建的hash值就會改變

我們使用多入口打包來體驗一下:

// webpack.config.js
module.exports = {
  entry: { 
        main: './src/main.js',
        index: './src/index.js'
    },
    output: {
        filename: '[name].[hash:6].js',
        path: __dirname + '/dist',
        clean: true
    },
  // ...
}

此時我們使用了預留位置來設定檔案指紋[name].[hash:6].js代表的是檔名+6位hash

此時我們執行npm run build,看打包出來的內容如下:

此時兩個js檔案的hash都是207495

我們修改一下index.js的內容,再打包一次

我們會發現此時兩個js檔案的hash都變成了9f0e2d

chunkhash

chunkhash 是和 webpack 打包的模組相關,每一個 entry 作為一個模組,會產生不同的 Chunkhash 值,檔案改變時只會影響當前chunk組的hash值

我們再來看看chunkhash

// webpack.config.js
module.exports = {
  entry: { 
        main: './src/main.js',
        index: './src/index.js'
    },
    output: {
        filename: '[name].[chunkhash:6].js',
        path: __dirname + '/dist',
        clean: true
    },
  // ...
}

還是延用上面的例子,這次我們只修改main.js檔案內容

修改前兩個檔案的hash值如下:

修改後:

此時只有main.js的打包產物的hash發生了變化

contenthash

contenthash 是和根據檔案內容相關,單個檔案發生變化,只會引起此檔案的hash值

這裡我們使用miniCssExtractPlugin將CSS內容提取成檔案,併為它設定contenthash

// webpack.config.js
const miniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  entry: { 
        main: './src/main.js',
        index: './src/index.js'
    },
    output: {
        filename: '[name].[contenthash:6].js',
        path: __dirname + '/dist',
        clean: true
    },
  mudole: {
    rules: [
      {
        test: /\.css$/,
        use: [miniCssExtractPlugin.loader, 'css-loader']
      },
      // ...
    ]
  },
  plugins: [
    // ...
    new miniCssExtractPlugin({
            filename: 'css/[name].[contenthash:6].css'
        }),
  ]
  // ...
}

然後打包看一下此時的hash:

我們修改index.css內容再打包一次

此時只有index.css的打包產物hash值發生了變化。

根據不同的檔案型別一般選擇不同的檔案指紋策略,通常情況下:

  • JS檔案採用[chunkhash]檔案指紋策略
  • CSS檔案採用[contenthash]檔案指紋策略
  • 圖片資源採用[hash]檔案指紋策略

程式碼壓縮

壓縮JS

目前最成熟的JavaScript程式碼壓縮工具是UglifyJS,它能夠分析JavaScript語法樹,理解程式碼含義,從而能做到諸如去掉無效程式碼、去掉紀錄檔輸出程式碼、縮短變數名等優化。但很遺憾的是UglifyJS不再維護,並且它不支援 ES6 + 。

現在推薦使用的是Terser,它在 UglifyJS 基礎上增加了 ES6 語法支援,並重構程式碼解析、壓縮演演算法,使得執行效率與壓縮率都有較大提升,並且Webpack5.0 後預設使用 Terser 作為 JavaScript 程式碼壓縮器

簡單實用:

// webpack.config.js
module.exports = {
  //...
  optimization: {
    minimize: true
  }
}

需要注意的是在生產模式中構建時,Terser壓縮是預設開啟的

當然它也允許你通過提供一個或多個客製化過的TerserPlugin範例,覆蓋預設的壓縮工具,實現更精細的壓縮功能

// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
  //...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
        },
      })
    ]
  }
}

在Webpack4中 預設使用 uglifyjs-webpack-plugin壓縮程式碼,也可以通過 minimizer 陣列替換為 Terser 外掛

壓縮CSS

CSS程式碼同樣也可以使用webpack來進行壓縮,比較常見的CSS壓縮工具有:cssnanocss-minimizer-webpack-plugin

對於 webpack5 或更高版本,官方推薦使用 CssMinimizerWebpackPlugin,該外掛是使用 cssnano 優化和壓縮 CSS,支援快取和並行模式下執行。

安裝:

npm i css-minimizer-webpack-plugin

設定:

// webpack.config.js

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 用壓縮css
const MiniCssExtractPlugin = require("mini-css-extract-plugin");  // 用來提取css成單獨的檔案

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.css$/,
        // 注意,MiniCssExtractPlugin.loader 與 style-loader不能同時使用
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  optimization: {
    minimize: true,
    minimizer: [
      // Webpack5 之後,約定使用 '...' 字面量保留預設 minimizer 設定
      "...",
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

⚠️這裡需要注意的是需要使用 mini-css-extract-plugin 將 CSS 程式碼抽取為單獨的 CSS 產物檔案,這樣才能命中 css-minimizer-webpack-plugin 預設的 test 邏輯。

壓縮HTML

我們之前使用的html-webpack-plugin,它除了可以生成html模版,也可以用來對html進行壓縮。

htmlWebpackPlugin常見引數

  • template:模板的路徑,預設會去尋找 src/index.ejs 是否存在。

  • filename:輸出檔案的名稱,預設為 index.html

  • inject:是否將資源注入到模版中,預設為 true

  • minify:壓縮引數。在生產模式下(production),預設為 true;否則,預設為false

// webpack.config.js

module.exports = {
  // ...
  plugins: [
    // ...
    new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html',
            minify: true
        }),
  ]
}

生成的 HTML 將使用 html-minifier-terser 和以下選項進行壓縮,所以它實際上的壓縮功能其實是html-minifier-terser來實現的,更多設定可以檢視這個工具檔案

{
  collapseWhitespace: true,
  keepClosingSlash: true,
  removeComments: true,
  removeRedundantAttributes: true,
  removeScriptTypeAttributes: true,
  removeStyleLinkTypeAttributes: true,
  useShortDoctype: true
}

禁止生成LICENSE檔案

經過上面這些設定後,我發現了一個奇怪的問題,那就是每個bundle產物都多了一個同名的LICENSE.txt檔案,開啟一看裡面都是一些註釋內容。

為什麼會生成這些檔案,帶著疑惑我去翻了下官方檔案,Webpack5 預設壓縮程式碼工具為terser-webpack-plugin,那就先從它入手吧。

在它的設定中找到了extractComments引數,預設值為true,表示將註釋剝離到單獨的檔案中。

如果我們不想要,直接關掉該設定就行了

module.exports = {
  // ...
  optimization: {
        minimize: true,
        minimizer: [
            new cssMinimizerPlugin(),
            new terserPlugin({
                extractComments: false,  // 關閉註釋剝離功能
            }),
            '...'
        ]
    },
}

CSS增強(autoprefixer)

前端最頭疼的問題莫過於處理相容性,因為前端的執行環境並不固定,可以在各種瀏覽器以及各種webview中執行,並且每個瀏覽器廠商對CSS的寫法也各不相同,這就勢必會導致出現一些問題。

比如為了相容各種瀏覽器核心,圓角屬性應該這樣寫:

.container {
  -moz-border-radius: 16px;
  -webkit-border-radius: 16px;
  -o-border-radius: 16px;
  border-radius: 16px;
}

試想一下如果在開發中需要你這樣寫,那是不是太不合理了?

我們一般都會通過webpack設定外掛來幫我們解決這個問題,處理CSS我們首先會想到postcss,沒錯webpack也有使用postcss處理CSS的loader --- postcss-loader,然後我們還需要使用postcss的外掛autoprefixer來幫我們自動新增瀏覽器字首。

安裝:

npm i postcss postcss-loader autoprefixer

修改設定:

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      //...
      {
        test: /\.css$/,
        use: [miniCssExtractPlugin.loader, 
              'css-loader', 
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: ['autoprefixer']
                  }

                }
              }]
      },
    ]
  }
  //...
}

⚠️這裡需要注意的是,如果你想自定義轉換的規則,最好是將 autoprefixer 的 browsers選項替換為 browserslist 設定。在 package.json 或。Browserslistrc 檔案。使用 browsers選項可能導致錯誤,並且browserslist 設定可以用於 babel、 autoprefixer、 postcss-norize 等工具。

比如package.json中設定browserslist:

// package.json
{
  //...
  "browserslist": [
    "last 10 Chrome versions",
    "last 5 Firefox versions",
    "Safari >= 6", 
    "ie> 8"
  ] 
}

此時我們打包的CSS的產物就會自動新增瀏覽器字首

靜態資源拷貝

假如我們需要在html中參照一些不需要打包處理的資源,比如下面這種情況

index.html中引入了一些紀錄檔的工具函數,這時候我們直接跑起來會發現這個檔案直接404了,這是怎麼回事?

首先我們寫的路徑肯定是沒問題的,問題在於我們打包後這個utils檔案肯定是不在這個位置了,所以會報404

所以這裡我們需要使用copy-webpack-plugin將檔案拷貝至dist目錄下

// webpack.config.js
const copyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
  // ...
  plugins: [
    new copyWebpackPlugin({
      patterns: [
        {from: 'module', to: __dirname + '/dist/module/'}
      ]
    }),
  ]
}

此時再打包,我們會發現dist目錄下已經有了module/utils.js,並且頁面也不會再報404了

sourcemap

SourceMap 就是一個資訊檔案,裡面儲存著程式碼的位置資訊。這種檔案主要用於開發偵錯,現在程式碼都會經過壓縮混淆,這樣報錯提示會很難定位程式碼。通過 SourceMap 能快速定位到原始碼,並進行偵錯。

比如我們沒有開啟sourcemap,然後開發過程中報錯了,它的報錯資訊是這樣的:

定位過去是打包後的內容,這樣的話對我們排查報錯非常不方便。

當我們開啟sourcemap,再來看看這個同樣的報錯是怎樣的:

// webpack.config.js
module.exports = {
  // ...
  devtool: 'eval-cheap-module-source-map',
}

此時的報錯指向就非常清晰了~

關鍵字

devtool的值有20多種,並且都是由以下七種關鍵字的一個或多個組成

  • eval 關鍵字

devtool 值包含 eval 時,生成的模組程式碼會被包裹進一段 eval 函數中,且模組的 Sourcemap 資訊通過 //# sourceURL 直接掛載在模組程式碼內

  • source-map 關鍵字

devtool 包含 source-map 時,Webpack 才會生成 Sourcemap 內容

  • cheap 關鍵字

devtool 包含 cheap 時,生成的 Sourcemap 內容會拋棄維度的資訊,這就意味著瀏覽器只能對映到程式碼行維度

  • module 關鍵字

module 關鍵字只在 cheap 場景下生效,例如 cheap-module-source-mapeval-cheap-module-source-map。當 devtool 包含 cheap 時,Webpack 根據 module 關鍵字判斷按 loader 聯調處理結果作為 source,還是按處理之前的程式碼作為 source

  • nosources 關鍵字

devtool 包含 nosources 時,生成的 Sourcemap 內容中不包含原始碼內容 —— 即 sourcesContent 欄位

  • inline 關鍵字

devtool 包含 inline 時,Webpack 會將 Sourcemap 內容編碼為 Base64 DataURL,直接追加到產物檔案中

  • hidden 關鍵字

通常,產物中必須攜帶 //# sourceMappingURL= 指令,瀏覽器才能正確找到 Sourcemap 檔案,當 devtool 包含 hidden 時,編譯產物中不包含 //# sourceMappingURL= 指令

devtool的值以及各自的功能可以在webpack檔案上檢視

如何選擇

  • 對於開發環境,適合使用:
    • eval:速度極快,但只能看到原始檔案結構,看不到打包前的程式碼內容;
    • cheap-eval-source-map:速度比較快,可以看到打包前的程式碼內容,但看不到 loader 處理之前的原始碼;
    • cheap-module-eval-source-map:速度比較快,可以看到 loader 處理之前的原始碼,不過定位不到列級別;
    • eval-source-map:初次編譯較慢,但定位精度最高;
  • 對於生產環境,則適合使用:
    • source-map:資訊最完整,但安全性最低,外部使用者可輕易獲取到壓縮、混淆之前的原始碼,慎重使用;
    • hidden-source-map:資訊較完整,安全性較低,外部使用者獲取到 .map 檔案地址時依然可以拿到原始碼,慎重使用;
    • nosources-source-map:原始碼資訊缺失,但安全性較高,需要配合 Sentry 等工具實現完整的 Sourcemap 對映。

解決跨域

在開發過程中,我們勢必會遇到跨域問題,對於本地開發我們一般可以通過設定代理來解決

我們先來簡單寫一個介面:

const express = require('express')
const app = express()
app.get('/api/getInfo', (req, res) => {
    res.json({
        code: 200,
        data: {
            name: 'nanjiu',
            age: 18
        }
    })
})

app.listen(3000, () => {
    console.log('服務已啟動~')
})

然後把服務跑起來,再到vue專案中去呼叫

const getInfo = async () => {
    try {
        const res = await axios.get('http://localhost:3000/api/getInfo')
        console.log(res)
    } catch(err) {
        console.log(err)
    }
}

這時候你會發現介面呼叫跨域了

設定代理

接著我們再來通過webpack設定代理解決跨域問題,由於我們本地使用了webpack-dev-server,所以我們可以直接通過它來設定

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    hot: true,
    open: true,
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
}

這個時候我們的介面請求就正常了

由於篇幅問題,這篇文章就介紹到這裡了,後面會接著更新webpack更多高階用法。

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新文章~