Stories

Detail Return Return

【webpack系列】從基礎配置到掌握進階用法

前言

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

文件指紋

文件指紋指的是打包輸出的文件名後綴,一般用來做版本管理、緩存等

w1.png

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,看打包出來的內容如下:

w2.png

此時兩個js文件的hash都是207495

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

w3.png

我們會發現此時兩個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值如下:

w4.png

修改後:

w5.png

此時只有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:

w6.png

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

w7.png

此時只有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文件,打開一看裏面都是一些註釋內容。

w-license.png

為什麼會生成這些文件,帶着疑惑我去翻了下官方文檔,Webpack5 默認壓縮代碼工具為terser-webpack-plugin,那就先從它入手吧。

在它的配置中找到了extractComments參數,默認值為true,表示將註釋剝離到單獨的文件中。

w-ext.png

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

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的產物就會自動添加瀏覽器前綴

w8.png

靜態資源拷貝

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

w9.png

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

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

w10.png

所以這裏我們需要使用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了

w11.png

sourcemap

SourceMap 就是一個信息文件,裏面儲存着代碼的位置信息。這種文件主要用於開發調試,現在代碼都會經過壓縮混淆,這樣報錯提示會很難定位代碼。通過 SourceMap 能快速定位到源代碼,並進行調試。

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

w12.gif

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

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

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

w13.gif

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

關鍵字

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

這時候你會發現接口調用跨域了

w14.png

配置代理

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

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

這個時候我們的接口請求就正常了

w15.png

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

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

user avatar
0 users favorite the story!

Post Comments

Some HTML is okay.