1. 程式人生 > 其它 >Webpack入門-學習總結

Webpack入門-學習總結

【轉載】:本文源地址http://www.woc12138.com/article/45

目錄

一、Webpack 簡介

1.1 webpack 是什麼

webpack 是一種前端資源構建工具,一個靜態模組打包器(module bundler)。

在webpack 看來, 前端的所有資原始檔(js/json/css/img/less/...)都會作為模組處理。
它將根據模組的依賴關係進行靜態分析,打包生成對應的靜態資源(bundle)。

1.2 webpack 五個核心概念

1.2.1 Entry

入口(Entry):指示 webpack 以哪個檔案為入口起點開始打包,分析構建內部依賴圖。

1.2.2 Output

輸出(Output):指示 webpack 打包後的資源 bundles 輸出到哪裡去,以及如何命名。

1.2.3 Loader

Loader:讓 webpack 能夠去處理那些非 JS 的檔案,比如樣式檔案、圖片檔案(webpack 自身只理解
JS)

1.2.4 Plugins

外掛(Plugins):可以用於執行範圍更廣的任務。外掛的範圍包括,從打包優化和壓縮,
一直到重新定義環境中的變數等。

1.2.5 Mode

模式(Mode):指示 webpack 使用相應模式的配置。

選項 描述 特點
development 會將 DefinePlugin 中 process.env.NODE_ENV 的值設定為 development。啟用 NamedChunksPlugin 和 NamedModulesPlugin。 能讓程式碼本地除錯執行的環境
production 會將 DefinePlugin 中 process.env.NODE_ENV 的值設定為 production。啟用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugin。 能讓程式碼優化上線執行的環境

二、Webpack 初體驗

2.1 初始化配置

  1. 初始化 package.json:npm init

  2. 下載安裝webpack:(webpack4以上的版本需要全域性/本地都安裝webpack-cli)

    全域性安裝:cnpm i webpack webpack-cli -g

    本地安裝:cnpm i webpack webpack-cli -D

2.2 編譯打包應用

建立 src 下的 js 等檔案後,不需要配置 webpack.config.js 檔案,在命令列就可以編譯打包。

指令:

  • 開發環境:webpack ./src/index.js -o ./build/built.js --mode=development

    webpack會以 ./src/index.js 為入口檔案開始打包,打包後輸出到 ./build/built.js 整體打包環境,是開發環境

  • 生產環境:webpack ./src/index.js -o ./build/built.js --mode=production

    webpack會以 ./src/index.js 為入口檔案開始打包,打包後輸出到 ./build/built.js 整體打包環境,是生產環境

結論:

  1. webpack 本身能處理 js/json 資源,不能處理 css/img 等其他資源
  2. 生產環境和開發環境將 ES6 模組化編譯成瀏覽器能識別的模組化,但是不能處理 ES6 的基本語法轉化為 ES5(需要藉助 loader)
  3. 生產環境比開發環境多一個壓縮 js 程式碼

三、Webpack 開發環境的基本配置

webpack.config.js 是 webpack 的配置檔案。

作用: 指示 webpack 幹哪些活(當你執行 webpack 指令時,會載入裡面的配置)

所有構建工具都是基於 nodejs 平臺執行的,模組化預設採用 commonjs。

開發環境配置主要是為了能讓程式碼執行。主要考慮以下幾個方面:

  • 打包樣式資源
  • 打包 html 資源
  • 打包圖片資源
  • 打包其他資源
  • devServer

下面是一個簡單的開發環境webpack.confg.js配置檔案

// resolve用來拼接絕對路徑的方法
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 引用plugin

module.exports = {
  // webpack配置
  entry: './src/js/index.js', // 入口起點
  output: {
    // 輸出
    // 輸出檔名
    filename: 'js/build.js',
    // __dirname是nodejs的變數,代表當前檔案的目錄絕對路徑
    path: resolve(__dirname, 'build'), // 輸出路徑,所有資源打包都會輸出到這個資料夾下
  },
  // loader配置
  module: {
    rules: [
      // 詳細的loader配置
      // 不同檔案必須配置不同loader處理
      {
        // 匹配哪些檔案
        test: /\.less$/,
        // 使用哪些loader進行處理
        use: [
          // use陣列中loader執行順序:從右到左,從下到上,依次執行(先執行css-loader)
          // style-loader:建立style標籤,將js中的樣式資源插入進去,新增到head中生效
          'style-loader',
          // css-loader:將css檔案變成commonjs模組載入到js中,裡面內容是樣式字串
          'css-loader',
          // less-loader:將less檔案編譯成css檔案,需要下載less-loader和less
          'less-loader'
        ],
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        // url-loader:處理圖片資源,問題:預設處理不了html中的img圖片
        test: /\.(jpg|png|gif)$/,
        // 需要下載 url-loader file-loader
        loader: 'url-loader',
        options: {
          // 圖片大小小於8kb,就會被base64處理,優點:減少請求數量(減輕伺服器壓力),缺點:圖片體積會更大(檔案請求速度更慢)
          // base64在客戶端本地解碼所以會減少伺服器壓力,如果圖片過大還採用base64編碼會導致cpu呼叫率上升,網頁載入時變卡
          limit: 8 * 1024,
          // 給圖片重新命名,[hash:10]:取圖片的hash的前10位,[ext]:取檔案原來副檔名
          name: '[hash:10].[ext]',
          // 問題:因為url-loader預設使用es6模組化解析,而html-loader引入圖片是conmonjs,解析時會出問題:[object Module]
          // 解決:關閉url-loader的es6模組化,使用commonjs解析
          esModule: false,
          outputPath: 'imgs',
        },
      },
      {
        test: /\.html$/,
        // 處理html檔案的img圖片(負責引入img,從而能被url-loader進行處理)
        loader: 'html-loader',
      },
      // 打包其他資源(除了html/js/css資源以外的資源)
      {
        // 排除html|js|css|less|jpg|png|gif檔案
        exclude: /\.(html|js|css|less|jpg|png|gif)/,
        // file-loader:處理其他檔案
        loader: 'file-loader',
        options: {
          name: '[hash:10].[ext]',
          outputPath: 'media',
        },
      },
    ],
  },
  // plugin的配置
  plugins: [
    // html-webpack-plugin:預設會建立一個空的html檔案,自動引入打包輸出的所有資源(JS/CSS)
    // 需要有結構的HTML檔案可以加一個template
    new HtmlWebpackPlugin({
      // 複製這個./src/index.html檔案,並自動引入打包輸出的所有資源(JS/CSS)
      template: './src/index.html',
    }),
  ],
  // 模式
  mode: 'development', // 開發模式
  // 開發伺服器 devServer:用來自動化,不用每次修改後都重新輸入webpack打包一遍(自動編譯,自動開啟瀏覽器,自動重新整理瀏覽器)
  // 特點:只會在記憶體中編譯打包,不會有任何輸出(不會像之前那樣在外面看到打包輸出的build包,而是在記憶體中,關閉後會自動刪除)
  // 啟動devServer指令為:npx webpack-dev-server
  devServer: {
    // 專案構建後路徑
    contentBase: resolve(__dirname, 'build'),
    // 啟動gzip壓縮
    compress: true,
    // 埠號
    port: 3000,
    // 自動開啟瀏覽器
    open: true,
  },
}

其中,大部分配置都在註釋中給出解釋。

  • 執行專案的兩個指令:
    webpack 會將打包結果輸出出去(build資料夾)
    npx webpack-dev-server 只會在記憶體中編譯打包,沒有輸出

  • loader 和 plugin 的不同:(plugin 一定要先引入才能使用)

    loader:1. 下載 2. 使用(配置 loader)

    plugins:1.下載 2. 引入 3. 使用

四、Webpack 生產環境的基本配置

而生產環境的配置需要考慮以下幾個方面:

  • 提取 css 成單獨檔案
  • css 相容性處理
  • 壓縮 css
  • js 語法檢查
  • js 相容性處理
  • js 壓縮
  • html 壓縮

下面是一個基本的生產環境下的webpack.config.js配置

const { resolve } = require('path')
const MiniCssExtractorPlugin = require('mini-css-extract-plugin')
const OptimiziCssAssetsWebpackPlugin = require('optimizi-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 定義node.js的環境變數,決定使用browserslist的哪個環境
process.env.NODE_ENV = 'production'

// 複用loader的寫法
const commonCssLoader = [
  // 這個loader取代style-loader。作用:提取js中的css成單獨檔案然後通過link載入
  MiniCssExtractPlugin.loader,
  // css-loader:將css檔案整合到js檔案中
  // 經過css-loader處理後,樣式檔案是在js檔案中的
  // 問題:1.js檔案體積會很大2.需要先載入js再動態建立style標籤,樣式渲染速度就慢,會出現閃屏現象
  // 解決:用MiniCssExtractPlugin.loader替代style-loader
  'css-loader',
  /*
    postcss-loader:css相容性處理:postcss --> 需要安裝:postcss-loader postcss-preset-env
    postcss需要通過package.json中browserslist裡面的配置載入指定的css相容性樣式
    在package.json中定義browserslist:
    "browserslist": {
      // 開發環境 --> 設定node環境變數:process.env.NODE_ENV = development
      "development": [ // 只需要可以執行即可
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ],
      // 生產環境。預設是生產環境
      "production": [ // 需要滿足絕大多數瀏覽器的相容
        ">0.2%",
        "not dead",
        "not op_mini all"
      ]
    },
  */
  {
    loader: 'postcss-loader',
    options: {
      ident: 'postcss', // 基本寫法
      plugins: () => [
        // postcss的外掛
        require('postcss-preset-env')(),
      ],
    },
  },
]

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build'),
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [...commonCssLoader],
      },
      {
        test: /\.less$/,
        use: [...commonCssLoader, 'less-loader'],
      },
      /*
        正常來講,一個檔案只能被一個loader處理
        當一個檔案要被多個loader處理,那麼一定要指定loader執行的先後順序
        先執行eslint再執行babel(用enforce)
      */
      {
        /*
          js的語法檢查: 需要下載 eslint-loader eslint
          注意:只檢查自己寫的原始碼,第三方的庫是不用檢查的
          airbnb(一個流行的js風格) --> 需要下載 eslint-config-airbnb-base eslint-plugin-import
          設定檢查規則:
            package.json中eslintConfig中設定
              "eslintConfig": {
                "extends": "airbnb-base", // 繼承airbnb的風格規範
                "env": {
                  "browser": true // 可以使用瀏覽器中的全域性變數(使用window不會報錯)
                }
              }
        */
        test: /\.js$/,
        exclude: /node_modules/, // 忽略node_modules
        enforce: 'pre', // 優先執行
        loader: 'eslint-loader',
        options: {
          // 自動修復
          fix: true,
        },
      },
      /*
        js相容性處理:需要下載 babel-loader @babel/core
          1. 基本js相容性處理 --> @babel/preset-env
            問題:只能轉換基本語法,如promise高階語法不能轉換
          2. 全部js相容性處理 --> @babel/polyfill
            問題:只要解決部分相容性問題,但是將所有相容性程式碼全部引入,體積太大了
          3. 需要做相容性處理的就做:按需載入  --> core-js
      */
      {
        // 第三種方式:按需載入
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          // 預設:指示babel做怎樣的相容性處理
          presets: [
            '@babel/preset-env', // 基本預設
            {
              useBuiltIns: 'usage', //按需載入
              corejs: { version: 3 }, // 指定core-js版本
              targets: { // 指定相容到什麼版本的瀏覽器
                chrome: '60',
                firefox: '50',
                ie: '9',
                safari: '10',
                edge: '17'
              },
            },
          ],
        },
      },
      {
        // 圖片處理
        test: /\.(jpg|png|gif)/,
        loader: 'url-loader',
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          outputPath: 'imgs',
          esModule: false, // 關閉url-loader預設使用的es6模組化解析
        },
      },
      // html中的圖片處理
      {
        test: /\.html$/,
        loader: 'html-loader',
      },
      // 處理其他檔案
      {
        exclude: /\.(js|css|less|html|jpg|png|gif)/,
        loader: 'file-loader',
        options: {
          outputPath: 'media',
        },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      // 對輸出的css檔案進行重新命名
      filename: 'css/built.css',
    }),
    // 壓縮css
    new OptimiziCssAssetsWebpackPlugin(),
    // HtmlWebpackPlugin:html檔案的打包和壓縮處理
    // 通過這個外掛會自動將單獨打包的樣式檔案通過link標籤引入
    new HtmlWebpackPlugin({
      template: './src/index.html',
      // 壓縮html程式碼
      minify: {
        // 移除空格
        collapseWhitespace: true,
        // 移除註釋
        removeComments: true,
      },
    }),
  ],
  // 生產環境下會自動壓縮js程式碼
  mode: 'production',
}

五、Webpack 優化配置

5.1 開發環境效能優化

5.1.1 HMR(模組熱替換)

HMR: hot module replacement 熱模組替換 / 模組熱替換

作用:一個模組發生變化,只會重新打包構建這一個模組(而不是打包所有模組) ,極大提升構建速度

程式碼:只需要在 devServer 中設定 hot 為 true,就會自動開啟HMR功能(只能在開發模式下使用)

devServer: {
  contentBase: resolve(__dirname, 'build'),
  compress: true,
  port: 3000,
  open: true,
  // 開啟HMR功能
  // 當修改了webpack配置,新配置要想生效,必須重啟webpack服務
  hot: true
}

每種檔案實現熱模組替換的情況:

  • 樣式檔案:可以使用HMR功能,因為開發環境下使用的 style-loader 內部預設實現了熱模組替換功能

  • js 檔案:預設不能使用HMR功能(修改一個 js 模組所有 js 模組都會重新整理)

    --> 實現 HMR 需要修改 js 程式碼(新增支援 HMR 功能的程式碼)

    // 繫結
    if (module.hot) {
      // 一旦 module.hot 為true,說明開啟了HMR功能。 --> 讓HMR功能程式碼生效
      module.hot.accept('./print.js', function() {
        // 方法會監聽 print.js 檔案的變化,一旦發生變化,只有這個模組會重新打包構建,其他模組不會。
        // 會執行後面的回撥函式
        print();
      });
    }
    

    注意:HMR 功能對 js 的處理,只能處理非入口 js 檔案的其他檔案。

  • html 檔案: 預設不能使用 HMR 功能(html 不用做 HMR 功能,因為只有一個 html 檔案,不需要再優化)

    使用 HMR 會導致問題:html 檔案不能熱更新了(不會自動打包構建)

    解決:修改 entry 入口,將 html 檔案引入(這樣 html 修改整體重新整理)

    entry: ['./src/js/index.js', './src/index.html']
    

5.1.2 source-map

source-map:一種提供原始碼到構建後代碼的對映的技術 (如果構建後代碼出錯了,通過對映可以追蹤原始碼錯誤)

引數:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

程式碼:

devtool: 'eval-source-map'

可選方案:[生成source-map的位置|給出的錯誤程式碼資訊]

  • source-map:外部,錯誤程式碼準確資訊 和 原始碼的錯誤位置
  • inline-source-map:內聯,只生成一個內聯 source-map,錯誤程式碼準確資訊 和 原始碼的錯誤位置
  • hidden-source-map:外部,錯誤程式碼錯誤原因,但是沒有錯誤位置(為了隱藏原始碼),不能追蹤原始碼錯誤,只能提示到構建後代碼的錯誤位置
  • eval-source-map:內聯,每一個檔案都生成對應的 source-map,都在 eval 中,錯誤程式碼準確資訊 和 原始碼的錯誤位
  • nosources-source-map:外部,錯誤程式碼準確資訊,但是沒有任何原始碼資訊(為了隱藏原始碼)
  • cheap-source-map:外部,錯誤程式碼準確資訊 和 原始碼的錯誤位置,只能把錯誤精確到整行,忽略列
  • cheap-module-source-map:外部,錯誤程式碼準確資訊 和 原始碼的錯誤位置,module 會加入 loader 的 source-map

內聯 和 外部的區別:1. 外部生成了檔案,內聯沒有 2. 內聯構建速度更快

開發/生產環境可做的選擇:

開發環境:需要考慮速度快,除錯更友好

  • 速度快( eval > inline > cheap >... )
    1. eval-cheap-souce-map
    2. eval-source-map
  • 除錯更友好
    1. souce-map
    2. cheap-module-souce-map
    3. cheap-souce-map

最終得出最好的兩種方案 --> eval-source-map(完整度高,內聯速度快) / eval-cheap-module-souce-map(錯誤提示忽略列但是包含其他資訊,內聯速度快)

生產環境:需要考慮原始碼要不要隱藏,除錯要不要更友好

  • 內聯會讓程式碼體積變大,所以在生產環境不用內聯
  • 隱藏原始碼
    1. nosources-source-map 全部隱藏
    2. hidden-source-map 只隱藏原始碼,會提示構建後代碼錯誤資訊

最終得出最好的兩種方案 --> source-map(最完整) / cheap-module-souce-map(錯誤提示一整行忽略列)

5.2 生產環境效能優化

5.2.1 優化打包構建速度

5.2.1.1 oneOf

oneOf:匹配到 loader 後就不再向後進行匹配,優化生產環境的打包構建速度

程式碼:

module: {
  rules: [
    {
      // js 語法檢查
      test: /\.js$/,
      exclude: /node_modules/,
      // 優先執行
      enforce: 'pre',
      loader: 'eslint-loader',
      options: {
        fix: true
      }
    },
    {
      // oneOf 優化生產環境的打包構建速度
      // 以下loader只會匹配一個(匹配到了後就不會再往下匹配了)
      // 注意:不能有兩個配置處理同一種類型檔案(所以把eslint-loader提取出去放外面)
      oneOf: [
        {
          test: /\.css$/,
          use: [...commonCssLoader]
        },
        {
          test: /\.less$/,
          use: [...commonCssLoader, 'less-loader']
        },
        {
          // js 相容性處理
          test: /\.js$/,
          exclude: /node_modules/,
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  useBuiltIns: 'usage',
                  corejs: {version: 3},
                  targets: {
                    chrome: '60',
                    firefox: '50'
                  }
                }
              ]
            ]
          }
        },
        {
          test: /\.(jpg|png|gif)/,
          loader: 'url-loader',
          options: {
            limit: 8 * 1024,
            name: '[hash:10].[ext]',
            outputPath: 'imgs',
            esModule: false
          }
        },
        {
          test: /\.html$/,
          loader: 'html-loader'
        },
        {
          exclude: /\.(js|css|less|html|jpg|png|gif)/,
          loader: 'file-loader',
          options: {
            outputPath: 'media'
          }
        }
      ]
    }
  ]
},

5.2.1.2 babel 快取

babel 快取:類似 HMR,將 babel 處理後的資源快取起來(哪裡的 js 改變就更新哪裡,其他 js 還是用之前快取的資源),讓第二次打包構建速度更快

程式碼:

{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  options: {
    presets: [
      [
        '@babel/preset-env',
        {
          useBuiltIns: 'usage',
          corejs: { version: 3 },
          targets: {
            chrome: '60',
            firefox: '50'
          }
        }
      ]
    ],
    // 開啟babel快取
    // 第二次構建時,會讀取之前的快取
    cacheDirectory: true
  }
},

檔案資源快取

檔名不變,就不會重新請求,而是再次用之前快取的資源

1.hash: 每次 wepack 打包時會生成一個唯一的 hash 值。

問題:重新打包,所有檔案的 hsah 值都改變,會導致所有快取失效。(可能只改動了一個檔案)

2.chunkhash:根據 chunk 生成的 hash 值。來源於同一個 chunk的 hash 值一樣

問題:js 和 css 來自同一個chunk,hash 值是一樣的(因為 css-loader 會將 css 檔案載入到 js 中,所以同屬於一個chunk)

3.contenthash: 根據檔案的內容生成 hash 值。不同檔案 hash 值一定不一樣(檔案內容修改,檔名裡的 hash 才會改變)

修改 css 檔案內容,打包後的 css 檔名 hash 值就改變,而 js 檔案沒有改變 hash 值就不變,這樣 css 和 js 快取就會分開判斷要不要重新請求資源 --> 讓程式碼上線執行快取更好使用

5.2.1.3 多程序打包

多程序打包:某個任務消耗時間較長會卡頓,多程序可以同一時間幹多件事,效率更高。

優點是提升打包速度,缺點是每個程序的開啟和交流都會有開銷(babel-loader消耗時間最久,所以使用thread-loader針對其進行優化)

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    /* 
      thread-loader會對其後面的loader(這裡是babel-loader)開啟多程序打包。 
      程序啟動大概為600ms,程序通訊也有開銷。(啟動的開銷比較昂貴,不要濫用)
      只有工作消耗時間比較長,才需要多程序打包
    */
    {
      loader: 'thread-loader',
      options: {
        workers: 2 // 程序2個
      }
    },
    {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              useBuiltIns: 'usage',
              corejs: { version: 3 },
              targets: {
                chrome: '60',
                firefox: '50'
              }
            }
          ]
        ],
        // 開啟babel快取
        // 第二次構建時,會讀取之前的快取
        cacheDirectory: true
      }
    }
  ]
},

5.2.1.4 externals

externals:讓某些庫不打包,通過 cdn 引入

webpack.config.js 中配置:

externals: {
  // 拒絕jQuery被打包進來(通過cdn引入,速度會快一些)
  // 忽略的庫名 -- npm包名
  jquery: 'jQuery'
}

需要在 index.html 中通過 cdn 引入:

<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>

5.2.1.5 dll

dll:讓某些庫單獨打包,後直接引入到 build 中。可以在 code split 分割出 node_modules 後再用 dll 更細的分割,優化程式碼執行的效能。

webpack.dll.js 配置:(將 jquery 單獨打包)

/*
  node_modules的庫會打包到一起,但是很多庫的時候打包輸出的js檔案就太大了
  使用dll技術,對某些庫(第三方庫:jquery、react、vue...)進行單獨打包
  當執行webpack時,預設查詢webpack.config.js配置檔案
  需求:需要執行webpack.dll.js檔案
    --> webpack --config webpack.dll.js(執行這個指令表示以這個配置檔案打包)
*/
const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // 最終打包生成的[name] --> jquery
    // ['jquery] --> 要打包的庫是jquery
    jquery: ['jquery']
  },
  output: {
    // 輸出出口指定
    filename: '[name].js', // name就是jquery
    path: resolve(__dirname, 'dll'), // 打包到dll目錄下
    library: '[name]_[hash]', // 打包的庫裡面向外暴露出去的內容叫什麼名字
  },
  plugins: [
    // 打包生成一個manifest.json --> 提供jquery的對映關係(告訴webpack:jquery之後不需要再打包和暴露內容的名稱)
    new webpack.DllPlugin({
      name: '[name]_[hash]', // 對映庫的暴露的內容名稱
      path: resolve(__dirname, 'dll/manifest.json') // 輸出檔案路徑
    })
  ],
  mode: 'production'
};

webpack.config.js 配置:(告訴 webpack 不需要再打包 jquery,並將之前打包好的 jquery 跟其他打包好的資源一同輸出到 build 目錄下)

// 引入外掛
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

// plugins中配置:
plugins: [
  new HtmlWebpackPlugin({
    template: './src/index.html'
  }),
  // 告訴webpack哪些庫不參與打包,同時使用時的名稱也得變
  new webpack.DllReferencePlugin({
    manifest: resolve(__dirname, 'dll/manifest.json')
  }),
  // 將某個檔案打包輸出到build目錄下,並在html中自動引入該資源
  new AddAssetHtmlWebpackPlugin({
    filepath: resolve(__dirname, 'dll/jquery.js')
  })
],

5.2.2 優化程式碼執行的效能

5.2.2.1 快取

5.2.2.2 tree shaking(樹搖)

tree shaking:去除無用程式碼

前提:1. 必須使用 ES6 模組化 2. 開啟 production 環境 (這樣就自動會把無用程式碼去掉)

作用:減少程式碼體積

在 package.json 中配置:

"sideEffects": false 表示所有程式碼都沒有副作用(都可以進行 tree shaking)

這樣會導致的問題:可能會把 css / @babel/polyfill 檔案幹掉(副作用)

所以可以配置:"sideEffects": ["*.css", "*.less"] 不會對css/less檔案tree shaking處理

5.2.2.3 code split(程式碼分割)

程式碼分割。將打包輸出的一個大的 bundle.js 檔案拆分成多個小檔案,這樣可以並行載入多個檔案,比載入一個檔案更快。

1.多入口拆分

entry: {
    // 多入口:有一個入口,最終輸出就有一個bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
  },
  output: {
    // [name]:取檔名
    filename: 'js/[name].[contenthash:10].js',
    path: resolve(__dirname, 'build')
  },

2.optimization:

optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  • 將 node_modules 中的程式碼單獨打包(大小超過30kb)
  • 自動分析多入口chunk中,有沒有公共的檔案。如果有會打包成單獨一個chunk(比如兩個模組中都引入了jquery會被打包成單獨的檔案)(大小超過30kb)

3.import 動態匯入語法:

/*
  通過js程式碼,讓某個檔案被單獨打包成一個chunk
  import動態匯入語法:能將某個檔案單獨打包(test檔案不會和index打包在同一個檔案而是單獨打包)
  webpackChunkName:指定test單獨打包後文件的名字
*/
import(/* webpackChunkName: 'test' */'./test')
  .then(({ mul, count }) => {
    // 檔案載入成功~
    // eslint-disable-next-line
    console.log(mul(2, 5));
  })
  .catch(() => {
    // eslint-disable-next-line
    console.log('檔案載入失敗~');
  });

5.2.2.4 lazy loading(懶載入/預載入)

1.懶載入:當檔案需要使用時才載入(需要程式碼分割)。但是如果資源較大,載入時間就會較長,有延遲。

2.正常載入:可以認為是並行載入(同一時間載入多個檔案)沒有先後順序,先載入了不需要的資源就會浪費時間。

3.預載入 prefetch(相容性很差):會在使用之前,提前載入。等其他資源載入完畢,瀏覽器空閒了,再偷偷載入這個資源。這樣在使用時已經載入好了,速度很快。所以在懶載入的基礎上加上預載入會更好。

程式碼:

document.getElementById('btn').onclick = function() {
  // 將import的內容放在非同步回撥函式中使用,點選按鈕,test.js才會被載入(不會重複載入)
  // webpackPrefetch: true表示開啟預載入
  import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
  import('./test').then(({ mul }) => {
    console.log(mul(2, 5))
  })
};

5.2.2.5 pwa(離線可訪問技術)

pwa:離線可訪問技術(漸進式網路開發應用程式),使用 serviceworker 和 workbox 技術。優點是離線也能訪問,缺點是相容性差。

webpack.config.js 中配置:

const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); // 引入外掛

// plugins中加入:
new WorkboxWebpackPlugin.GenerateSW({
  /*
    1. 幫助serviceworker快速啟動
    2. 刪除舊的 serviceworker

    生成一個 serviceworker 配置檔案
  */
  clientsClaim: true,
  skipWaiting: true
})

index.js 中還需要寫一段程式碼來啟用它的使用:

/*
  1. eslint不認識 window、navigator全域性變數
    解決:需要修改package.json中eslintConfig配置
    "env": {
      "browser": true // 支援瀏覽器端全域性變數
    }
  2. sw程式碼必須執行在伺服器上
    --> nodejs
    或-->
      npm i serve -g
      serve -s build 啟動伺服器,將打包輸出的build目錄下所有資源作為靜態資源暴露出去
*/
if ('serviceWorker' in navigator) { // 處理相容性問題
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js') // 註冊serviceWorker
      .then(() => {
        console.log('sw註冊成功了~');
      })
      .catch(() => {
        console.log('sw註冊失敗了~');
      });
  });
}

六、Webpack 配置詳情

6.1 entry

entry: 入口起點

  1. string --> './src/index.js',單入口

    打包形成一個 chunk。 輸出一個 bundle 檔案。此時 chunk 的名稱預設是 main

  2. array --> ['./src/index.js', './src/add.js'],多入口

    所有入口檔案最終只會形成一個 chunk,輸出出去只有一個 bundle 檔案。

    (一般只用在 HMR 功能中讓 html 熱更新生效)

  3. object,多入口

    有幾個入口檔案就形成幾個 chunk,輸出幾個 bundle 檔案,此時 chunk 的名稱是 key 值

--> 特殊用法:

entry: {
  // 最終只會形成一個chunk, 輸出出去只有一個bundle檔案。
  index: ['./src/index.js', './src/count.js'], 
  // 形成一個chunk,輸出一個bundle檔案。
  add: './src/add.js'
}

6.2 output

output: {
  // 檔名稱(指定名稱+目錄)
  filename: 'js/[name].js',
  // 輸出檔案目錄(將來所有資源輸出的公共目錄)
  path: resolve(__dirname, 'build'),
  // 所有資源引入公共路徑字首 --> 'imgs/a.jpg' --> '/imgs/a.jpg'
  publicPath: '/',
  chunkFilename: 'js/[name]_chunk.js', // 指定非入口chunk的名稱
  library: '[name]', // 打包整個庫後向外暴露的變數名
  libraryTarget: 'window' // 變數名新增到哪個上 browser:window
  // libraryTarget: 'global' // node:global
  // libraryTarget: 'commonjs' // conmmonjs模組 exports
},

6.3 module

module: {
  rules: [
    // loader的配置
    {
      test: /\.css$/,
      // 多個loader用use
      use: ['style-loader', 'css-loader']
    },
    {
      test: /\.js$/,
      // 排除node_modules下的js檔案
      exclude: /node_modules/,
      // 只檢查src下的js檔案
      include: resolve(__dirname, 'src'),
      enforce: 'pre', // 優先執行
      // enforce: 'post', // 延後執行
      // 單個loader用loader
      loader: 'eslint-loader',
      options: {} // 指定配置選項
    },
    {
      // 以下配置只會生效一個
      oneOf: []
    }
  ]
},

6.4 resolve

// 解析模組的規則
resolve: {
  // 配置解析模組路徑別名: 優點:當目錄層級很複雜時,簡寫路徑;缺點:路徑不會提示
  alias: {
    $css: resolve(__dirname, 'src/css')
  },
  // 配置省略檔案路徑的字尾名(引入時就可以不寫檔案字尾名了)
  extensions: ['.js', '.json', '.jsx', '.css'],
  // 告訴 webpack 解析模組應該去找哪個目錄
  modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
}

這樣配置後,引入檔案就可以這樣簡寫:import '$css/index';

6.5 dev server

devServer: {
  // 執行程式碼所在的目錄
  contentBase: resolve(__dirname, 'build'),
  // 監視contentBase目錄下的所有檔案,一旦檔案變化就會reload
  watchContentBase: true,
  watchOptions: {
    // 忽略檔案
    ignored: /node_modules/
  },
  // 啟動gzip壓縮
  compress: true,
  // 埠號
  port: 5000,
  // 域名
  host: 'localhost',
  // 自動開啟瀏覽器
  open: true,
  // 開啟HMR功能
  hot: true,
  // 不要顯示啟動伺服器日誌資訊
  clientLogLevel: 'none',
  // 除了一些基本資訊外,其他內容都不要顯示
  quiet: true,
  // 如果出錯了,不要全屏提示
  overlay: false,
  // 伺服器代理,--> 解決開發環境跨域問題
  proxy: {
    // 一旦devServer(5000)伺服器接收到/api/xxx的請求,就會把請求轉發到另外一個伺服器3000
    '/api': {
      target: 'http://localhost:3000',
      // 傳送請求時,請求路徑重寫:將/api/xxx --> /xxx (去掉/api)
      pathRewrite: {
        '^/api': ''
      }
    }
  }
}

其中,跨域問題:同源策略中不同的協議、埠號、域名就會產生跨域。

正常的瀏覽器和伺服器之間有跨域,但是伺服器之間沒有跨域。程式碼通過代理伺服器執行,所以瀏覽器和代理伺服器之間沒有跨域,瀏覽器把請求傳送到代理伺服器上,代理伺服器替你轉發到另外一個伺服器上,伺服器之間沒有跨域,所以請求成功。代理伺服器再把接收到的響應響應給瀏覽器。這樣就解決開發環境下的跨域問題。

6.6 optimization

contenthash 快取會導致一個問題:修改 a 檔案導致 b 檔案 contenthash 變化。
因為在 index.js 中引入 a.js,打包後 index.js 中記錄了 a.js 的 hash 值,而 a.js 改變,其重新打包後的 hash 改變,導致 index.js 檔案內容中記錄的 a.js 的 hash 也改變,從而重新打包後 index.js 的 hash 值也會變,這樣就會使快取失效。(改變的是a.js檔案但是 index.js 檔案的 hash 值也改變了)
解決辦法:runtimeChunk --> 將當前模組記錄其他模組的 hash 單獨打包為一個檔案 runtime,這樣 a.js 的 hash 改變只會影響 runtime 檔案,不會影響到 index.js 檔案

output: {
  filename: 'js/[name].[contenthash:10].js',
  path: resolve(__dirname, 'build'),
  chunkFilename: 'js/[name].[contenthash:10]_chunk.js' // 指定非入口檔案的其他chunk的名字加_chunk
},
optimization: {
  splitChunks: {
    chunks: 'all',
    /* 以下都是splitChunks預設配置,可以不寫
    miniSize: 30 * 1024, // 分割的chunk最小為30kb(大於30kb的才分割)
    maxSize: 0, // 最大沒有限制
    minChunks: 1, // 要提取的chunk最少被引用1次
    maxAsyncRequests: 5, // 按需載入時並行載入的檔案的最大數量為5
    maxInitialRequests: 3, // 入口js檔案最大並行請求數量
    automaticNameDelimiter: '~', // 名稱連線符
    name: true, // 可以使用命名規則
    cacheGroups: { // 分割chunk的組
      vendors: {
        // node_modules中的檔案會被打包到vendors組的chunk中,--> vendors~xxx.js
        // 滿足上面的公共規則,大小超過30kb、至少被引用一次
        test: /[\\/]node_modules[\\/]/,
        // 優先順序
        priority: -10
      },
      default: {
        // 要提取的chunk最少被引用2次
        minChunks: 2,
        prority: -20,
        // 如果當前要打包的模組和之前已經被提取的模組是同一個,就會複用,而不是重新打包
        reuseExistingChunk: true
      }
    } */
  },
  // 將index.js記錄的a.js的hash值單獨打包到runtime檔案中
  runtimeChunk: {
    name: entrypoint => `runtime-${entrypoint.name}`
  },
  minimizer: [
    // 配置生產環境的壓縮方案:js/css
    new TerserWebpackPlugin({
      // 開啟快取
      cache: true,
      // 開啟多程序打包
      parallel: true,
      // 啟用sourceMap(否則會被壓縮掉)
      sourceMap: true
    })
  ]
}

七、Webpack5 介紹和使用

此版本重點關注以下內容:

  • 通過持久快取提高構建效能.
  • 使用更好的演算法和預設值來改善長期快取.
  • 通過更好的樹搖和程式碼生成來改善捆綁包大小.
  • 清除處於怪異狀態的內部結構,同時在 v4 中實現功能而不引入任何重大更改.
  • 通過引入重大更改來為將來的功能做準備,以使我們能夠儘可能長時間地使用 v5.

下載

npm i webpack@next webpack-cli -D

自動刪除 Node.js Polyfills

早期,webpack 的目標是允許在瀏覽器中執行大多數 node.js 模組,但是模組格局發生了變化,許多模組用途現在主要是為前端目的而編寫的。webpack <= 4 附帶了許多 node.js 核心模組的 polyfill,一旦模組使用任何核心模組(即 crypto 模組),這些模組就會自動應用。

儘管這使使用為 node.js 編寫的模組變得容易,但它會將這些巨大的 polyfill 新增到包中。在許多情況下,這些 polyfill 是不必要的。

webpack 5 會自動停止填充這些核心模組,並專注於與前端相容的模組。

遷移:

  • 儘可能嘗試使用與前端相容的模組。
  • 可以為 node.js 核心模組手動新增一個 polyfill。錯誤訊息將提示如何實現該目標。

Chunk 和模組 ID

添加了用於長期快取的新演算法。在生產模式下預設情況下啟用這些功能。

chunkIds: "deterministic", moduleIds: "deterministic"

Chunk ID

你可以不用使用 import(/* webpackChunkName: "name" */ "module") 在開發環境來為 chunk 命名,生產環境還是有必要的

webpack 內部有 chunk 命名規則,不再是以 id(0, 1, 2)命名了

Tree Shaking

  1. webpack 現在能夠處理對巢狀模組的 tree shaking
// inner.js
export const a = 1;
export const b = 2;

// module.js
import * as inner from './inner';
export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a);

在生產環境中, inner 模組暴露的 b 會被刪除

  1. webpack 現在能夠多個模組之前的關係
import { something } from './something';

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

當設定了"sideEffects": false時,一旦發現test方法沒有使用,不但刪除test,還會刪除"./something"

  1. webpack 現在能處理對 Commonjs 的 tree shaking

Output

webpack 4 預設只能輸出 ES5 程式碼

webpack 5 開始新增一個屬性 output.ecmaVersion, 可以生成 ES5 和 ES6 / ES2015 程式碼.

如:output.ecmaVersion: 2015

SplitChunk

// webpack4
minSize: 30000;
// webpack5
minSize: {
  javascript: 30000,
  style: 50000,
}

Caching

// 配置快取
cache: {
  // 磁碟儲存
  type: "filesystem",
  buildDependencies: {
    // 當配置修改時,快取失效
    config: [__filename]
  }
}

快取將儲存到 node_modules/.cache/webpack

監視輸出檔案

之前 webpack 總是在第一次構建時輸出全部檔案,但是監視重新構建時會只更新修改的檔案。

此次更新在第一次構建時會找到輸出檔案看是否有變化,從而決定要不要輸出全部檔案。

預設值

  • entry: "./src/index.js
  • output.path: path.resolve(__dirname, "dist")
  • output.filename: "[name].js"