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服務:https://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配置檔案打包。會發現打包速度提高了很多。