webpack基本用法及原理(10000+)

1 webpack是什麼

所有工具的出現,都是為了解決特定的問題,那麼前端熟悉的webpack是為了解決什麼問題呢?

1.1 為什麼會出現webpack

js模組化:
瀏覽器認識的語言是HTML,CSS,Javascript,而其中css和javascript是通過html的標籤link,script引入進來。

隨著前端項目的越來越複雜,css和js文件會越來越龐大,那麼在開發階段,就必須要把css和js按功能拆分成幾個小文件,方便開發。

那麼拆分的小文件如何引入到html中呢?css可以通過link標籤或者@importcss語法,但是js因為沒有模組導入的語法(ES6有了import,但還不是所有瀏覽器兼容),就只能通過script標籤引入。但是這樣的話會導致很多問題:

  1. http請求大量增多,影響頁面呈現速度。
  2. 全局變數混亂,難以維護。

針對js模組化出現了很多的解決方案,總結來說有幾種規範:

  1. CommonJs,語法為:require(), module.exports(同步載入,適用於node伺服器環境)
  2. ES6 Mode,語法為:import,export(非同步載入,適用於瀏覽器環境)
  3. AMD,語法為:require(),define()(非同步載入,適用於瀏覽器環境)

工程化:
除了js模組化的問題之外,前端還有很多其他的問題,比如程式碼混淆,程式碼壓縮,scss,less等css預編譯語言的編譯,typescript的編譯,eslint檢驗程式碼規範,如果這些任務都需要手工去執行的話,太繁瑣,也容易出錯。

1.2 webpack能做什麼

  1. 模組化
    其實webpack的核心就是解決js模組化問題的工具,運行在node環境中,同時可以支援commonjs,es6,amd的模組語法(可以使用:reuire/mocule.exports,import/export,require/define的方式來導入導出模組)。

    可以將開發時候拆分為不同文件的js程式碼,打包成一個js文件。也可以通過配置靈活的拆分js程式碼,通過 tree shaking 刪減沒有使用到的程式碼。

    模組化打包時webpack的核心功能,但是它還有兩個非常重要的機制loader和plugin。

  2. loader
    webpack本身只支援js,json文件的模組化打包,但是有開放出loader介面,通過不同的loader可以將其他格式的文件轉化為可識別的模組,比如:
    css-loader可以識別css文件,raw-loader可以直接將文件當作模組,less-loader,sass-loader可以直接識別less,sass文件。

  3. plugin
    插件機制是webpack的另一個重要的拓展,webpack在打包的過程中,會暴露出不同的生命周期事件,而插件會監聽這些事件,然後做出對應的操作,比如:
    UglifyJsPlugin可以混淆壓縮程式碼,EslintWebpackPlugin可以執行eslint的程式碼格式檢測和自動修復。

總結:
webpack是一個運行在node環境下,對js文件進行模組化打包的工具。通過loader機制可以實現除js格式外的其他格式文件,通過plugin機制可以實現自動執行一些工程化需要的任務。

2 webpack怎麼用

那麼webpack要怎麼使用呢?

2.1 安裝運行

首先要安裝webapck,使用npm(npm基本用法及原理),安裝webpack(核心),webpack-cli(命令行工具):

npm install webpack webpack-cli

然後創建以下兩個文件:name.js,index.js,
我們以es6的語法導入導出模組,es6模式的模組變數的導出時按引用導出,就是在模組的變數如果在外部被修改,也會作用到模組內部,而commonjs的模式是按值導出,即模組外部的修改,不會影響到模組內部。

//name.js
let name = "小明"
function say(){
    console.log('my name is ',name)
}
export  {
    name,
    say
}

//index.js
import { name,say } from "./name.js";

name = "小紅"
say()
console.log('he name is ',name)

然後運行打包命令:

//用npx直接運行webpack命令
npx webpack 

//或者用npm的腳本運行打包
//package.json
{
	script:{
		pack:'webpack'
	}
}
npm run pack

webpack默認從index.js文件開始打包,所以如果開始文件的名稱為index,就可以不需要寫配置文件,就可以直接打包。
默認打包的模式是『production’即生產模式,打包成功後,會自動創建dist文件夾,並生成main.js文件:

//main.js
(()=>{"use strict";let e="小明";e="小紅",console.log("my name is ","小紅"),console.log("he name is ","小紅")})();

我們看到打包後的文件把index.js和name.js兩個文件合成了一個文件,並對程式碼進行了混淆壓縮(生產模式),這是最基本的webpack最核心的功能 6— 打包。
但是顯然,在實際工作中我們不會這麼簡單的使用,那就需要用的配置文件了,下面是一個比較接近實際工作中的例子。

2.2 配置文件 webpack-config.js

以下的項目會有幾個文件:index.js , utils.js, style.scss, index.html, webpack-config.js。
基本功能就是在index.js文件中引入utils.js文件里的方法並調用。然後用scss語法編寫樣式,最後把打包的文件加入到已有的index.html文件中。
通過命令:npx webpack serve(可以放入npm腳本配置中,然後運行 npm run xxx),實現的效果是:

  • scss自動編譯
  • index.js utils.js style.scss 文件打包成一個文件
  • 把打包的文件自動添加到index.html中
  • 打包完成後,自動打開默認瀏覽器,查看頁面
  • 文件有變更的話,會自動重新打包,刷新頁面
//utils.js
export function sayHello(){
    console.log('hello world')
}
//index.js
import './styles.scss'
import { sayHello } from "./utils";
sayHello()
/*styles.scss*/
$bg : black;
$fontC:rgb(218, 17, 117);
body{
    background:$bg; 
    h3{
        color:$fontC;
    }
}
<!--index.html-->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>webpack</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
      <h3>hello webpack</h3>
  </body>
</html>
//webpack-config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 打包模式
  mode:'development', //development,production,none
  devtool:'cheap-source-map', // eval-source-map,source-map,cheap-source-map
  // 入口配置
  entry: {
    app: './src/index.js',
  },
  // 出口配置
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,//每次打包,清除dist文件夾
  },

  // 本地伺服器
  devServer:{
    port:8888,//埠
    open:true,//自動打開瀏覽器
    hot:true,//啟動熱更新
  },

  // loader
  module:{
    // 處理scss文件
    rules:[
      {
        test:/\.s[ac]ss$/i,
        use:[
          'style-loader',//將js模組生成style標籤節點
          'css-loader',//將css轉化成js模組
          'sass-loader'//將scss文件編譯成css文件
        ]
      }
    ]
  },

  // 插件
  plugins: [
    // 自動把打包後的文件加入到html文件
    new HtmlWebpackPlugin({
      // 生成html文件的模板
      template: './index.html'
    }),
  ],
};

webpack的打包是在node環境下執行的,所以node的語法這裡都可以用,最終輸出的是一個js對象。
配置可以分成幾個部分(參考webpack配置文檔)

  • 打包模式

    • mode 預置了開發環境和生產環境的一些優化。
      在這裡插入圖片描述

    • devtool 控制是否生成,以及如何生成 source map。有了source map文件的話,如果程式碼有報錯可以映射到打包之前的程式碼(源程式碼),可以方便定位錯誤。
      可以有很多的選擇,一般來說,在開發環境下選擇:eval-cheap-module-source-map,cheap-source-map。生產環境選擇:不配置,source-map。

  • 入口、出口

    • entry 入口文件,webpack會從這個文件開始查找依賴的包,可以配置多個
    • output 出口文件,webpack會根據這裡的配置,輸出打包後的文件。
  • 本地伺服器
    此功能需要安裝webpack-dev-server插件npm install --save-dev webpack-dev-server,啟動時需要用serve命令npx webpack serve.
    啟動成功後,會在本地開啟一個web伺服器,並且有實時更新,熱模組替換等功能。
    配置項是在devServer中。

  • loader
    webpack本是只支援對js文件的打包,但是因為有loader機制,可以通過配置rules實現對其他文件的打包。
    示例程式碼中實現的是對scss/sass文件的打包,同一個relues中的loader的執行順序是從右到左(逆序),所以順序不能亂。第一個執行的loader 會將其結果(被轉換後的資源)傳遞給下一個 要執行的loader。

  • 插件plugins
    webpack在打包的時候,會暴露其生命周期,插件就是在特定的生命周期執行的操作,通過插件的機制,可以實現很多強大的功能。
    示例程式碼中使用了HtmlWebpackPlugin插件,功能是在打包完成後,自動把打包後的程式碼加入到html文件中。如果沒有任何配置,則會自動生成一個html文件,並通過<script>標籤把js文件引入進來。

3 實踐中的優化

3.1 配置文件拆分與合併–merge

在實際項目中,開發環境和生產環境的配置往往會有很大的區別,所以會有兩個配置文件,而這兩個配置文件又會有一些公共的配置,所以就會有如下三個配置文件:

  • webpack.dev.js
  • webpack.prod.js
  • webpack.common.js
    那麼這些配置文件是如何結合的呢?這就要用到webpack-merge插件了。
// webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 入口配置
  entry: {
    app: './src/index.js',
  },
  // 出口配置
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,//每次打包,清除dist文件夾
  },
  // 插件
  plugins: [
    // 自動把打包後的文件加入到html文件
    new HtmlWebpackPlugin({
      // 生成html文件的模板
      template: './index.html'
    }),
  ],
};
//webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
  // 本地伺服器
  devServer:{
    port:8888,//埠
    open:true,//自動打開瀏覽器
    hot:true,//啟動熱更新
  },
});
//webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
  devtool:'source-map'
});
// package.json
{
	...
  "scripts": {
    "dev": "webpack serve --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js",
  },
	...
}

然後分別執行npm run dev,npm run build就可以了。

3.2 程式碼分離

webpack會把所有程式碼打包成一個文件(包括業務程式碼,npm包),這樣最後的包就會很大,打包效率也很慢,所以可以有時候需要做程式碼分離。

  • 第三方庫分離
    有一些第三方庫可能會需要獨立引入,而不是放在業務程式碼裡面,因為不會改動或者需要cdn服務,比如jquery有免費的cdn服務://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js
    那這些獨立引入的js文件就不需要加入到webpack打包,只需要在externals添加配置就行。

    // webpack.config.js
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      // 打包模式
      mode:'development', //development,production,none
      devtool:'cheap-source-map', // eval-source-map,source-map,cheap-source-map
      // 入口配置
      entry: {
        app: './src/index.js',
      },
      // 出口配置
      output: {
        filename: '[name].[contenthash].js',//contenthash 是文件內容的hash值
        path: path.resolve(__dirname, 'dist'),
        clean: true,//每次打包,清除dist文件夾
      },
    
      // 插件
      plugins: [
        // 自動把打包後的文件加入到html文件
        new HtmlWebpackPlugin({
          // 生成html文件的模板
          template: './index.html'
        }),
      ],
    };
    

    這樣的話,雖然index.js里有引入jquery,webpack也不會把jquery打包進來,打包時間會減少,包的體積也會變小。

  • npm包分離
    第三方庫除了一些可以用script標籤引入的,大多數是通過npm引入的,這一類的js包也會也會合併到最後的app.js總包之中,使得app.js文件會過大,而且如果業務程式碼有一點改動的話,app.js的包就會全部都變動,導致瀏覽器就會重新下載app.js文件,使用不了瀏覽器內置的快取機制。
    我們可以通過配置,讓npm里的包與業務程式碼分開。

    // index.js
    
    import _ from 'lodash'
    import $ from 'jquery'
    import { sayHello } from "./utils"
    
    $('#title').text('hello jquery')
    
    // webpack.config.js
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      // 打包模式
      mode:'development', //development,production,none
      devtool:'cheap-source-map', // eval-source-map,source-map,cheap-source-map
      // 入口配置
      entry: {
        app: './src/index.js',
      },
      // 出口配置
      output: {
        filename: '[name].[contenthash].js', //contenthash是文件內容的hash值
        path: path.resolve(__dirname, 'dist'),
        clean: true,//每次打包,清除dist文件夾
      },
    
      // 插件
      plugins: [
        // 自動把打包後的文件加入到html文件
        new HtmlWebpackPlugin({
          // 生成html文件的模板
          template: './index.html'
        }),
      ],
      optimization: {
        runtimeChunk: 'single',// 把webpack引導文件獨立出來
        splitChunks: {
          cacheGroups: {
            vendor: {
            //所有node_modules下的包合併成一個,並獨立出來
              test: /[\\/]node_modules[\\/]/, 
              //控制哪種導入方式的js包才分離出來, 'all'-全部的js包,'async'-非同步導入的js包,'initial'-初始導入的js包
              chunks: 'all',
              name:'vendor' //獨立後的包的名稱
            }
          }
        }
      },
    };
    
    

    這樣打包目錄下就有三個文件:

    • app.js: 業務程式碼
    • runtime.js:webpack的引導程式碼
    • vendor.js: npm引入的js包程式碼

    一般有變動的就只有app.js文件了。npm引入的js包是否可以再分成幾個文件呢?可以的,參看 SplitChunksPlugin文檔

  • 業務程式碼分離
    有時候不僅第三方庫需要分離,我們自己寫的業務程式碼可能也會很大,也需要分離。要實現業務程式碼的分離只要添加多個入口就可以了。

    // index.js
    import { sayHello } from "./utils"
    
    sayHello()
    console.log('hello index')
    
    // utils.js
    export function sayHello(){
        console.log('hello utils')
    }
    
    // webpack.config.js
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
     mode: 'development',
     devtool: 'cheap-source-map',
      // 入口配置
      entry: {
        app: {
          import:'./src/index.js',
          dependOn:'utils'
        },
        utils:'./src/utils.js'
      },
      // 出口配置
      output: {
        filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,//每次打包,清楚dist文件夾
      },
    
      // 插件
      plugins: [
        // 自動把打包後的文件加入到html文件
        new HtmlWebpackPlugin({
          // 生成html文件的模板
          template: './index.html'
        }),
      ],
    };
    

    這樣utils.js文件也從主包app.js中分離了出來,要注意的是app入口加了dependOn:'utils',為了讓app.js裡面不要重複打包utils.js。

  • 動態載入
    有時候不是需要頁面一開始的時候,就載入全部的js包,而是等到特定的時機再去載入某些js包,這就需要動態載入了,只需要用到import()就可以了,注意這是import的函數使用方式。

    // index.js
    
      const element = document.createElement('div');
      element.id = 'title'
      element.innerHTML ='Hello webpack';
    
      const button = document.createElement('button');
      button.innerHTML = 'Click me';
      button.onclick = importJquery;
    
    
      document.body.appendChild( element);
      document.body.appendChild( button);
    
      async function importJquery(){
    	    const { default: $ } = await import('jquery');
    	    $('#title').text('hello jquery')
    	}
    
    // webpack.config.js
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
     mode: 'development',
     devtool: 'cheap-source-map',
      // 入口配置
      entry: {
        app: {
          import:'./src/index.js',
        },
      },
      // 出口配置
      output: {
        filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,//每次打包,清楚dist文件夾
      },
    
      // 插件
      plugins: [
        // 自動把打包後的文件加入到html文件
        new HtmlWebpackPlugin({
          // 生成html文件的模板
          template: './index.html'
        }),
      ],
    };
    

    如果運行程式碼的話,會發現頁面一開始進入的時候,並沒有引入jquery的包,但是點擊按鈕的時候,就開始導入了,這就動態載入。
    並且發現webpack.config.js並沒有做什麼特殊的配置,這是因為動態導入的js包webpack會自動給獨立為一個js文件。
    import()返回的是一個promise對象。

3.3 動態鏈接庫 dll

webpack每次打包的時候,都會把涉及到的js包都處理一遍。但是實際上有些js包是不會有改動到的,所以打包過後的文件每次都是一樣的,每次都重新打包的話,會加增打包時間。
有一種解決方案是:把不會變動的js包先打包一次,以後每次打包的時候,直接引用就可以了。
先添加一個獨立的配置文件 webpack.dll.config.js

//webpack.dll.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  // 入口文件
  entry: {
    // 項目中用到該兩個依賴庫文件
    jquery_lodash: ['jquery','lodash'],
  },
  // 輸出文件
  output: {
    // 文件名稱
    filename: '[name].dll.js', 
    // 將輸出的文件放到dll目錄下
    path: path.resolve(__dirname, 'dll'),
	
	// 文件輸出的全局變數
    library: '_dll_[name]',
  },
  plugins: [
    // 使用插件 DllPlugin
    new webpack.DllPlugin({
        // 動態鏈接庫的全局變數名稱,需要和 output.library 中保持一致
        // 該欄位的值也就是輸出的 manifest.json 文件 中 name 欄位的值
        name: '_dll_[name]',
        // 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
        path: path.join(__dirname, 'dll', '[name].manifest.json')
      }),
  ]
};

執行打包腳本 npx webpack --config webpack.dll.config.js,就會再dll目錄下輸出文件:jquery_lodash.dll.js,jquery_lodash.manifest.json
主要用到的插件是:

  • DllPlugin 的作用就是生成manifest.json文件。

然後配置項目打包用的webpack.config.js文件:

//webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');


module.exports = {
  mode: 'development',
  devtool: 'cheap-source-map',
  // 入口配置
  entry: {
    app: {
      import:'./src/index.js',
    },
  },
  // 出口配置
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,//每次打包,清楚dist文件夾
  },

  // 插件
  plugins: [
    // 自動把打包後的文件加入到html文件
    new HtmlWebpackPlugin({
      // 生成html文件的模板
      template: './index.html'
    }),
    // 引用dll中的文件,
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./dll/jquery_lodash_dll.manifest.json')
    }),
    //把dll文件加入到index.html中
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/jquery_lodash_dll.dll.js'),
      publicPath: './',
    }),
  ],
};

主要用到的插件是:

  • DllReferencePlugin 檢索引用文件的時候,如果發現manifest.json裡面有,就告訴webpack不要打包該文件,因為已經打包好了。
  • AddAssetHtmlWebpackPlugin 因為webpack沒有打包dll里的文件,所以需要手動把它加入到index.html中。

然後就可以正常的打包項目了:

// index.js
 import _ from 'lodash'
 import $ from 'jquery'

$('#title').text('hello jquery')

執行打包腳本 npx webpack,會自動使用webpack.config.js配置文件打包。會發現打包速度提高了很多。