在模块化被引入JavaScript的早期,并没有在浏览器中运行模块的本地支持。在Node.js中使用CommonJS蓝图实现了对模块化编程的支持,并且被那些使用JavaScript构建服务器端应用程序的人所采用。
这对大型网络应用来说也很有前景,因为开发者可以避免命名空间的冲突,并通过以更模块化的模式编写代码来建立更可维护的代码库。但仍有一个挑战:模块不能在网络浏览器中使用,而网络浏览器通常是执行JavaScript的地方。
为了解决这个问题,webpack、Parcel、Rollup以及谷歌的Closure Compiler等模块捆绑器被编写出来,以创建优化的代码包,供终端用户的浏览器下载和执行。
捆绑 "你的代码是什么意思?
捆绑代码是指将多个模块合并和优化成一个或多个可生产的捆绑代码。这里提到的捆绑可以更好地理解为整个捆绑过程的最终产品。
在这篇文章中,我们将重点讨论webpack,这是一个由Tobias Koppers编写的工具,随着时间的推移,它已经发展成为JavaScript工具链中的一个主要工具,经常被用于大型和小型项目。
注意: 为了从这篇文章中受益,最好熟悉JavaScript模块。你还需要节点安装在你的本地机器上,这样你就可以在本地安装和使用webpack。
什么是webpack?
webpack是一个高度可扩展和可配置的静态模块捆绑器,用于JavaScript应用程序。凭借其可扩展性,你可以插入外部加载器和插件来实现你的最终目标。
如下图所示,webpack从根入口点开始浏览你的应用程序,建立由直接或间接作用于根文件的依赖关系组成的依赖关系图,并产生组合模块的优化包。

为了理解webpack是如何工作的,我们需要理解它所使用的一些术语。这些术语在本文中经常使用,而且在webpack的文档中也经常被引用。
- Chunk
一个chunk指的是从模块中提取的代码。这些代码将被存储在一个chunk文件中。在使用webpack进行代码拆分时,通常会用到chunk。 - 模块
模块是你的应用程序中被分解的部分,你导入这些模块来执行特定的任务或功能。Webpack支持使用ES6、CommonJS和AMD语法创建的模块。 - Assets
assets这个词在webpack和其他捆绑程序中经常被使用。它指的是在构建过程中捆绑的静态文件。这些文件可以是任何东西,从图片到字体甚至是视频文件。当你进一步阅读这篇文章时,你会看到我们如何使用加载器来处理不同的资产类型。
一旦我们理解了什么是webpack和它使用的术语,让我们看看它们是如何应用于为一个演示项目整理配置文件的。
注意:你还需要安装webpack-cli ,以便在你的机器上使用webpack。如果没有安装,终端会提示你安装它。
webpack 配置文件
除了从终端使用webpack-cli之外,你还可以通过一个配置文件在你的项目中使用webpack。但随着webpack的最新版本,我们可以在我们的项目中使用webpack而不需要配置文件。然后我们把webpack 作为我们的package.json 文件中的一个命令的值,而不加任何标志。这样一来,webpack就会认为你的项目的入口文件是在src 目录中。它将捆绑入口文件并将其输出到dist 目录中。
一个例子是下面的package.json 文件样本。我们使用webpack来捆绑应用程序,不需要配置文件。
{
"name" : "Smashing Magazine",
"main": "index.js",
"scripts": {
"build" : "webpack"
},
"dependencies" : {
"webpack": "^5.24.1"
}
}
当运行它上面文件中的构建命令时,webpack会将文件捆绑在src/index.js 目录下,并在dist 目录下的main.js 文件中输出。然而,webpack要比这灵活得多。我们可以通过编辑带有-- config 标志的配置文件来改变入口点、调整输出点和完善许多其他的默认行为。
一个例子是上面的package.json 文件中修改过的构建命令。
"build" : "webpack --config webpack.config.js"
上面,我们添加了--config 标志,并指定了一个webpack.config.js 作为拥有新的webpack配置的文件。
但是,webpack.config.js 文件还不存在。所以我们需要在我们的应用程序目录中创建它,并将下面的代码粘贴到该文件中。
# webpack.config.js
const path = require("path")
module.exports = {
entry : "./src/entry",
output : {
path: path.resolve(__dirname, "dist"),
filename: "output.js"
}
}
上面的文件仍然配置了webpack来捆绑你的JavaScript文件,但现在我们可以定义一个自定义的入口和输出文件路径,而不是webpack使用的默认路径。
关于webpack配置文件,有几件事需要注意。
- webpack配置文件是一个JavaScript文件,写成一个JavaScriptCommonJS模块。
- 一个webpack配置文件导出了一个具有多个属性的对象。这些属性中的每一个都是在捆绑你的代码时用来配置webpack的选项。一个例子是
mode选项。mode
在配置中,该选项用于在捆绑过程中设置NODE_ENV值。它可以有一个production或development的值。如果没有指定,它将默认为none。同样重要的是要注意,webpack会根据mode的值来捆绑你的资产。例如,webpack会在开发模式下自动缓存你的捆绑文件,以优化和减少捆绑时间。
webpack的概念
当通过CLI或通过配置文件配置webpack时,有四个主要概念作为选项被应用。本文的下一节将重点介绍这些概念,并在为一个演示的Web应用程序构建配置时应用它们。
值得注意的是,下面解释的概念与其他模块捆绑器有一些相似之处。例如,当用配置文件使用Rollup时,你可以定义一个输入字段来指定依赖图的入口点,一个输出对象来配置产生的块的放置方式和位置,还有一个插件对象用于添加外部插件。
输入字段
配置文件中的输入字段包含了webpack开始构建依赖图的文件的路径。从这个入口文件开始,webpack将进入其他直接或间接依赖该入口点的模块。
你的配置的入口点可以是一个具有单一文件值的单一入口类型,类似于下面的例子。
# webpack.configuration.js
module.exports = {
mode: "development",
entry : "./src/entry"
}
入口点也可以是一个多主入口类型,有一个包含多个入口文件路径的数组,与下面的例子类似。
# webpack.configuration.js
const webpack = require("webpack")
module.exports = {
mode: "development",
entry: [ './src/entry', './src/entry2' ],
}
输出
正如其名称所暗示的,一个配置的输出字段是创建的捆绑文件的位置。当你有几个模块的时候,这个字段就会很方便。你可以指定自己的文件名,而不是使用webpack生成的名字。
# webpack.configuration.js
const webpack = require("webpack");
const path = require("path");
module.exports = {
mode: "development",
entry: './src/entry',
output: {
filename: "webpack-output.js",
path: path.resolve(__dirname, "dist"),
}
}
装载器
默认情况下,webpack只理解你应用程序中的JavaScript文件。然而,webpack将每一个作为模块导入的文件都视为一个依赖,并将其添加到依赖图中。为了处理静态资源,如图片、CSS文件、JSON文件,甚至是你存储在CSV中的数据,webpack使用加载器将这些文件 "加载 "到包中。
加载器非常灵活,可以用于很多方面,从转译你的ES代码,到处理你的应用程序的样式,甚至用ESLint对你的代码进行提示。
有三种方法可以在你的应用程序中使用加载器。其中一种是通过内联的方式,直接在文件中导入它。例如,为了最小化图片尺寸,我们可以在文件中直接使用image-loader 载入器,如下图所示。
// main.js
import ImageLoader from 'image-loader'
另一个使用加载器的首选方案是通过你的webpack配置文件。这样,你可以用加载器做更多的事情,比如指定你想应用加载器的文件类型。要做到这一点,我们创建一个rules 数组,并在一个对象中指定加载器,每个加载器都有一个测试字段,其中有一个与我们想应用加载器的资产相匹配的regex表达式。
例如,在前面的例子中直接导入image-loader ,我们可以在webpack配置文件中使用文档中的最基本选项。这将看起来像这样。
# webpack.config.js
const webpack = require("webpack")
const path = require("path")
const merge = require("webpack-merge")
module.exports = {
mode: "development",
entry: './src/entry',
output: {
filename: "webpack-output.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
'img-loader'
]
}
]
}
}
仔细看一下包含上述image-loader 的对象中的test 字段。我们可以发现匹配所有图片文件的regex表达式:jp(e)g 、png 、gif 和svg 格式。
使用Loader的最后一种方法是通过CLI使用--module-bind 标志。
awesome-webpackreadme包含了一个详尽的加载器列表,你可以在webpack中使用这些加载器,每个加载器都被归入它们所执行的操作类别。下面是一些你可能会发现在你的应用程序中很方便的加载器。
- [响应式加载器(Responsive-loader]你会发现这个加载器在添加图片以适应你的响应式网站或应用程序时非常有用。它从一张图片创建多个不同尺寸的图片,并返回一个与图片相匹配的
srcset,以便在适当的显示屏幕尺寸下使用。 - [Babel-loader] 这用于将你的JavaScript代码从现代ECMA语法转为ES5。
- [GraphQL加载器]
如果你是一个GraphQL爱好者,你会发现这个加载器很有帮助,因为它可以加载你的
.graphql文件,其中包含你的GraphQL模式、查询和突变--同时还有启用验证的选项。
插件
插件的使用允许webpack编译器对捆绑模块产生的块执行任务。虽然webpack不是一个任务运行器,但通过插件,我们可以执行一些加载器在捆绑代码时无法执行的自定义操作。
webpack插件的一个例子是webpack内置的ProgressPlugin。它提供了一种方法来定制编译过程中在控制台中打印出来的进度。
# webpack.config.js
const webpack = require("webpack")
const path = require("path")
const merge = require("webpack-merge")
const config = {
mode: "development",
entry: './src/entry',
output: {
filename: "webpack-output.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
'img-loader'
]
}
]
},
plugins: [
new webpack.ProgressPlugin({
handler: (percentage, message ) => {
console.info(percentage, message);
},
})
]
}
module.exports = config
通过上面配置中的Progress插件,我们提供了一个处理函数,它将在编译过程中打印出编译百分比和信息到控制台。

下面是来自awesome-webpackreadme的几个插件,你会发现它们在你的webpack应用中很方便。
- 离线插件
这个插件首先利用服务工作者或AppCache(如果有的话),为webpack管理的项目提供一个离线体验。 - Purgecss-webpack-plugin
当试图优化你的webpack项目时,这个插件很方便,因为它在编译过程中会删除应用程序中未使用的CSS。
在这一点上,我们已经为一个相对较小的应用程序完全设置了第一个webpack配置。让我们进一步考虑如何在我们的应用程序中用webpack做某些事情。
处理多个环境
在你的应用程序中,你可能需要为开发或生产环境配置不同的webpack。例如,你可能不希望webpack在你的生产环境中每次向持续集成管道进行新的部署时都输出小的警告日志。
根据webpack和社区的建议,有几种方法可以实现这一点。一种方法是将你的配置文件转换为导出一个返回对象的函数。这样,当前环境将被webpack编译器作为第一个参数传入该函数,而其他选项则作为第二个参数。
如果你想根据当前环境以不同的方式执行一些操作,这种处理你的webpack环境的方法会很方便。然而,对于具有更复杂配置的大型应用程序,你最终可能会得到一个包含大量条件语句的配置。
下面的代码片段显示了一个如何使用functions 方法处理同一文件中的production 和development 环境的例子。
// webpack.config.js
module.exports = function (env, args) {
return {
mode : env.production ? 'production' : 'development',
entry: './src/entry',
output: {
filename: "webpack-output.js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
env.development && (
new webpack.ProgressPlugin({
handler: (percentage, message ) => {
console.info(percentage, message);
},
})
)
]
}
}
通过上面代码片断中的导出函数,你会看到传递到函数中的env 参数是如何与三元操作符一起使用来切换值的。它首先被用来设置webpack模式,然后它也被用来只在开发模式下启用ProgressPlugin。
另一种更优雅的处理生产和开发环境的方法是为这两种环境创建不同的配置文件。一旦我们这样做了,我们就可以在捆绑应用程序时在package.json 脚本中使用它们的不同命令。看一下下面的片段。
{
"name" : "smashing-magazine",
"main" : "index.js"
"scripts" : {
"bundle:dev" : "webpack --config webpack.dev.config.js",
"bundle:prod" : "webpack --config webpack.prod.config.js"
},
"dependencies" : {
"webpack": "^5.24.1"
}
}
在上面的package.json ,我们有两个脚本命令,每个都使用不同的配置文件,在捆绑应用程序的资产时处理特定环境。现在你可以在开发模式下使用npm run bundle:dev ,或者在创建生产就绪的捆绑程序时使用npm run bundle:prod ,来捆绑你的应用程序。
使用第二种方法,你可以避免从函数中返回配置对象时引入的条件性语句。然而,现在你还必须维护多个配置文件。
分割配置文件
在这一点上,我们的webpack配置文件是38行代码(LOC)。这对于一个只有一个加载器和一个插件的演示应用程序来说是很好的。
但对于一个更大的应用程序,我们的webpack配置文件肯定会更长,因为有几个加载器和插件,每个都有自己的自定义选项。为了保持配置文件的整洁和可读性,我们可以将配置分割成多个小对象,然后使用webpack-merge包将这些配置对象合并到一个基本文件中。
为了将其应用于我们的webpack项目,我们可以将单个配置文件分成三个小文件:一个用于加载器,一个用于插件,最后一个文件作为基础配置文件,我们将其他两个文件放在一起。
创建一个webpack.plugin.config.js 文件,并将下面的代码粘贴到其中,以使用带有额外选项的插件。
// webpack.plugin.config.js
const webpack = require('webpack')
const plugin = [
new webpack.ProgressPlugin({
handler: (percentage, message ) => {
console.info(percentage, message);
},
})
]
module.exports = plugin
上面,我们有一个单一的插件,是我们从webpack.configuration.js 文件中提取的。
接下来,用下面的代码创建一个webpack.loader.config.js 文件,用于webpack加载器。
// webpack.loader.config.js
const loader = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
'img-loader'
]
}
]
}
}
在上面的代码块中,我们把webpackimg-loader 移到一个单独的文件中。
最后,创建一个webpack.base.config.js 文件,其中webpack应用程序的基本输入和输出配置将与上述两个创建的文件一起保存。
// webpack.base.config.js
const path = require("path")
const merge = require("webpack-merge")
const plugins = require('./webpack.plugin.config')
const loaders = require('./webpack.loader.config')
const config = merge(loaders, plugins, {
mode: "development",
entry: './src/entry',
output: {
filename: "webpack-output.js",
path: path.resolve(__dirname, "dist"),
}
});
module.exports = config
看一眼上面的webpack文件,你可以观察到它与原来的webpack.config.js 文件相比是多么紧凑。现在,配置的三个主要部分已经被分解成较小的文件,可以单独使用。
优化大型构建
随着你在一段时间内不断地开发你的应用程序,你的应用程序在功能和规模上肯定会越来越大。在这种情况下,新的文件会被创建,旧的文件会被修改或重构,新的外部包会被安装--所有这些都会导致webpack发出的包的大小增加。
默认情况下,如果你的配置模式被设置为production ,webpack会自动尝试代表你优化包。例如,webpack默认应用的一项技术(从webpack 4+开始)是Tree-Shaking,以优化和减少你的包的大小。从本质上讲,这是一种优化技术,用于删除未使用的代码。在捆绑过程中,在简单的层面上,导入和导出语句被用来检测未使用的模块,然后将它们从发出的捆绑中移除。
你也可以通过在你的配置文件中添加一个带有某些字段的optimization 对象来手动优化你的应用程序捆绑。webpack文档的优化部分包含一个完整的字段列表,你可以在optimization 对象中使用,嗯,优化你的应用程序。让我们考虑20个记录的字段中的一个。
minimize
这个布尔字段是用来指示webpack最小化包的大小。默认情况下,webpack会尝试使用TerserPlugin来实现这一点,TerserPlugin是webpack提供的一个代码最小化包。
"最小化适用于通过从代码中删除不必要的数据来最小化你的代码,这反过来又减少了处理后产生的代码大小。"
我们还可以通过在optimization 对象中添加一个minimizer 数组字段来使用其他首选的最小化器。下面是使用Uglifyjs-webpack-plugin的一个例子。
// webpack.config.js
const Uglify = require("uglifyjs-webpack-plugin")
module.exports = {
optimization {
minimize : true,
minimizer : [
new Uglify({
cache : true,
test: /\.js(\?.*)?$/i,
})
]
}
}
上面,uglifyjs-webpack-plugin 被用作minifier,有两个相当重要的选项。首先,启用cache ,意味着Uglify只会在新的修改时对现有文件进行最小化,而test 选项则指定了我们要最小化的特定文件类型。
注意: uglifyjs-webpack-plugin给出了一个全面的选项列表,在用它来最小化你的代码时可以使用。
一个小小的优化演示
让我们手动尝试优化一个演示程序,在一个更大的项目中应用一些字段,看看有什么不同。虽然我们不会深入优化应用程序,但我们会看到在development 模式下运行webpack与在production 模式下运行webpack之间的包大小差异。
在这个演示中,我们将使用一个用Electron构建的桌面应用程序,它的用户界面也使用了React.js--所有这些都与webpack捆绑在一起。Electron和React.js听起来是一个相当重的组合,可能会产生一个更大的捆绑包。
要在本地试用该演示,请从GitHub仓库克隆该应用程序,并使用下面的命令安装依赖项。
# clone repository
git clone https://github.com/vickywane/webpack-react-demo.git
# change directory
cd demo-electron-react-webpack
# install dependencies
npm install
这个桌面应用程序相当简单,只有一个使用styled-components的单页样式。当用yarn start 命令启动桌面应用程序时,单页显示从CDN获取的图片列表,如下图所示。

让我们先创建一个这个应用程序的开发包,不做任何手动优化,以分析最终的包的大小。
从项目目录下的终端运行yarn build:dev ,将创建开发捆绑包。另外,它将向你的终端打印出以下统计数据。

该命令将向我们显示整个编译和发射的捆绑包的统计数据。
请注意mainRenderer.js 大块是在1.11 Mebibyte(大约1.16 MB)。mainRenderer 是Electron应用程序的入口点。
接下来,让我们在webpack.base.config.js 文件中添加uglifyjs-webpack-plugin作为一个已安装的插件,用于代码的最小化。
// webpack.base.config.js
const Uglifyjs = require("uglifyjs-webpack-plugin")
module.exports = {
plugins : [
new Uglifyjs({
cache : true
})
]
}
最后,让我们在production 模式下用webpack运行捆绑应用程序。从你的终端运行yarn build:prod 命令将在你的终端上输出以下数据。

请注意这次的mainRenderer 块。它已经下降到了惊人的182Kibytes(大约186KB),这比之前发出的mainRenderer chunk大小的80%还多!
让我们使用webpack-bundler-analyzer来进一步观察发射的捆绑物。使用yarn add webpack-bundle-analyzer 命令安装该插件,并修改webpack.base.config.js 文件,使其包含以下添加该插件的代码。
// webpack.base.config.js
const Uglifyjs = require("uglifyjs-webpack-plugin");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer");
.BundleAnalyzerPlugin;
const config = {
plugins: [
new Uglifyjs({
cache : true
}),
new BundleAnalyzerPlugin(),
]
};
module.exports = config;
从你的终端运行yarn build:prod ,对要重新捆绑的应用程序进行捆绑。默认情况下,webpack-bundle-analyzer将启动一个HTTP服务器,在你的浏览器中提供可视化的捆绑物概述。

从上面的图片中,我们可以看到发出的包和包内的文件大小的可视化表示。在视觉上,我们可以观察到,在文件夹node_modules ,最大的文件是react-dom.production.min.js ,其次是stylis.min.js 。
使用分析器可视化的文件大小,我们将更好地了解什么安装的包贡献了包的主要部分。然后,我们可以寻找方法来优化它,或者用一个更轻的包来代替它。
注: webpack-analyzer-plugin文档列出了其他可用于显示从你发射的包中创建的分析的方法。
webpack社区
webpack的优势之一是其背后庞大的开发者社区,这对第一次尝试webpack的开发者来说有很大的帮助。就像这篇文章一样,在使用webpack时,有几篇文章、指南和资源与文档一起,作为一个很好的指南。
摘要
经过七年的发展,webpack已经真正证明了自己是大量项目使用的JavaScript工具链的重要组成部分。这篇文章只是对webpack的灵活性和可扩展性所能实现的东西做了一个概述。
下次你需要为你的应用程序选择一个模块捆绑器时,希望你能更好地理解Webpack的一些核心概念,它所解决的问题,以及设置配置文件的步骤。