首頁  >  文章  >  web前端  >  Webpack是什麼?詳解它是如何運作的?

Webpack是什麼?詳解它是如何運作的?

青灯夜游
青灯夜游轉載
2022-10-13 19:36:142543瀏覽

Webpack是什麼?詳解它是如何運作的?

Webpack簡介

Webpack是一款模組打包工具。它為不同的依賴創建模組,將其整體打包成可管理的輸出檔案。這一點對於單一頁面應用(現今Web應用的事實標準)來說特別有用。

假設我們有一個可以執行兩個簡單數學任務(加法和乘法)的應用程序,為了方便維護,我們決定切分這些函數到不同的文件中去。

index.html

<html>
<head>
    <script src="src/sum.js"></script>
    <script src="src/multiply.js"></script>
    <script src="src/index.js"></script>
</head>
</html>

index.js

var totalMultiply = multiply(5, 3);
var totalSum = sum(5, 3);
console.log(&#39;Product of 5 and 3 = &#39; + totalMultiply);
console.log(&#39;Sum of 5 and 3 = &#39; + totalSum);

multiply.js

#
var multiply = function (a, b) {
    var total = 0;
    for (var i = 0; i < b; i++) {
        total = sum(a, total);
    }
    return total;
};

sum.js

var sum = function (a, b) {
    return a + b;
};

這個應用程式的輸出應該是:

Product of 5 and 3 = 15
Sum of 5 and 3 = 8

Webpack是如何幫助我們的?

我們不能只是使用工具,而不知道這些工具能幫助我們做什麼。那麼,Webpack幫我們做了什麼呢?

用模組來拯救依賴

在上面的程式碼中,我們可以看到,multiply.jsindex.js皆依賴sum.js。因此,如果index.html導入依賴時使用了錯誤的順序,那麼我們的應用程式就無法運作。舉個例子,如果index.js先被導入,或sum.jsmultiply.js之後被導入,都會得到錯誤。

基於上面的例子,讓我們想像一下,一個真實的Web應用往往可能會包含多達幾十個依賴項,這些依賴項之間還可能存在依賴關係,維護這些依賴項之間的順序想想就讓人窒息。這裡還可能存在變數被其它依賴覆蓋的風險,而這將會導致難以發現的BUG。

為了解決這個痛點,Webpack會將依賴轉換為作用域更小的模組,從而避免變數被覆蓋。依賴轉換為模組帶來的額外好處是,Webpack可以為我們管理這些依賴。具體做法是,Webpack會在需要時,把依賴模組引入進來,並且匹配對應的作用域。

透過打包來減少HTTP請求次數

#我們還是回看一下index.html,這個檔案中我們需要下載三個獨立的檔案。當然這裡文件比較少還能夠應付,但還是之前提到的問題,真實的Web應用中,依賴項可能會很多,而這將會導致用戶不得不等待所有依賴項挨個下載完成後,主應用程序才能運行。

而這就引出了Webpack的另一個特性-打包。 Webpack可以將所有的依賴物打包成一個文件,而這就意味著,使用者只需要下載一個依賴項,主應用程式就可以運作。

綜上所述,Webpack的主要特性就是打包模組化。透過使用外掛程式和載入器,我們可以擴充Webpack的這兩大功能。

讓依賴可用,並組合它們

我們將使用CommonJS模組語法,作為初始設定。當然,這裡也有諸如AMD,ES2015等其它選擇,但這裡我們將先使用CommonJS,稍後遷移到ES2015。

CommonJS將模組匯出,使得其它程式碼可以使用導出模組中的函數或變數。我們可以透過require將導出模組中的值讀出來。

index.html

<html>
<head>
    <script src="./dist/bundle.js""></script>
</head>
</html>

index.js

var multiply = require('./multiply');
var sum = require('./sum');
var totalMultiply = multiply(5, 3);
var totalSum = sum(5, 3);
console.log(&#39;Product of 5 and 3 = &#39; + totalMultiply);
console.log(&#39;Sum of 5 and 3 = &#39; + totalSum);

multiply.js

#
var sum = require('./sum');
var multiply = function (a, b) {
    var total = 0;
    for (var i = 0; i < b; i++) {
        total = sum(a, total);
    }
    return total;
};
module.exports = multiply;

sum.js

var sum = function (a, b) {
    return a + b;
};
module.exports = sum;

觀察上面的程式碼,不難發現,為了讓函數sum與函數multiply能夠被使用,我們在sum.jsmultiply.js腳本中,導出了這兩個函數。這裡有個細節,不知道大家是否注意到了,在index.html中我們現在只需要導入一個bundle.js檔。

這可幫大忙了!我們現在不再需要關注依賴的順序,可以暴露我們想暴露的內容,並使得其它內容仍然保持私有。同時,我們現在只需要導入一個文件,而不是三個,這有助於提高應用程式的載入速度。

Webpack的初始設定

為了實現我們上面要達到的效果,我們需要對Webpack做一些初步的設定。

var path = require(&#39;path&#39;);
module.exports = {
  entry: &#39;./src/index.js&#39;,    
  output: {
    path: path.resolve(__dirname, &#39;./dist/&#39;),
    filename: &#39;bundle.js&#39;
  }
}

這裡我們實作了一個最簡單的設定。我們至少要告訴Webpack,我們應用的入口在哪,輸出結果應該是什麼。讓我們來詳細看看每個屬性所代表的意義。

  • entry: 这个属性表示应用的入口。入口就意味着,这是我们加载程序和程序逻辑的起点。Webpack将从这个入口开始,遍历整棵依赖树。根据遍历结果建立一个依赖间关系图,并创建需要的模块。
  • output.path: 这个属性表示存放打包结果的绝对路径。这里为了方便使用,我们采用了Node.js自带的函数path,这个函数能够根据我们程序所处的位置,动态的创建绝对路径。其中,__dirname是Node.js的一个工具变量,它表示当前文件的目录名。
  • output.filename: 这个属性表示打包结果的文件名。它的名字可以是任意的,只不过我们习惯叫它bundle.js

来看看bundle.js

阅读生成的bundle.js代码,可以给我们带来一些启发。

// the webpack bootstrap
(function (modules) {
    // The module cache
    var installedModules = {};
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        // Create a new module (and put it into the cache)
        // Execute the module function
        // Flag the module as loaded
        // Return the exports of the module
    }


    // expose the modules object (__webpack_modules__)
    // expose the module cache
    // Load entry module and return exports
    return __webpack_require__(0);
})
/************************************************************************/
([
    // index.js - our application logic
    /* 0 */
    function (module, exports, __webpack_require__) {
        var multiply = __webpack_require__(1);
        var sum = __webpack_require__(2);
        var totalMultiply = multiply(5, 3);
        var totalSum = sum(5, 3);
        console.log(&#39;Product of 5 and 3 = &#39; + totalMultiply);
        console.log(&#39;Sum of 5 and 3 = &#39; + totalSum);
    },
    // multiply.js
    /* 1 */
    function (module, exports, __webpack_require__) {
        var sum = __webpack_require__(2);
        var multiply = function (a, b) {
            var total = 0;
            for (var i = 0; i < b; i++) {
                total = sum(a, total);
            }
            return total;
        };
        module.exports = multiply;
    },
    // sum.js
    /* 2 */
    function (module, exports) {
        var sum = function (a, b) {
            return a + b;
        };
        module.exports = sum;
    }
]);

从上面的代码可以看出,Webpack把每一个js脚本都封装到一个模块中,并把这些模块放进数组中。模块数组被传入Webpack的引导程序中,引导程序会把这些模块加入Webpack并执行,使得模块可用。

这里bundle.js返回的是__webpack_require__(0),而这刚好对应了模块数组中的index.js部分。基于此我们同样可以得到正确的运行结果,而不需要处理依赖管理,下载依赖的次数也仅需要一次。

Loader让Webpack更好用

Webpack仅能理解最基本的JavaScript ES5代码,它自身仅支持创建模块并打包JavaScript ES5。如果我们不仅仅局限于JavaScript ES5,例如我们想使用ES2015,这就需要告诉Webpack如何处理ES2015。这里我们的处理方式往往是,我们需要将其它语言(如TypeScript)或其它版本(如ES2015)预处理成JavaScript ES5,再让Webpack做打包。这里就需要使用Babel来做转换,把ES2015转换为ES5(当然Babel能做的事情远不止如此)。

为了说明这个过程,我们使用ES2015重写之前的功能。

index.js

import multiply from &#39;./multiply&#39;;
import sum from &#39;./sum&#39;;
const totalMultiply = multiply(5, 3);
const totalSum = sum(5, 3);
console.log(`Product of 5 and 3 = ${totalMultiply}`);
console.log(`Sum of 5 and 3 = ${totalSum}`);

multiply.js

import sum from &#39;./sum&#39;;
const multiply = (a, b) => {
    let total = 0;
    for(let i=0;i<b;i++) {
        total = sum(a, total);
    }
    return total;
};
export default multiply;

sum.js

const sum = (a, b) => a + b;
export default sum;

这里我们使用了很多ES2015的新特性,例如箭头函数、const关键字、模板字符串和ES2015的导入导出。ES2015的代码Webpack无法处理,所以我们需要Babel来进行转换。想要让Babel配合Webpack完成工作,我们就需要用到Babel Loader。事实上,Loader就是Webpack处理JavaScript ES5以外内容的方式。有了加载器,我们就可以让Webpack处理各式各样的文件。

想要在Webpack中使用Babel Loader,我们还需要三个Babel依赖:

  • babel-loader: 提供Babel与Webpack之间的接口;

  • babel-core: 提供读取和解析代码的功能,并生成对应的输出;

  • babel-preset-es2015: 提供将ES2015转换为ES5的Babel规则;

在Webpack中配置Babel Loader的代码,差不多是下面这样子:

const path = require(&#39;path&#39;);
module.exports = {
    entry: &#39;./src/index.js&#39;,
    output: {
        path: path.resolve(__dirname, &#39;./dist/&#39;),
        filename: &#39;bundle.js&#39;
    },
    module: {
        loaders: [
            {
                test: /.js$/,
                loader: &#39;babel-loader&#39;,
                exclude: /node_modules/,
                query: {
                    presets: [&#39;es2015&#39;]
                }
            }
        ]
    }
};

这段代码你可以在webpack.config.js中找到。值得注意的是,Webpack中是支持同时存在多个Loader的,所以提供的值是一个数组。接着,还是让我们来看看每个属性代表的含义。

  • test: 我们只希望Loader处理JavaScript文件,这里通过一个正则表达式匹配.js文件;
  • loader: 要使用的Loader,这里使用了babel-loader
  • exclude: 哪些文件不需要被处理,这里我们不希望Babel处理node_modules下的任何文件;
  • query.presets: 我们需要使用哪个规则,这里我们使用Babel转换ES2015的规则;

配置好这些内容后,再次查看打包生成的bundle.js,其中的内容看起来就像下面这样:

/* 2 */
function(module, exports) {
  var sum = function sum(a, b) {
    return a + b;
    };
    
    module.exports = sum;
}

可以看到,Babel Loader已经把ES2015的代码变成了ES5的代码。

CSS与样式,让Webpack看起来更出色

接下来,让我们拓展上面的例子,输出计算结果。我们将在页面上创建一个body,然后把乘积与加和的结果添加到span中。

import multiply from &#39;./multiply&#39;;
import sum from &#39;./sum&#39;;

const totalMultiply = multiply(5, 3);
const totalSum = sum(5, 3);

// create the body
const body = document.createElement("body");
document.documentElement.appendChild(body);

// calculate the product and add it to a span
const multiplyResultsSpan = document.createElement(&#39;span&#39;);
multiplyResultsSpan.appendChild(document.createTextNode(`Product of 5 and 3 = ${totalMultiply}`));

// calculate the sum and add it to a span
const sumResultSpan = document.createElement(&#39;span&#39;);
sumResultSpan.appendChild(document.createTextNode(`Sum of 5 and 3 = ${totalSum}`));

// add the results to the page
document.body.appendChild(multiplyResultsSpan);
document.body.appendChild(sumResultSpan);

这段代码的输出结果,应该与之前是一致的,区别仅在于显示在页面上。

Product of 5 and 3 = 15 Sum of 5 and 3 = 8

我们可以使用CSS来美化这个结果,比如,我们可以让每个结果都独占一行,并且给每个结果都加上边框。为了实现这一点,我们可以使用如下的CSS代码。

span {
    border: 5px solid brown;
    display: block;
}

我们需要将这个CSS也导入应用中。这里最简单的解决方案是,在我们的html中添加一个link标签。但有了Webpack提供的强大功能,我们可以先导入它,再用Webpack来处理这个样式。

在代码中导入CSS带来的另一个好处是,开发者可以清晰的看到CSS与其使用之间的关联。这里需要注意的是,CSS的作用域并不局限于它所导入的模块本身,其作用域仍然是全局的,但从开发者的角度看,这样使用更加清晰。

import multiply from &#39;./multiply&#39;;
import sum from &#39;./sum&#39;;

// import the CSS we want to use here
import &#39;./math_output.css&#39;;

const totalMultiply = multiply(5, 3);
const totalSum = sum(5, 3);

// create the body
const body = document.createElement("body");
document.documentElement.appendChild(body);

// calculate the product and add it to a span
const multiplyResultsSpan = document.createElement(&#39;span&#39;);
multiplyResultsSpan.appendChild(document.createTextNode(`Product of 5 and 3 = ${totalMultiply}`));

// calculate the sum and add it to a span
const sumResultSpan = document.createElement(&#39;span&#39;);
sumResultSpan.appendChild(document.createTextNode(`Sum of 5 and 3 = ${totalSum}`));

// add the results to the page
document.body.appendChild(multiplyResultsSpan);
document.body.appendChild(sumResultSpan);

这段代码中,与前面代码的唯一区别在于,我们导入了CSS。我们需要两个Loader来处理我们的CSS:

  • css-loader: 用于处理CSS导入,具体来说,获取导入的CSS并加载CSS文件内容;

  • style-loader: 获取CSS数据,并添加它们到HTML文档中;

现在我们在webpack.config.js中的Webpack配置看起来像这样:

const path = require(&#39;path&#39;);
module.exports = {
    entry: &#39;./src/index.js&#39;,
    output: {
        path: path.resolve(__dirname, &#39;./dist/&#39;),
        filename: &#39;bundle.js&#39;
    },
    module: {
        loaders: [
            {
                test: /.js$/,
                loader: &#39;babel-loader&#39;,
                exclude: /node_modules/,
                query: {
                    presets: [&#39;es2015&#39;]
                }
            },
            {
                test: /.css$/,
                loaders: [&#39;style-loader&#39;, &#39;css-loader&#39;]
            }
        ]
    }
};

我们还是来看看新增的CSS配置属性所表示的内容。

  • test: 我们需要告诉Loader,我们只需要它处理CSS文件。这里的正则表达式仅匹配CSS文件。
  • loaders: 这里与前面不同的是,我们使用了多个Loader。还有一个需要注意的细节是,Webpack从右向左处理loader,因此css-loader处理的结果(读出CSS文件内容)会被传递给style-loader,最终得到的是style-loader的处理结果(将样式添加到HTML文档中)。

假如我们现在需要提取CSS,并输出到一个文件中,再导入该文件。为了实现这一点,我们就要用到Plugin。Loader的作用在于,在数据被打包输出前进行预处理。而Plugin则可以阻止预处理的内容直接出现在我们的打包结果中。

我们的Webpack配置现在变成了这样:

const path = require(&#39;path&#39;);
const ExtractTextPlugin = require(&#39;extract-text-webpack-plugin&#39;);
module.exports = {
    entry: &#39;./src/index.js&#39;,
    output: {
        path: path.resolve(__dirname, &#39;./dist/&#39;),
        filename: &#39;bundle.js&#39;
    },
    module: {
        loaders: [
            {
                test: /.js$/,
                loader: &#39;babel-loader&#39;,
                exclude: /node_modules/,
                query: {
                    presets: [&#39;es2015&#39;]
                }
            },
            {
                test: /.css$/,
                loader: ExtractTextPlugin.extract(&#39;css-loader&#39;)
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin(&#39;style.css&#39;)
    ]
};

在这段代码的顶部,我们导入了ExtractTextPlugin,并使用这个插件改造了之前的CSS Loader。这里的作用是,css-loader的处理结果不再直接返回给Webpack,而是传递给ExtractTextPlugin。在底部我们配置了这个Plugin。

这里配置的作用是,对于传递给ExtractTextPlugin的CSS样式数据,将会被保存在名为style.css的文件中。这样做的好处与之前处理JavaScript时一样,我们可以将多个独立的CSS文件合并为一个文件,从而减少加载样式时的下载次数。

最终我们可以直接使用我们合并好的CSS,实现和之前一致的效果。

<html>
  <head>
    <link rel="stylesheet" href="dist/style.css"/>
    <script src="./dist/bundle.js""></script>
  </head>
</html>

使用Webpack处理图片

现在我们开始尝试向应用中添加图片,并让Webpack来协助我们处理这些图片。这里我们添加了两张图片,一个用于求和,一个用于求积。为了让Webpack有能力处理这些图片,我们使用这两个Loader:

  • image-webpack-loader: 尝试帮助我们自动压缩图片体积;

  • url-loader: 如果image-webpack-loader的输出图片体积小,就内联使用这些图片,如果image-webpack-loader的输出图片体积大,就将图像包含在输出目录中;

我们准备了两张图片,用于求积的图片(multiply.png)相对较大,用于求和的图片(sum.png)相对较小。首先,我们添加一个图片工具方法,这个方法会为我们创建图片,并将图片添加到文档中。

image_util.js

const addImageToPage = (imageSrc) => {
    const image = document.createElement(&#39;img&#39;);
    image.src = imageSrc;
    image.style.height = &#39;100px&#39;;
    image.style.width = &#39;100px&#39;;
    document.body.appendChild(image);
};
export default addImageToPage;

接着,让我们导入这个图片工具方法,以及我们想要添加到index.js中的图片。

import multiply from &#39;./multiply&#39;;
import sum from &#39;./sum&#39;;

// import our image utility
import addImageToPage from &#39;./image_util&#39;;

// import the images we want to use
import multiplyImg from &#39;../images/multiply.png&#39;;
import sumImg from &#39;../images/sum.png&#39;;

// import the CSS we want to use here
import &#39;./math_output.css&#39;;

const totalMultiply = multiply(5, 3);
const totalSum = sum(5, 3);

// create the body
const body = document.createElement("body");
document.documentElement.appendChild(body);

// calculate the product and add it to a span
const multiplyResultsSpan = document.createElement(&#39;span&#39;);
multiplyResultsSpan.appendChild(document.createTextNode(`Product of 5 and 3 = ${totalMultiply}`));

// calculate the sum and add it to a span
const sumResultSpan = document.createElement(&#39;span&#39;);
sumResultSpan.appendChild(document.createTextNode(`Sum of 5 and 3 = ${totalSum}`));

// add the results to the page
addImageToPage(multiplyImg);
document.body.appendChild(multiplyResultsSpan);
addImageToPage(sumImg);
document.body.appendChild(sumResultSpan);

最后,我们还是来修改webpack.config.js,配置两个新的Loader来处理这些图片。

const path = require(&#39;path&#39;);
const ExtractTextPlugin = require(&#39;extract-text-webpack-plugin&#39;);
module.exports = {
    entry: &#39;./src/index.js&#39;,
    output: {
        path: path.resolve(__dirname, &#39;./dist/&#39;),
        filename: &#39;bundle.js&#39;,
        publicPath: &#39;dist/&#39;
    },
    module: {
        loaders: [
            {
                test: /.js$/,
                loader: &#39;babel-loader&#39;,
                exclude: /node_modules/,
                query: {
                    presets: [&#39;es2015&#39;]
                }
            },
            {
                test: /.css$/,
                loader: ExtractTextPlugin.extract(&#39;css-loader&#39;)
            },
            {
                test: /.png$/,
                loaders: [
                    &#39;url-loader?limit=5000&#39;,
                    &#39;image-webpack-loader&#39;
                ]
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin(&#39;style.css&#39;)
    ]
};

还是老规矩,我们来看看新增的参数都表示什么含义。

  • output.publicPath: 让url-loader知道,保存到磁盘的文件需要添加指定的前缀。例如,我们需要保存一个output_file.png,那么最终保存的路径应该是dist/output_file.png
  • test: 还是和之前一样,通过正则表达式匹配图像文件,非图像文件不处理;
  • loaders: 这里还是要再强调一下,Webpack从右向左处理loader,因此image-webpack-loader的处理结果将会被传递给url-loader继续处理。

现在我们执行Webpack打包,会得到下面三个东西。

38ba485a2e2306d9ad96d479e36d2e7b.png
bundle.js
style.css

这里的38ba485a2e2306d9ad96d479e36d2e7b.png实际上就是我们的大图片multiply.png,较小的图片sum.png会被内联到bundle.js中,就像下面这样。

module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAMAAAACDyzWAAAC6FBMVEUAuv8AgL...."

这其实相当于

img.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAMAAAACDyzWAAAC6FBMVEUAuv8AgL..."

更多编程相关知识,请访问:编程视频!!

以上是Webpack是什麼?詳解它是如何運作的?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除