webpack4從零學習常用配置

zybing發表於2021-09-09

webpack 的核心價值就是前端原始碼的打包,即將前端原始碼中每一個檔案(無論任何型別)都當做一個 pack ,然後分析依賴,將其最終打包出線上執行的程式碼。webpack 的四個核心部分

  • entry 規定入口檔案,一個或者多個

  • output 規定輸出檔案的位置

  • loader 各個型別的轉換工具

  • plugin 打包過程中各種自定義功能的外掛

webpack 如今已經進入 v4.x 版本,v5.x 估計也會很快釋出。不過看 v5 的變化相比於 v4 ,常用的配置沒有變,這是一個好訊息,說明基本穩定。


需要了解的 webpack

前端工程化是近幾年前端發展迅速的主要推手之一,webpack 無疑是前端工程化的核心工具。目前前端工程化工具還沒有到一鍵生成,或者重度繼承到某個 IDE 中(雖然有些 cli 工具可以直接建立),還是需要開發人員手動做一些配置。

因此,作為前端開發人員,熟練應用 webpack 的常用配置、常用最佳化方案是必備的技能 —— 這也正是本文的內容。另外,webpack 的實現原理算是一個加分項,不要求所有開發人員掌握,本文也沒有涉及。


基礎配置

初始化環境

npm init -y 初始化 npm 環境,然後安裝 webpack npm i webpack webpack-cli -D

新建 src 目錄並在其中新建 index.js ,隨便寫點 console.log('index js') 。然後根目錄建立 webpack.config.js ,內容如下

const path = require('path')module.exports = {
    // mode 可選 development 或 production ,預設為後者
    // production 會預設壓縮程式碼並進行其他最佳化(如 tree shaking)
    mode: 'development',
    entry: path.join(__dirname, 'src', 'index'),
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    }}

然後增加 package.json 的 scripts

  "scripts": {
    "build": "webpack"
  },

然後執行 npm run build 即可打包檔案到 dist 目錄。

區分 dev 和 build

使用 webpack 需要兩個最基本的功能:第一,開發的程式碼執行一下看看是否有效;第二,開發完畢了將程式碼打包出來。這兩個操作的需求、配置都是完全不一樣的。例如,執行程式碼時不需要壓縮以便 debug ,而打包程式碼時就需要壓縮以減少檔案體積。因此,這裡我們還是先把兩者分開,方便接下來各個步驟的講解。

首先,安裝 npm i webpack-merge -D ,然後根目錄新建 build 目錄,其中新建如下三個檔案。

// webpack.common.js 公共的配置const path = require('path')const srcPath = path.join(__dirname, '..', 'src')const distPath = path.join(__dirname, '..', 'dist')module.exports = {
    entry: path.join(srcPath, 'index')}
// webpack.dev.js 執行程式碼的配置(該檔案暫時用不到,先建立了,下文會用到)const path = require('path')const webpackCommonConf = require('./webpack.common.js')const { smart } = require('webpack-merge')const srcPath = path.join(__dirname, '..', 'src')const distPath = path.join(__dirname, '..', 'dist')module.exports = smart(webpackCommonConf, {
    mode: 'development'})
// webpack.prod.js 打包程式碼的配置const path = require('path')const webpackCommonConf = require('./webpack.common.js')const { smart } = require('webpack-merge')const srcPath = path.join(__dirname, '..', 'src')const distPath = path.join(__dirname, '..', 'dist')module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        filename: 'bundle.[contentHash:8].js',  // 打包程式碼時,加上 hash 戳
        path: distPath,
        // publicPath: ''  // 修改所有靜態檔案 url 的字首(如 cdn 域名),這裡暫時用不到
    }})

修改 package.json 中的 scripts

  "scripts": {
    "build": "webpack --config build/webpack.prod.js"
  },

重新執行 npm run build 即可看到打包出來的程式碼。最後,別忘了將根目錄下的 webpack.config.js 刪除。

這將引發一個新的問題:js 程式碼中將如何判斷是什麼環境呢?需要藉助 webpack.DefinedPlugin 外掛來定義全域性變數。可以在 webpack.dev.js 和 webpack.prod.js 中做如下配置:

// 引入 webpackconst webpack = require('webpack')// 增加 webpack 配置
    plugins: [
        new webpack.DefinePlugin({
            // 注意:此處 webpack.dev.js 中寫 'development' ,webpack.prod.js 中寫 'production'
            ENV: JSON.stringify('development')
        })

最後,修改 src/index.js 只需加入一行 console.log(ENV) ,然後重啟 npm run dev 即可看到效果。

JS 模組化

webpack 預設支援 js 各種模組化,如常見的 commonJS 和 ES6 Module 。但是推薦使用 ES6 Module ,因為 production 模式下,ES6 Module 會預設觸發 tree shaking ,而 commonJS 則沒有這個福利。究其原因,ES6 Module 是靜態引用,在編譯時即可確定依賴關係,而 commonJS 是動態引用。

不過使用 ES6 Module 時,ES6 的解構賦值語法這裡有一個坑,例如 index.js 中有一行 import {fn, name} from './a.js' ,此時 a.js 中有以下幾種寫法,大家要注意!

// 正確寫法一export function fn() {
    console.log('fn')}export const name = 'b'
// 正確寫法二function fn() {
    console.log('fn')}const name = 'b'export {
    fn,
    name}
// 錯誤寫法function fn() {
    console.log('fn')}export default {
    fn,
    name: 'b'}

該現象的具體原因可參考  。下文馬上要講解啟動本地服務,讀者可以馬上寫一個 demo 自己驗證一下這個現象。


啟動本地服務

上文建立的 webpack.dev.js 一直沒使用,下面就要用起來。

使用 html

啟動本地服務,肯定需要一個 html 頁面作為載體,新建一個 src/index.html 並初始化內容

<!DOCTYPE html><html><head><title>Document</title></head><body>
    <p>this is index html</p></body></html>

要使用這個 html 檔案,還需要安裝 npm i html-webpack-plugin -D ,然後配置 build/webpack.common.js ,因為無論 dev 還是 prod 都需要打包 html 檔案。

    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html'
        })
    ]

重新執行 npm run build 會發現打包出來了 dist/index.html ,且內部已經自動插入了打包的 js 檔案。

webpack-dev-server

有了 html 和 js 檔案,就可以啟動服務了。首先安裝 npm i webpack-dev-server -D ,然後開啟 build/webpack.dev.js配置。只有執行程式碼才需要本地 server ,打包程式碼時不需要。

devServer: {
    port: 3000,
    progress: true,  // 顯示打包的進度條
    contentBase: distPath,  // 根目錄
    open: true,  // 自動開啟瀏覽器
    compress: true  // 啟動 gzip 壓縮}

開啟 package.json 修改 scripts ,增加 "dev": "webpack-dev-server --config build/webpack.dev.js", 。然後執行 npm run dev ,開啟瀏覽器訪問 localhost:3000 即可看到效果。

解決跨域

實際開發中,server 端提供的埠地址和前端可能不同,導致 ajax 收到跨域限制。使用 webpack-dev-server 可配置代理,解決跨域問題。如有需要,在 build/webpack.dev.js 中增加如下配置。

    devServer: {
        // 設定代理
        proxy: {
            // 將本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': '',

            // 將本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: '',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }

處理 ES6

使用 babel

由於現在瀏覽器還不能保證完全支援 ES6 ,將 ES6 編譯為 ES5 ,需要藉助 babel 這個神器。安裝 babel npm i babel-loader @babel/core @babel/preset-env -D ,然後修改 build/webpack.common.js 配置

    module: {
        rules: [
            {
                test: /.js$/,
                loader: ['babel-loader'],
                include: srcPath,
                exclude: /node_modules/
            },
        ]
    },

還要根目錄下新建一個 .babelrc json 檔案,內容下

{
    "presets": ["@babel/preset-env"],
    "plugins": []}

在 src/index.js 中加入一行 ES6 程式碼,如箭頭函式 const fn = () => { console.log('this is fn') } 。然後重新執行 npm run dev,可以看到瀏覽器中載入的 js 中,這個函式已經被編譯為 function 形式。

使用高階特性

babel 可以解析 ES6 大部分語法特性,但是無法解析 class 、靜態屬性、塊級作用域,還有很多大於 ES6 版本的語法特性,如裝飾器。因此,想要把日常開發中的 ES6 程式碼全部轉換為 ES5 ,還需要藉助很多 babel 外掛。

安裝 npm i @babel/plugin-proposal-class-properties @babel/plugin-transform-block-scoping @babel/plugin-transform-classes -D ,然後配置 .babelrc

{
    "presets": ["@babel/preset-env"],
    "plugins": [
        "@babel/plugin-proposal-class-properties",
        "@babel/plugin-transform-block-scoping",
        "@babel/plugin-transform-classes"
    ]}

在 src/index.js 中新增一段 class 程式碼,然後重新執行 npm run build ,打包出來的程式碼會將 class 轉換為 function 形式。


source map

source map 用於反解析壓縮程式碼中錯誤的行列資訊,dev 時程式碼沒有壓縮,用不到 source map ,因此要配置 build/webpack.prod.js

// webpack 中 source map 的可選項,是情況選擇一種:// devtool: 'source-map'  // 1. 生成獨立的 source map 檔案// devtool: 'eval-source-map'  // 2. 同 1 ,但不會產生獨立的檔案,整合到打包出來的 js 檔案中// devtool: 'cheap-module-source-map'  // 3. 生成單獨的 source map 檔案,但沒有列資訊(因此檔案體積較小)devtool: 'cheap-module-eval-source-map'  // 4. 同 3 ,但不會產生獨立的檔案,整合到打包出來的 js 檔案中

生產環境下推薦使用 1 或者 3 ,即生成獨立的 map 檔案。修改之後,重新執行 npm run build ,會看到打包出來了 map 檔案。


處理樣式

在 webpack 看來,不僅僅是 js ,其他的檔案也是一個一個的模組,透過相應的 loader 進行解析並最終產出。

處理 css

安裝必要外掛 npm i style-loader css-loader -D ,然後配置 build/webpack.common.js

    module: {
        rules: [
            { /* js loader */ },
            {
                test: /.css$/,
                loader: ['style-loader', 'css-loader']  // loader 的執行順序是:從後往前
            }
        ]
    },

新建一個 css 檔案,然後引入到 src/index.js 中 import './css/index.css' ,重新執行 npm run dev 即可看到效果。

處理 less

less sass 都是常用 css 預處理語言,以 less 為例講解。安裝必要外掛 npm i less less-loader -D ,然後配置 build/webpack.common.js

            {
                test: /.less$/,
                loader: ['style-loader', 'css-loader', 'less-loader']  // 增加 'less-loader' ,注意順序
            }

新建一個 less 檔案,然後引入到 src/index.js 中 import './css/index.less' ,重新執行 npm run dev 即可看到效果。

自動新增字首

一些 css3 的語法,例如 transform: rotate(45deg); 為了瀏覽器相容性需要加一些字首,如 webkit- ,可以透過 webpack 來自動新增。安裝 npm i postcss-loader autoprefixer -D ,然後配置

            {
                test: /.css$/,
                loader: ['style-loader', 'css-loader', 'postcss-loader']  // 增加 'postcss-loader' , 注意順序
            }

還要新建一個 postcss.config.js 檔案,內容是

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

重新執行 npm run dev 即可看到效果,自動增加了必要的字首。

抽離 css 檔案

預設情況下,webpack 會將 css 程式碼全部寫入到 html 的 <style> 標籤中,但是打包程式碼時需要抽離到單獨的 css 檔案中。安裝 npm i mini-css-extract-plugin -D 然後配置 build/webpack.prod.js(打包程式碼時才需要,執行時不需要)

// 引入外掛const MiniCssExtractPlugin = require('mini-css-extract-plugin')// 增加 webpack 配置
    module: {
        rules: [
            {
                test: /.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,這裡不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        })
    ]

如需要壓縮 css ,需要安裝 npm i terser-webpack-plugin optimize-css-assets-webpack-plugin -D ,然後增加配置

// 引入外掛const TerserJSPlugin = require('terser-webpack-plugin')const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')// 增加 webpack 配置
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
    },

執行 npm run build 即可看到打包出來的 css 是獨立的檔案,並且是被壓縮過的。


處理圖片

要在 js 中 import 圖片,或者在 css 中設定背景圖片。安裝 npm i file-loader -D 然後配置 build/webpack.common.js

            {
                test: /.(png|jpg|gif)$/,
                use: 'file-loader'
            }

如果想要處理 html 程式碼中 <img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="..."/> 的形式,則安裝 npm i html-withimg-loader -D 然後配置 build/webpack.common.js

            {
                test: /.html$/,
                use: 'html-withimg-loader'
            }

打包之後,dist 目錄下會生成一個類似 917bb63ba2e14fc4aa4170a8a702d9f8.jpg 的檔案,並被引入到打包出來的結果中。

如果想要將小圖片用 base64 格式產出,則安裝 npm i url-loader -D ,然後配置 build/webpack.common.js

            {
                test: /.(png|jpg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小於 5kb 的圖片用 base64 格式產出
                        // 否則,依然延用 file-loader 的形式,產出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目錄下
                        outputPath: '/img/',

                        // 設定圖片的 cdn 地址(也可以統一在外面的 output 中設定,那將作用於所有靜態資源)
                        // publicPath: ''
                    }
                }
            },

多頁應用

src 下有 index.js index.html 和 other.js other.html ,要打包輸出兩個頁面,且分別引用各自的 js 檔案。

第一,配置輸入輸出

    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    output: {
        filename: '[name].[contentHash:8].js',  // [name] 表示 chunk 的名稱,即上面的 index 和 other
        path: distPath    },

第二,配置 html 外掛

    plugins: [
        // 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示該頁面要引用哪些 chunk (即上面的 index 和 other),預設全部引用
            chunks: ['index']  // 只引用 index.js
        }),
        // 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other']  // 只引用 other.js
        }),

抽離公共程式碼

公共模組

多個頁面或者入口,如果引用了同一段程式碼,如上文的多頁面例子中,index.js 和 other.js 都引用了 import './common.js' ,則 common.js 應該被作為公共模組打包。webpack v4 開始棄用了 commonChunkPlugin 改用 splitChunks ,可修改 build/webpack.prod.js 中的配置

    optimization: {
        // 分割程式碼塊
        splitChunks: {
            // 快取分組
            cacheGroups: {
                // 公共的模組
                common: {
                    chunks: 'initial',
                    minSize: 0,  // 公共模組的大小限制
                    minChunks: 2  // 公共模組最少複用過幾次
                }
            }
        }
    },

重新執行 npm run build ,即可看到有 common 模組被單獨打包出來,就是 common.js 的內容。

第三方模組

同理,如果我們的程式碼中引用了 jquery lodash 等,也希望將第三方模組單獨打包,和自己開發的業務程式碼分開。這樣每次重新上線時,第三方模組的程式碼就可以藉助瀏覽器快取,提高使用者訪問網頁的效率。修改配置檔案,增加下面的 vendor: {...} 配置。

    optimization: {
        // 分割程式碼塊
        splitChunks: {
            // 快取分組
            cacheGroups: {
                // 第三方模組
                vendor: {
                    priority: 1, // 許可權更高,優先抽離,重要!!!
                    test: /node_modules/,
                    chunks: 'initial',
                    minSize: 0,  // 大小限制
                    minChunks: 1  // 最少複用過幾次
                },

                // 公共的模組
                common: {
                    chunks: 'initial',
                    minSize: 0,  // 公共模組的大小限制
                    minChunks: 2  // 公共模組最少複用過幾次
                }
            }
        }
    },

重啟 npm run build ,即可看到 vendor 模組被打包出來,裡面是 jquery 或者 lodash 等第三方模組的內容。


懶載入

webpack 支援使用 import(...) 語法進行資源懶載入。安裝 npm i @babel/plugin-syntax-dynamic-import -D 然後將外掛配置到 .babelrc 中。

新建 src/dynamic-data.js 用於測試,內容是 export default { message: 'this is dynamic' } 。然後在 src/index.js 中加入

setTimeout(() => {
    import('./dynamic-data.js').then(res => {
        console.log(res.default.message)  // 注意這裡的 default
    })}, 1500)

重新執行 npm run dev 重新整理頁面,可以看到 1.5s 之後列印出 this is dynamic 。而且,dynamic-data.js 也是 1.5s 之後被載入進瀏覽器的 —— 懶載入,雖然檔名變了。

重新執行 npm run build 也可以看到 dynamic-data.js 的內容被打包一個單獨的檔案中。


常見效能最佳化

tree shaking

使用 import 引入,在 production 環境下,webpack 會自動觸發 tree shaking ,去掉無用程式碼。但是使用 require 引入時,則不會觸發 tree shaking。這是因為 require 是動態引入,無法在編譯時判斷哪些功能被使用。而 import 是靜態引入,編譯時即可判斷依賴關係。

noParse

不去解析某些 lib 其內部的依賴,即確定這些 lib 沒有其他依賴,提高解析速度。可配置到 build/wepback.common.js 中

    module: {
        noParse: /jquery|lodash/,  // 不解析 jquery 和 lodash 的內部依賴

ignorePlugin

以常用的 moment 為例。安裝 npm i moment -d 並且 import moment from 'moment' 之後,monent 預設將所有語言的 js 都載入進來,使得打包檔案過大。可以透過 ignorePlugin 外掛忽略 locale 下的語言檔案,不打包進來。

    plugins: [
        new webpack.IgnorePlugin(/./locale/, /moment/),  // 忽略 moment 下的 /locale 目錄

這樣,使用時可以手動引入中文包,並設定語言

import moment from 'moment'import 'moment/locale/zh-cn' // 手動引入中文語言包moment.locale('zh-cn')const r = moment().endOf('day').fromNow()console.log(r)

happyPack

多程式打包,參考  。注意,小專案使用反而會變慢。只有專案較大,打包出現明顯瓶頸時,才考慮使用 happypack 。


常用外掛和配置

ProvidePlugin

如要給所有的 js 模組直接使用 $ ,不用每次都 import $ from 'jquery' ,可做如下配置

    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery'
        }),

externals

如果 jquery 已經在 html 中透過 cdn 引用了,無需再打包,可做如下配置

    externals: {
        jquery: 'jQuery'
    },

alias

設定 alias 別名在實際開發中比較常用,尤其是專案較大,目錄較多時。可做如下配置

    resolve: {
        alias: {
            Utilities: path.join(srcPath, 'utilities')
        }
    },

在該配置之前,可能需要 import Utility from '../../utilities/utility' 使用。配置之後就可以 import Utility from 'Utilities/utility' 使用,一來書寫簡潔,二來不用再考慮相對目錄的層級關係。

extensions

如果引用檔案時沒有寫字尾名,可以透過 extensions 來匹配。

    resolve: {
        extensions: [".js", ".json"]
    },

clean-webpack-plugin

由於使用了 contentHash ,每次 build 時候都可能打包出不同的檔案,因此要及時清理 dist 目錄。安裝 npm i clean-webpack-plugin -D ,然後在 build/webpack.prod.js 中配置

// 引入外掛const CleanWebpackPlugin = require('clean-webpack-plugin')// 增加配置
    plugins: [
        new CleanWebpackPlugin(),  // 預設清空 output.path 目錄

copy-webpack-plugin

build 時,將 src 目錄下某個檔案或者資料夾,無條件的複製到 dist 目錄下,例如 src/doc 目錄複製過去。安裝 npm i copy-webpack-plugin -D,然後在 build/webpack.prod.js 中配置

// 引入外掛const CopyWebpackPlugin = require('copy-webpack-plugin')// 增加配置
    plugins: [
        new CopyWebpackPlugin([
            {
                from: path.join(srcPath, 'doc'),  // 將 src/doc 複製到 dist/doc
                to: path.join(distPath, 'doc')
            }
        ]),

bannerPlugin

程式碼的版權宣告,在 build/webpack.prod.js 中配置即可。

    plugins: [
        new webpack.BannerPlugin('by github.com/wangfupeng1988 r'),

總結

webpack 發展至今配置非常多,該影片中也沒有全部講解出來,只是一些實際開發中常用的。其他的配置可以去看官網文件。

大家可以關注一下課程:

【新課】



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2822951/,如需轉載,請註明出處,否則將追究法律責任。

相關文章