學(xué)習(xí)筆記-webpack

webpack 打包

以下針對(duì) webpack 為 5 的情況,所有依賴的版本如下:

image.png

快速上手

const path = require('path')

module.exports = {
  // mode:工作模式:development, production, null
  // 不設(shè)置默認(rèn)為 production。
  // production 模式會(huì)自動(dòng)啟用一些優(yōu)化插件,比如壓縮,打包結(jié)果無(wú)法閱讀
  // development 模式會(huì)自動(dòng)優(yōu)化打包速度,添加調(diào)試過(guò)程中的輔助
  // null 模式運(yùn)行最原始狀態(tài)的打包,不做任何額外的處理
  mode: 'none',
  // entry:入口文件路徑。 如果是相對(duì)路徑的話, ./ 不能省略
  entry: './src/main.js',
  // output: 輸出文件路徑,是一個(gè)對(duì)象
  output: {
    // filename:輸出文件名
    filename: 'bundle.js',
    // 輸出文件路徑,必須為絕對(duì)路徑,所以使用 path.join(__dirname, xxx)
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/',  // 打包過(guò)后的文件最終位置
  },
  
}

Loader

在我們的項(xiàng)目中,我們需要處理的不僅僅是 js 的代碼,我們可以使用加載器對(duì)不同類型的文件進(jìn)行處理。Loader 是實(shí)現(xiàn)前端模塊化的核心,借助于 Loader 可以加載任何類型的資源。

通過(guò)配置 module 來(lái)配置 loader。rules 為規(guī)則配置。

可以將 Loader 分為幾個(gè)類型:

  • 編譯轉(zhuǎn)換類。例如 css-loader,將 css 代碼轉(zhuǎn)換為 js 進(jìn)行工作。
  • 文件操作類。例如 file-loader,對(duì)文件進(jìn)行拷貝,再將文件路徑向外導(dǎo)出。
  • 代碼檢查類。統(tǒng)一代碼風(fēng)格,從而提高代碼質(zhì)量,不會(huì)修改生產(chǎn)環(huán)境的代碼。例如 eslint-loader。
const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/',
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          // es6+ 新特性可以使用 babel-loader 進(jìn)行編譯轉(zhuǎn)換
          // webpack 只是打包工具,加載器可以用來(lái)編譯轉(zhuǎn)換代碼
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        // 匹配打包過(guò)程中遇到的文件路徑
        test: /.css$/,
        // 如果配置了多個(gè) loader,執(zhí)行時(shí)從后往前執(zhí)行
        use: [
          'style-loader',  // 將 css-loader 轉(zhuǎn)換過(guò)后的結(jié)果通過(guò) style 標(biāo)簽的形式追加到頁(yè)面上
          'css-loader'  // 將css文件轉(zhuǎn)換為js模塊,
        ]
      },
      {
        test: /\.(png|jpg|gif)$/i,
        // 小文件使用 Data URLs,減少請(qǐng)求次數(shù)
        // 大文件單獨(dú)提取存放,提高加載速度
        // use: 'file-loader',  // 文件資源加載器
        // use: 'url-loader'  // 將圖片轉(zhuǎn)換為 Data Urls,圖片將被轉(zhuǎn)為 base64
        // 將 use 設(shè)為對(duì)象,loader 設(shè)為 url-loader,并設(shè)置一個(gè) limit
        // 此時(shí)文件小于 10kb 使用 url-loader,大于10kb則默認(rèn)使用 file-loader
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10 * 1024,  // 10 KB
              // esModule: false, // 解決 html 中載入圖片導(dǎo)致的[Object Module]問(wèn)題
            }
          }
        ]
      },
      {
        test: /\.html$/i,
        loader: 'html-loader',
        options: {
          esModule: false,  // 禁用 es modules 語(yǔ)法
          sources: {
            list: [
              '...',
              {
                tag: 'a',
                attribute: 'href',
                type: 'src'
              }
            ]
          }
        }
      },
    ]
  }
}

Plugin

插件機(jī)制是 webpack 中另外一個(gè)核心特性,目的是為了增強(qiáng) webpack 在項(xiàng)目自動(dòng)化方面的能力。Loader 專注實(shí)現(xiàn)資源模塊加載,從而實(shí)現(xiàn)整體項(xiàng)目打包,而 Plugin 是為了解決除資源加載以外其他的自動(dòng)化工作。eg:

  • 在打包之前清除上一次的 dist 目錄
  • 拷貝靜態(tài)文件至輸出目錄
  • 壓縮輸出代碼

clean-webpack-plugin:用來(lái)在打包前清除 dist 的插件。

html-webpack-plugin:自動(dòng)生成使用 bundle.js 的 HTML。由于我們的HTML都是通過(guò)硬編碼的方式放在根目錄下,發(fā)布的時(shí)候需要同時(shí)發(fā)布這個(gè)HTML文件,而且還要確保資源文件路徑正確,需要手動(dòng)修改。通過(guò)這個(gè)插件就可以解決這個(gè)問(wèn)題。webpack 打包的時(shí)候知道自己生成了多少 bundle,將 bundle 自動(dòng)放入 HTML 文件中,這樣 html 也輸出到了 dist 目錄,而且對(duì) bundle 的引入是注入的,能夠確保路徑正確。

copy-webpack-plugin: 拷貝文件。

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  // mode:工作模式:development, production, null
  // 不設(shè)置默認(rèn)為 production。
  // production 模式會(huì)自動(dòng)啟用一些優(yōu)化插件,比如壓縮,打包結(jié)果無(wú)法閱讀
  // development 模式會(huì)自動(dòng)優(yōu)化打包速度,添加調(diào)試過(guò)程中的輔助
  // null 模式運(yùn)行最原始狀態(tài)的打包,不做任何額外的處理
  mode: 'none',
  // entry:入口文件路徑。 如果是相對(duì)路徑的話, ./ 不能省略
  entry: './src/main.js',
  // output: 輸出文件路徑,是一個(gè)對(duì)象
  output: {
    // filename:輸出文件名
    filename: 'bundle.js',
    // 輸出文件路徑,必須為絕對(duì)路徑,所以使用 path.join(__dirname, xxx)
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/',  // 打包過(guò)后的文件最終位置
  },
  // 使用加載器對(duì)不同類型的文件進(jìn)行處理
  // Loader 是實(shí)現(xiàn)前端模塊化的核心,借助于 Loader 就可以加載任何類型的資源
  module: {
    // 規(guī)則配置
    rules: [
      {
        test: /.js$/,
        exclude: /(node_modules)|ejs$/,
        use: {
          // es6+ 新特性可以使用 babel-loader 進(jìn)行編譯轉(zhuǎn)換
          // webpack 只是打包工具,加載器可以用來(lái)編譯轉(zhuǎn)換代碼
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        // 匹配打包過(guò)程中遇到的文件路徑
        test: /.css$/,
        // 如果配置了多個(gè) loader,執(zhí)行時(shí)從后往前執(zhí)行
        use: [
          'style-loader',  // 將 css-loader 轉(zhuǎn)換過(guò)后的結(jié)果通過(guò) style 標(biāo)簽的形式追加到頁(yè)面上
          'css-loader'  // 將css文件轉(zhuǎn)換為js模塊,
        ]
      },
      {
        test: /\.(png|jpg|gif)$/i,
        // 小文件使用 Data URLs,減少請(qǐng)求次數(shù)
        // 大文件單獨(dú)提取存放,提高加載速度
        // use: 'file-loader',  // 文件資源加載器
        // use: 'url-loader'  // 將圖片轉(zhuǎn)換為 Data Urls,圖片將被轉(zhuǎn)為 base64
        // 將 use 設(shè)為對(duì)象,loader 設(shè)為 url-loader,并設(shè)置一個(gè) limit
        // 此時(shí)文件小于 10kb 使用 url-loader,大于10kb則默認(rèn)使用 file-loader
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10 * 1024,  // 10 KB
              // esModule: false, // 解決 html 中載入圖片導(dǎo)致的[Object Module]問(wèn)題
            }
          }
        ]
      },
      {
        test: /\.html$/i,
        loader: 'html-loader',
        options: {
          esModule: false,  // 禁用 es modules 語(yǔ)法
          sources: {
            list: [
              '...',
              {
                tag: 'a',
                attribute: 'href',
                type: 'src'
              }
            ]
          }
        }
      },

      {
        test: /.md$/,
        use: [
          'html-loader',
          './markdown-loader'
        ]
      }
    ]
  },
  // plugins: 用來(lái)配置插件
  // 絕大多數(shù)插件都是導(dǎo)出一個(gè)類型
  // 使用插件就是創(chuàng)建一個(gè)這個(gè)類型的實(shí)例,將實(shí)例放入 Plugin 數(shù)組中
  plugins: [
    // clean-webpack-plugin 是用來(lái)在打包前清除 dist 的插件
    new CleanWebpackPlugin(),
    // html-webpack-plugin 是自動(dòng)生成使用 bundle.js 的 HTML
    // html-webpack-plugin 中也可以傳入一個(gè) options 作為配置選項(xiàng)
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      meta: {     // 設(shè)置元數(shù)據(jù)標(biāo)簽
        viewport: 'width=device-width'
      },
      title: 'webpack plugin sample',  // 標(biāo)題
      // html-webpack-plugin 中的 <%= htmlWebpackPlugin.options.title %> 會(huì)被 html-loader 當(dāng)做字符串處理,所以會(huì)不生效,需要將 html 模板改為 ejs 類型
      // 如果使用了 babel-loader,就會(huì)去跑 ejs 文件,會(huì)報(bào)錯(cuò),所以要在 babel-loader 設(shè)置忽略 ejs 文件
      template: 'src/index.ejs'   // 模板,根據(jù)模板生成頁(yè)面
    }),
    // html-webpack-plugin 可以用于生成多個(gè) html 文件
    // 用于生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    }),

    // copy-webpack-plugin: 拷貝
    // 開發(fā)階段最好不要使用這個(gè)插件
    new CopyWebpackPlugin({
      patterns: [
        { from: 'public', to: 'public' }
      ]
    })
  ]
}

webpack-dev-server

安裝依賴,然后執(zhí)行 yarn webpack serve --open 會(huì)自動(dòng)打開瀏覽器并打開 watch 模式監(jiān)聽頁(yè)面變化刷新頁(yè)面。

webpack-dev-server 默認(rèn)會(huì)把所有可以打包的文件放到內(nèi)存里(不會(huì)寫入磁盤)。一般在開發(fā)階段不需要將靜態(tài)資源打包,還可以通過(guò)在 devServer 中配置 contentBase,設(shè)置為靜態(tài)資源的目錄,開發(fā)階段就可以訪問(wèn)到靜態(tài)資源。

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/',  // 打包過(guò)后的文件最終位置
  },
  devServer:{
  
    // 靜態(tài)資源目錄,可以是字符串或數(shù)組,也就是說(shuō)可以配置一個(gè)或多個(gè)
    contentBase:'./public'
  },
}

HMR - 熱更新

自動(dòng)刷新頁(yè)面會(huì)導(dǎo)致用戶的操作狀態(tài)丟失,這個(gè)時(shí)候我們可以使用 HMR - 熱更新(熱替換),熱替換只將修改的模塊實(shí)時(shí)替換至應(yīng)用中,在頁(yè)面同步更新的同時(shí)保持應(yīng)用的運(yùn)行狀態(tài)不受影響。它極大程度的提高了開發(fā)者的工作效率。

HMR 已經(jīng)集成在 webpack-dev-server 中,不需要再引入依賴。

  • 首先引入 webpack
const webpack = require('webpack')
  • 在 devServer 中配置 hot:true
devServer: {
    hot: true
}
  • 在 plugins 中配置插件
// 熱更新
new webpack.HotModuleReplacementPlugin()

然后運(yùn)行項(xiàng)目,發(fā)現(xiàn)修改 css 時(shí)實(shí)現(xiàn)了熱更新,而修改 js 時(shí)沒有實(shí)現(xiàn)熱更新。這是因?yàn)樾枰謩?dòng)處理JS更新后的熱更新邏輯。在成熟的項(xiàng)目框架中是不需要我們手動(dòng)來(lái)處理的,因?yàn)榭蚣芤呀?jīng)為我們處理了。

Proxy - 代理

devServer: {
    // 可以是字符串或數(shù)組,也就是說(shuō)可以配置一個(gè)或多個(gè)
    contentBase: './public',
    proxy: {
      '/api': {
        // http://localhost:8080/api/users -> https://api.github.com/api/user
        target: 'https://api.github.com',
        // http://localhost:8080/api/users -> https://api.github.com/user
        pathRewrite: {
          '^/api': ''
        },
        // 不能使用 localhost:8080 作為請(qǐng)求 github 的主機(jī)名
        changeOrigin: true
      }
    }
  },

Source Map

通過(guò) webpack 打包后的項(xiàng)目,我們想要調(diào)試或者定位錯(cuò)誤信息就會(huì)變的困難,可以用過(guò) Source Map 來(lái)解決。它是一個(gè)源代碼和轉(zhuǎn)換后的代碼的映射,一個(gè)轉(zhuǎn)換過(guò)后的代碼,通過(guò) Source Map 的逆向解析,就可以得到源代碼。

先簡(jiǎn)單使用一下:

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/',  // 打包過(guò)后的文件最終位置
  },
  // 在你的配置中加入這行代碼
  devtool: 'source-map',
}
image.png

報(bào)錯(cuò)信息就會(huì)現(xiàn)實(shí)文件名,點(diǎn)擊文件名就會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的代碼,而且可以在這里進(jìn)行斷點(diǎn)調(diào)試。

image.png

devtool 不僅有 source-map 的值,還有很多的值,他們的區(qū)別有編譯速度,重新編譯速度,適用環(huán)境等。

image.png

開發(fā)環(huán)境(cheap-module-eval-source-map):

  • 能夠定位到行
  • 定位到的文件會(huì)以真實(shí)代碼的樣子顯示(cheap-eval-source-map 會(huì)顯示轉(zhuǎn)譯成es5 的樣子,不好和源代碼定位)
  • 雖然首次打包速度慢,但是重寫打包相對(duì)較快

生產(chǎn)環(huán)境(nosources-source-map)

  • Source Map 會(huì)暴露源代碼
  • 可以找到報(bào)錯(cuò)源代碼的位置

webpack 不同環(huán)境下的配置

webpack.config.js 中的 module.exports 可以導(dǎo)出一個(gè)對(duì)象,也可以導(dǎo)出一個(gè)數(shù)組,里邊為多組配置,同樣也可以導(dǎo)出一個(gè)函數(shù),函數(shù)的參數(shù)是 env(環(huán)境)和 argv(運(yùn)行 cli 過(guò)程中傳入的所有參數(shù))。

module.exports = (env, argv) => {
  // 這里放置所有基本配置
  const config = {
  ...
  }
  if(env === 'production'){
    config.mode = 'production'
    config.devtool = false
    config.plugins = {
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    }
  }
  return config
}

這樣的話,執(zhí)行 yarn webpack 默認(rèn)打包的還是dev的配置,執(zhí)行 yarn webpack --env production,就相當(dāng)于給 傳遞了參數(shù) env 為 productioin,從而實(shí)現(xiàn) prod的打包。

但是更多情況下,項(xiàng)目比較大,都是通過(guò)配置不同文件來(lái)實(shí)現(xiàn)的。3個(gè)文件,一個(gè)公共配置,一個(gè)dev配置,一個(gè)prod配置,在dev 和prod配置文件中將公共配置文件引入,使用webpack 的依賴 'webpack-merge', 將 自己的配置 merge 到公共配置,這樣向plugins 這種數(shù)組結(jié)構(gòu)的也可以 merge 過(guò)去,并且不會(huì)將原來(lái)的配置完全覆蓋,而是合并處理。

const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
  mode:'production',
  plugins:[
     new CleanWebpackPlugin(),
     new CopyWebpackPlugin({
        patterns: [
          { from: 'public', to: 'public' }
        ]
      }),
  ]
})

DefinePlugin

webpack 為我們提供了很多開箱即用的插件。

eg: DefinePlugin

為代碼注入全局成員,自動(dòng)啟用,會(huì)為全局注入 process.env.NODE_ENV 常量,用來(lái)判斷運(yùn)行環(huán)境。

適用方法:

      new webpack.DefinePlugin({
        // 這里的 value 是一個(gè) js 代碼片段,所以傳入的是 字符串,這個(gè)字符串里邊是一個(gè)字符串
        // 也可以寫成 JSON.stringify('https://api.example.com')
        // API_BASE_URL:'"https://api.example.com"'
        API_BASE_URL: JSON.stringify('https://api.example.com')
      })

然后在全局打印 API_BASE_URL 變量,可以拿到它的值。

Tree Shaking

Tree Shaking 是在打包時(shí)自動(dòng)去除項(xiàng)目中一些沒有引用的東西。例如一個(gè)函數(shù)中 return 后的操作會(huì)被移除,export 出去的成員沒有引用會(huì)被移除等。

Tree Shaking 在生產(chǎn)環(huán)境打包過(guò)程中會(huì)自動(dòng)開啟。在其他環(huán)境,需要自己手動(dòng)配置:

module.export = {
  mode:'none',
  ...
  // optimization: 用來(lái)集中配置 webpack 的優(yōu)化功能
  optimization: {
    // 模塊只導(dǎo)出被使用的成員
    usedExports: true,
    // 盡可能合并每一個(gè)模塊到一個(gè)函數(shù)中
    // concatenateModules: true,
    // 壓縮輸出結(jié)果
    minimize: true
  }
}

Tree Shaking 只在 ES Module 語(yǔ)法生效,但是如果項(xiàng)目打包配置了 babel-loader,它可能會(huì)將 ES Module 轉(zhuǎn)換為 CommonJS 規(guī)范,那么 Tree Shaking 將不會(huì)生效。不過(guò)最新版本的 babel-loader 已經(jīng)自動(dòng)關(guān)閉了ES Module 轉(zhuǎn)換的插件,所以不會(huì)出現(xiàn)這個(gè)問(wèn)題,但是為了保險(xiǎn)起見,可以對(duì) babel-loader 進(jìn)行一些配置。

      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加載模塊時(shí)已經(jīng)轉(zhuǎn)換了 ESM,則會(huì)導(dǎo)致 Tree Shaking 失效
              // modules 表示轉(zhuǎn)換為什么模式, 設(shè)為 false 代表不會(huì)將 ES Module 轉(zhuǎn)換為 CommonJS,auto 默認(rèn)配置最新版中也會(huì)關(guān)閉轉(zhuǎn)換
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // 也可以使用默認(rèn)配置,也就是 auto,這樣 babel-loader 會(huì)自動(dòng)關(guān)閉 ESM 轉(zhuǎn)換
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }

concatenateModules - 合并模塊

webpack 打包后將一個(gè)模塊打包成一個(gè)函數(shù),就會(huì)有多個(gè)模塊函數(shù)。通過(guò) concatenateModules 可以合并模塊。=,載配合 minimize 進(jìn)行壓縮,就會(huì)大大較少體積。

sideEffects - 副作用

它允許我們通過(guò)配置的方式標(biāo)識(shí)我們的代碼是否有副作用,從而為 Tree Shaking 提供更大的壓縮空間。

副作用:模塊執(zhí)行時(shí)除了導(dǎo)出成員之外所做的事情。

image.png

如上圖,一般我們?cè)趯懡M件的時(shí)候,會(huì)有多個(gè)組件文件,然后在 components/index 中統(tǒng)一導(dǎo)入再導(dǎo)出,但是在 index 中我們可能只引入一個(gè)組件,但是因?yàn)橐肓?'./components', 而 ‘components/index’ 又引入了所有模塊,導(dǎo)致所有組件都被加載執(zhí)行。sideEffects 就可以解決這個(gè)問(wèn)題。

我們?cè)?webpack.config.js 中的 optimization 中設(shè)置 sideEffects:true 來(lái)開啟這個(gè)屬性(這個(gè)屬性在生產(chǎn)環(huán)境會(huì)自動(dòng)開啟)

optimization: {
    sideEffects: true,
}

webpack 在打包的時(shí)候就會(huì)檢查 package.json 中是否有 sideEffects 的標(biāo)識(shí),以此來(lái)判斷是否有副作用,我們來(lái)設(shè)置為 false, 表示沒有副作用,這樣打包后,沒有用到的組件就不會(huì)被打包進(jìn)來(lái)了。

image.png

使用 sideEffects 的前提是確保你的代碼真的沒有副作用,否則在打包時(shí)就會(huì)誤刪掉有副作用的代碼。比如引入的 css 文件,或者引入一個(gè)擴(kuò)展的對(duì)象的原型方法的 js 文件,它們沒有導(dǎo)出任何成員,所以在引入的時(shí)候也不用導(dǎo)入什么成員,但是在引入后可以使用它們提供的方法,這就屬于這個(gè) css 或 js 的副作用。這個(gè)時(shí)候還標(biāo)識(shí)沒有副作用的話,這些文件就不會(huì)被打包,這時(shí)可以在 Package.json 中關(guān)掉 sideEffects 或者設(shè)置哪些文件有副作用,這樣 webpack 就不會(huì)忽略這些文件了。

image.png

代碼分割

通過(guò) webpack 實(shí)現(xiàn)前端整體模塊化的優(yōu)勢(shì)固然很明顯,但是它同樣存在一些弊端,那就是我們項(xiàng)目中所有代碼最終都會(huì)被打包到一起。如果我們的應(yīng)用非常復(fù)雜,模塊非常多,bundle 體積就會(huì)特別的大,而大多數(shù)時(shí)候并不是每個(gè)模塊在啟動(dòng)時(shí)都是必要的,但是這些又被打包到一起,就必須把所有模塊都加載進(jìn)來(lái)才能使用。應(yīng)用運(yùn)行在瀏覽器端,這就意味著會(huì)浪費(fèi)掉很多的流量和帶寬。所以我們需要把打包結(jié)果按照一定的規(guī)則分離到多個(gè) bundle 中,然后根據(jù)應(yīng)用的運(yùn)行需要按需加載。這樣就可以大大提高應(yīng)用的響應(yīng)效率以及運(yùn)行速度。

前面說(shuō)過(guò) webpack 就是把我們項(xiàng)目中散落的模塊打包到一起從而提高運(yùn)行效率,這里又說(shuō)應(yīng)該分離開來(lái),這兩個(gè)是不是自相矛盾呢?

其實(shí)不是的,只是物極必反。資源太大了也不行,太碎了也不行。

webpack 支持一種分包的功能,也就是代碼分割。

Code Splitting - 代碼分包/代碼分割

  • 多入口打包
  • 動(dòng)態(tài)導(dǎo)入

多入口打包

多入口打包一般適用于傳統(tǒng)的多頁(yè)應(yīng)用程序。一個(gè)頁(yè)面對(duì)應(yīng)一個(gè)打包入口,對(duì)于頁(yè)面公共部分再單獨(dú)提取。

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  // 將 entry 配置成一個(gè)對(duì)象
  // 一個(gè)屬性就是打包的一路入口
  // 屬性名就是入口名稱,值就是入口路徑
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    // 輸出文件名動(dòng)態(tài)輸出
    filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      // 由于打包后會(huì)將所有的 bundle 載入html,但是我們只需要載入對(duì)應(yīng)的那個(gè) bundle 載入對(duì)應(yīng)的 html
      // chunks 指定載入的 bundle
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

提取公共模塊

此時(shí) index 和 album 模塊中都會(huì)引入一些公共模塊,如果引入一些大型的模塊,比如vue 等,就會(huì)讓每個(gè)模塊體積都很大,所以我們需要把公共模塊提取出來(lái)。

optimizatioin: {
    splitChunks: {
      // 表示會(huì)把所有的公共模塊都提取到單獨(dú)的 bundle 中
      chunks: 'all'
    }
},

動(dòng)態(tài)導(dǎo)入

按需加載是我們開發(fā)瀏覽器應(yīng)用一個(gè)常見的需求,一般我們常說(shuō)的按需加載指的是加載數(shù)據(jù),這里所說(shuō)的按需加載指的是我們應(yīng)用在運(yùn)行過(guò)程中需要用到某個(gè)模塊時(shí),再加載這個(gè)模塊。這種方式可以極大的節(jié)省我們的帶寬和流量。webpack 中支持使用動(dòng)態(tài)導(dǎo)入的方式實(shí)現(xiàn)按需加載,而且所有動(dòng)態(tài)導(dǎo)入的模塊都會(huì)被自動(dòng)的提取到單獨(dú)的 bundle 中,從而實(shí)現(xiàn)分包。對(duì)比與多入口的方式,動(dòng)態(tài)導(dǎo)入更加靈活,可以通過(guò)代碼的邏輯去控制需不需要加載某個(gè)模塊,或者什么時(shí)候需要加載某個(gè)模塊。而我們分包的目的中就有很重要的一點(diǎn)是要讓模塊實(shí)現(xiàn)按需加載,從而提高應(yīng)用的響應(yīng)速度。

// import posts from './posts/posts'
// import album from './album/album'

const render = () => {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''

  if (hash === '#posts') {
    // mainElement.appendChild(posts())
    
    // 將以上直接導(dǎo)入的方式改為這種動(dòng)態(tài)導(dǎo)入的方式
    // 這是ES Module 提供的動(dòng)態(tài)導(dǎo)入,返回一個(gè) Promise 對(duì)象,用 then 方法可以接受返回值
    // /* webpackChunkName: 'components' */ 為魔法注釋,如果沒有魔法注釋,導(dǎo)出的文件將會(huì)以序號(hào)命名
    // 加上魔法注釋可以為組件起一個(gè)名字,如果名字相同,將會(huì)打包到同一個(gè) bundle
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)

只需按照 ES Module 的按需加載的方式導(dǎo)入,webpack 無(wú)需處理,就可以自動(dòng)分包。

MiniCssExtractPlugin

MiniCssExtractPlugin 是一個(gè)可以將 css 從打包結(jié)果提取出來(lái)的插件,通過(guò)這個(gè)插件可以實(shí)現(xiàn) css 的按需加載。

  • 首先引入插件 const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  • 然后在 plugins 中加入 new MiniCssExtractPlugin()
  • 在 loader 中我們之前是先通過(guò) css-loader 去解析,然后交給 style-loader 將樣式通過(guò) style 標(biāo)簽注入。使用 MiniCssExtractPlugin 我們是將樣式放入文件中通過(guò) Link 的方式引入,也就不需要使用 style-loader,使用 MiniCssExtractPlugin.loader 的方式注入。
{
        test: /\.css$/,
        use: [
          // 'style-loader', // 將樣式通過(guò) style 標(biāo)簽注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
}

需要注意的是,如果樣式文件體積不是很大的話,提取到單個(gè)文件中效果可能適得其反。如果 css 體積超過(guò)了 150kb 左右,才需要考慮是否將它提取到單獨(dú)文件中,否則的話 css 嵌入到代碼當(dāng)中減少了一次請(qǐng)求效果可能會(huì)更好。

OptimizeCssAssetsWebpackPlugin

當(dāng)我們打包生產(chǎn)的包時(shí)會(huì)發(fā)現(xiàn),剛剛提取出來(lái)的 css 文件沒有被壓縮,這是因?yàn)?webpack 提供的壓縮只針對(duì) js 文件,想要對(duì) css 文件進(jìn)行壓縮就需要借助插件,,webpack 官方推薦了一個(gè)插件 - ss-assets-webpack-plugin。

在 plugins 中加入 new mizeCssAssetsWebpackPlugin(),打包后css 文件也被壓縮了。

但是在官方文檔中會(huì)發(fā)現(xiàn),這個(gè)插件并不是配置在 plugins 屬性中,而是在 optimization 的 minimizer 屬性中。這是因?yàn)槿绻渲迷?plugins 下,這個(gè)插件在任何情況下都會(huì)正常工作,而配置在 minimizer 中,只會(huì)在 minimize 這樣一個(gè)特性開啟是才會(huì)工作,所以webpack 建議壓縮類的插件應(yīng)該配置在 minimizer 中,以便于可以通過(guò) minimize 這個(gè)選項(xiàng)統(tǒng)一控制。

optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin()
    ]
},

執(zhí)行 yarn webpack --mode production

可以發(fā)現(xiàn),css 文件壓縮了,但是這時(shí)又沒有壓縮了,這是因?yàn)樵O(shè)置了 minimizer 數(shù)組,webpack 認(rèn)為如果配置了這個(gè)數(shù)組,就是要用自定義壓縮插件,內(nèi)部的 js 壓縮器就會(huì)被覆蓋掉,所以我們需要手動(dòng)再把它添加回來(lái)。

yarn add terser-webpack-plugin --dev

const TerserWebpackPlugin = require('terser-webpack-plugin')

再將這個(gè)插件手動(dòng)添加進(jìn) minimizer

optimization: {
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
},

這時(shí)在生產(chǎn)模式打包,js 和 css 文件都可以被正常壓縮了。

輸出文件名 Hash

一般我們?cè)诓渴鹎岸速Y源文件時(shí),都會(huì)啟用服務(wù)器的靜態(tài)資源緩存。這樣的話,對(duì)于用戶的瀏覽器而言,可以緩存住我們應(yīng)用中的靜態(tài)資源,后續(xù)就不再需要請(qǐng)求服務(wù)器得到靜態(tài)資源文件了。整體應(yīng)用的響應(yīng)速度就有一個(gè)大幅度提升。不過(guò)也會(huì)有一些小小的問(wèn)題,如果緩存時(shí)間設(shè)置過(guò)短,效果不是特別明顯,如果過(guò)期時(shí)間設(shè)置的比較長(zhǎng),在這個(gè)過(guò)程中應(yīng)用重新部署,那這些更新將無(wú)法更新到客戶端。所以在生產(chǎn)模式下,我們建議給文件名設(shè)置 hash 值,一旦資源文件改變,文件名稱也可以一起變化,對(duì)于客戶端而言,全新的文件名就是全新的請(qǐng)求,也就沒有緩存的問(wèn)題,這樣就可以把服務(wù)端的緩存時(shí)間設(shè)置的特別長(zhǎng),也就不用擔(dān)心文件更新過(guò)后的問(wèn)題。

webpack 中 output 中的 filename 和插件中的 filename 都支持設(shè)置 hash 值。它們支持3中 hash,效果各不相同。

  • 首先是最普通的 hash,可以通過(guò) [hash] 拿到。這種 hash 是整個(gè)項(xiàng)目級(jí)別的,也就是這個(gè)項(xiàng)目中有任何一個(gè)改動(dòng),所有的 hash 值都會(huì)改變。
image.png
  • chunkhash:在打包過(guò)程中,同一路的打包 chunkhash 都是相同的。比如動(dòng)態(tài)導(dǎo)入的文件是一路 chunk。
image.png

這是兩路 chunk。

一個(gè)文件改變,同一個(gè) chunk 下的 chunkhash 都會(huì)改變,如果文件被別的文件引入,那引入的那個(gè)文件 chunkhash 也會(huì)被動(dòng)改變。相比于普通 hash,chunkhash 更精確。

  • contenthash: 文件級(jí)別的hash。文件修改時(shí)對(duì)應(yīng)的 bundle 的 contenthash 修改,引入它的文件也被動(dòng)修改。

contenthash 是解決緩存問(wèn)題最好的方式,因?yàn)樗_的定位到了文件級(jí)別的 hash。

如果覺得 20 位的 hash 太長(zhǎng),可以指定長(zhǎng)度,[contenthash:8] 指定 hash 長(zhǎng)度為8。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容