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

webpack 打包

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

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ì)路徑的話(huà), ./ 不能省略
  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ì)不同類(lèi)型的文件進(jìn)行處理。Loader 是實(shí)現(xiàn)前端模塊化的核心防症,借助于 Loader 可以加載任何類(lèi)型的資源孟辑。

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

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

  • 編譯轉(zhuǎn)換類(lèi)饲嗽。例如 css-loader,將 css 代碼轉(zhuǎn)換為 js 進(jìn)行工作奈嘿。
  • 文件操作類(lèi)貌虾。例如 file-loader,對(duì)文件進(jìn)行拷貝尽狠,再將文件路徑向外導(dǎo)出。
  • 代碼檢查類(lèi)巫财。統(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 專(zhuān)注實(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ì)路徑的話(huà)夕晓, ./ 不能省略
  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ì)不同類(lèi)型的文件進(jìn)行處理
  // Loader 是實(shí)現(xiàn)前端模塊化的核心酸些,借助于 Loader 就可以加載任何類(lèi)型的資源
  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è)類(lèi)型
  // 使用插件就是創(chuàng)建一個(gè)這個(gè)類(lèi)型的實(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 類(lèi)型
      // 如果使用了 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: 拷貝
    // 開(kāi)發(fā)階段最好不要使用這個(gè)插件
    new CopyWebpackPlugin({
      patterns: [
        { from: 'public', to: 'public' }
      ]
    })
  ]
}

webpack-dev-server

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

webpack-dev-server 默認(rèn)會(huì)把所有可以打包的文件放到內(nèi)存里(不會(huì)寫(xiě)入磁盤(pán))。一般在開(kāi)發(fā)階段不需要將靜態(tài)資源打包茬缩,還可以通過(guò)在 devServer 中配置 contentBase吼旧,設(shè)置為靜態(tài)資源的目錄,開(kāi)發(fā)階段就可以訪(fǎng)問(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)致用戶(hù)的操作狀態(tài)丟失扰法,這個(gè)時(shí)候我們可以使用 HMR - 熱更新(熱替換)毅厚,熱替換只將修改的模塊實(shí)時(shí)替換至應(yīng)用中吸耿,在頁(yè)面同步更新的同時(shí)保持應(yīng)用的運(yùn)行狀態(tài)不受影響。它極大程度的提高了開(kāi)發(fā)者的工作效率锤岸。

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

  • 首先引入 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í)沒(méi)有實(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

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

  • 能夠定位到行
  • 定位到的文件會(huì)以真實(shí)代碼的樣子顯示(cheap-eval-source-map 會(huì)顯示轉(zhuǎn)譯成es5 的樣子空执,不好和源代碼定位)
  • 雖然首次打包速度慢辨绊,但是重寫(xiě)打包相對(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
}

這樣的話(huà)码俩,執(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 的依賴(lài) '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 為我們提供了很多開(kāi)箱即用的插件媳危。

eg: DefinePlugin

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

適用方法:

      new webpack.DefinePlugin({
        // 這里的 value 是一個(gè) js 代碼片段集侯,所以傳入的是 字符串,這個(gè)字符串里邊是一個(gè)字符串
        // 也可以寫(xiě)成 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)目中一些沒(méi)有引用的東西生闲。例如一個(gè)函數(shù)中 return 后的操作會(huì)被移除月幌,export 出去的成員沒(méi)有引用會(huì)被移除等飞醉。

Tree Shaking 在生產(chǎn)環(huán)境打包過(guò)程中會(huì)自動(dòng)開(kāi)啟缅帘。在其他環(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)起見(jià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è)趯?xiě)組件的時(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)開(kāi)啟這個(gè)屬性(這個(gè)屬性在生產(chǎn)環(huán)境會(huì)自動(dòng)開(kāi)啟)

optimization: {
    sideEffects: true,
}

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

image.png

使用 sideEffects 的前提是確保你的代碼真的沒(méi)有副作用爵政,否則在打包時(shí)就會(huì)誤刪掉有副作用的代碼钾挟。比如引入的 css 文件,或者引入一個(gè)擴(kuò)展的對(duì)象的原型方法的 js 文件徽千,它們沒(méi)有導(dǎo)出任何成員,所以在引入的時(shí)候也不用導(dǎo)入什么成員百框,但是在引入后可以使用它們提供的方法铐维,這就屬于這個(gè) css 或 js 的副作用慎菲。這個(gè)時(shí)候還標(biāo)識(shí)沒(méi)有副作用的話(huà)露该,這些文件就不會(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)該分離開(kāi)來(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è)屬性就是打包的一路入口
  // 屬性名就是入口名稱(chēng)圾笨,值就是入口路徑
  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)入

按需加載是我們開(kāi)發(fā)瀏覽器應(yīng)用一個(gè)常見(jiàn)的需求悟耘,一般我們常說(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' */ 為魔法注釋?zhuān)绻麤](méi)有魔法注釋?zhuān)瑢?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'
        ]
}

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

OptimizeCssAssetsWebpackPlugin

當(dāng)我們打包生產(chǎn)的包時(shí)會(huì)發(fā)現(xiàn)岁经,剛剛提取出來(lái)的 css 文件沒(méi)有被壓縮,這是因?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è)特性開(kāi)啟是才會(huì)工作躺屁,所以webpack 建議壓縮類(lèi)的插件應(yīng)該配置在 minimizer 中经宏,以便于可以通過(guò) minimize 這個(gè)選項(xiàng)統(tǒng)一控制烛恤。

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

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

可以發(fā)現(xiàn),css 文件壓縮了苹熏,但是這時(shí)又沒(méi)有壓縮了轨域,這是因?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)資源緩存枯怖。這樣的話(huà)能曾,對(duì)于用戶(hù)的瀏覽器而言借浊,可以緩存住我們應(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ú)法更新到客戶(hù)端打颤。所以在生產(chǎn)模式下漓滔,我們建議給文件名設(shè)置 hash 值,一旦資源文件改變反肋,文件名稱(chēng)也可以一起變化石蔗,對(duì)于客戶(hù)端而言,全新的文件名就是全新的請(qǐng)求养距,也就沒(méi)有緩存的問(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盆昙。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘟芝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子晤郑,更是在濱河造成了極大的恐慌造寝,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褐鸥,死亡現(xiàn)場(chǎng)離奇詭異叫榕,居然都是意外死亡姊舵,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)荞下,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)尖昏,“玉大人构资,你說(shuō)我怎么就攤上這事〖L剩” “怎么了己单?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵纹笼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我矮嫉,道長(zhǎng)牍疏,這世上最難降的妖魔是什么鳞陨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任瞻惋,我火速辦了婚禮歼狼,結(jié)果婚禮上羽峰,老公的妹妹穿的比我還像新娘添瓷。我一直安慰自己,他們只是感情好坯汤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布惰聂。 她就那樣靜靜地躺著咱筛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪溉愁。 梳的紋絲不亂的頭發(fā)上沙热,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天篙贸,我揣著相機(jī)與錄音爵川,去河邊找鬼息楔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛圃泡,可吹牛的內(nèi)容都是我干的愿险。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鳖目,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼缤弦!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起狸捅,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤薪贫,失蹤者是張志新(化名)和其女友劉穎刻恭,沒(méi)想到半個(gè)月后鳍贾,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡橡淑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年梁棠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了斗埂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡男娄,死狀恐怖模闲,靈堂內(nèi)的尸體忽然破棺而出崭捍,到底是詐尸還是另有隱情,我是刑警寧澤翁授,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站贮配,受9級(jí)特大地震影響塞赂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜圆存,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一仇哆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧油讯,春花似錦延欠、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)心肪。三九已至,卻和暖如春贰镣,著一層夾襖步出監(jiān)牢的瞬間膳凝,已是汗流浹背恭陡。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工休玩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劫狠,地道東北人永部。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓苔埋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親荞膘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子玉工,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容